diff --git a/application/src/main/data/json/demo/dashboards/firmware.json b/application/src/main/data/json/demo/dashboards/firmware.json index 4711e34702..cfd0ed68c4 100644 --- a/application/src/main/data/json/demo/dashboards/firmware.json +++ b/application/src/main/data/json/demo/dashboards/firmware.json @@ -223,6 +223,27 @@ "funcBody": null, "usePostProcessing": null, "postFuncBody": null + }, + { + "name": "fw_url", + "type": "attribute", + "label": "fw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.4204673738685043, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ] } @@ -249,23 +270,23 @@ "icon": "edit", "type": "customPretty", "customHtml": "
\n \n

Edit firmware {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", - "customCss": "", + "customCss": "form {\n min-width: 300px !important;\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customResources": [], "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" }, { - "name": "Download firware", + "name": "Download firmware", "icon": "file_download", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n\n }\n });\n }\n }\n );\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "12533058-42f6-e75f-620c-219c48d01ec0" }, { - "name": "Copy checksum", + "name": "Copy checksum/URL", "icon": "content_copy", "type": "custom", - "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n}", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Firmware direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" } ] @@ -997,6 +1018,27 @@ "funcBody": null, "usePostProcessing": null, "postFuncBody": null + }, + { + "name": "fw_url", + "type": "attribute", + "label": "fw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.4204673738685043, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ] } @@ -1023,23 +1065,23 @@ "icon": "edit", "type": "customPretty", "customHtml": "
\n \n

Edit firmware {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", - "customCss": "", + "customCss": "form {\n min-width: 300px !important;\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customResources": [], "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" }, { - "name": "Download firware", + "name": "Download firmware", "icon": "file_download", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n\n }\n });\n }\n }\n );\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "12533058-42f6-e75f-620c-219c48d01ec0" }, { - "name": "Copy checksum", + "name": "Copy checksum/URL", "icon": "content_copy", "type": "custom", - "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n}", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Firmware direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" } ] @@ -1273,6 +1315,27 @@ "funcBody": null, "usePostProcessing": null, "postFuncBody": null + }, + { + "name": "fw_url", + "type": "attribute", + "label": "fw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.4204673738685043, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ] } @@ -1299,23 +1362,23 @@ "icon": "edit", "type": "customPretty", "customHtml": "
\n \n

Edit firmware {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", - "customCss": "", + "customCss": "form {\n min-width: 300px !important;\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customResources": [], "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" }, { - "name": "Download firware", + "name": "Download firmware", "icon": "file_download", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n\n }\n });\n }\n }\n );\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "12533058-42f6-e75f-620c-219c48d01ec0" }, { - "name": "Copy checksum", + "name": "Copy checksum/URL", "icon": "content_copy", "type": "custom", - "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n}", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Firmware direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" } ] @@ -1549,6 +1612,27 @@ "funcBody": null, "usePostProcessing": null, "postFuncBody": null + }, + { + "name": "fw_url", + "type": "attribute", + "label": "fw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.4204673738685043, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ] } @@ -1575,23 +1659,23 @@ "icon": "edit", "type": "customPretty", "customHtml": "
\n \n

Edit firmware {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", - "customCss": "", + "customCss": "form {\n min-width: 300px !important;\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customResources": [], "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" }, { - "name": "Download firware", + "name": "Download firmware", "icon": "file_download", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n\n }\n });\n }\n }\n );\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "12533058-42f6-e75f-620c-219c48d01ec0" }, { - "name": "Copy checksum", + "name": "Copy checksum/URL", "icon": "content_copy", "type": "custom", - "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n}", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Firmware direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" } ] @@ -1825,6 +1909,27 @@ "funcBody": null, "usePostProcessing": null, "postFuncBody": null + }, + { + "name": "fw_url", + "type": "attribute", + "label": "fw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.4204673738685043, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ] } @@ -1851,23 +1956,23 @@ "icon": "edit", "type": "customPretty", "customHtml": "
\n \n

Edit firmware {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", - "customCss": "", + "customCss": "form {\n min-width: 300px !important;\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n firmwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n firmwareId: vm.entity.firmwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.firmwareId = formValues.firmwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", "customResources": [], "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" }, { - "name": "Download firware", + "name": "Download firmware", "icon": "file_download", "type": "custom", - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n\n }\n });\n }\n }\n );\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceFirmware();\n\nfunction getDeviceFirmware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(data.firmwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.firmwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.firmwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "12533058-42f6-e75f-620c-219c48d01ec0" }, { - "name": "Copy checksum", + "name": "Copy checksum/URL", "icon": "content_copy", "type": "custom", - "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n}", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_checksum');\nvar checksum = data.data[0][1];\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Firmware checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'fw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Firmware direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not firmware set.', 2000, 'top');\n }\n}", "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" } ] @@ -1936,7 +2041,7 @@ } }, "device_firmware_history": { - "name": "Device Firmware history", + "name": "Firmware history: ${entityName}", "root": false, "layouts": { "main": { @@ -2379,7 +2484,8 @@ "titleColor": "rgba(0,0,0,0.870588)", "showFilters": true, "showDashboardLogo": false, - "dashboardLogoUrl": null + "dashboardLogoUrl": null, + "showUpdateDashboardImage": false } }, "name": "Firmware" diff --git a/application/src/main/data/json/demo/dashboards/software.json b/application/src/main/data/json/demo/dashboards/software.json new file mode 100644 index 0000000000..84316fa5fd --- /dev/null +++ b/application/src/main/data/json/demo/dashboards/software.json @@ -0,0 +1,2492 @@ +{ + "title": "Software", + "image": null, + "configuration": { + "description": "", + "widgets": { + "cd03188e-cd9d-9601-fd57-da4cb95fc016": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "entities_table", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "timewindow": { + "realtime": { + "interval": 1000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "enableSearch": true, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityLabel", + "displayEntityName": false, + "displayEntityType": false, + "enableSelectColumnDisplay": false, + "enableStickyHeader": true, + "enableStickyAction": false, + "entitiesTitle": "Devices", + "displayEntityLabel": true, + "entityLabelColumnTitle": "Device" + }, + "title": "New Entities table", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "8fdb88d0-50ac-2232-fdb7-69c30c16544e", + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current SW title", + "color": "#2196f3", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.09545533885166413, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current SW version", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.7206056602328659, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target SW title", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.9934225682766313, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target SW version", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5251724416842531, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_ts", + "type": "timeseries", + "label": "Target SW set time", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellContentFunction": "if (value !== '') {\n return ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');\n}\nreturn '';" + }, + "_hash": 0.31823244858578237, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Progress", + "color": "#9c27b0", + "settings": { + "columnWidth": "30%", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "return {\n 'padding-right': '30px'\n}", + "cellContentFunction": "if (value !== '') {\n var mapProgress = {\n 'QUEUED': 0,\n 'INITIATED': 5,\n 'DOWNLOADING': 10,\n 'DOWNLOADED': 55,\n 'VERIFIED': 60,\n 'UPDATING': 70,\n 'FAILED': 99,\n 'UPDATED': 100\n }\n var color = 'mat-primary';\n var progress = mapProgress[value];\n if (value == 'FAILED') {\n color = 'mat-accent';\n }\n return `
`;\n}" + }, + "_hash": 0.8174211757846257, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#f44336", + "settings": { + "columnWidth": "130px", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "if (value == 'FAILED') {\n return {'color' : '#D93025'};\n}\nreturn {};", + "cellContentFunction": "function icon(value) {\n if (value == 'QUEUED') {\n return '';\n }\n if (value == 'INITIATED' || value == 'DOWNLOADING' || value == 'DOWNLOADED') {\n return '';\n }\n if (value == 'VERIFIED' || value == 'UPDATING' ) {\n return 'update';\n }\n if (value == 'UPDATED') {\n return '';\n }\n if (value == 'FAILED') {\n return 'warning';\n }\n return '';\n}\nfunction capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\n\nreturn icon(value) + '' + capitalize(value) + '';" + }, + "_hash": 0.7764426948615217, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_checksum", + "type": "attribute", + "label": "sw_checksum", + "color": "#3f51b5", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.5594087842471693, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_url", + "type": "attribute", + "label": "sw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.3355829384124256, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "actions": { + "actionCellButton": [ + { + "name": "History software update", + "icon": "history", + "type": "openDashboardState", + "targetDashboardStateId": "device_software_history", + "setEntityId": true, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "98a1406c-3301-bc2f-2c5d-d637ce3b663b" + }, + { + "name": "Edit software", + "icon": "edit", + "type": "customPretty", + "customHtml": "
\n \n

Edit software {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", + "customCss": "form {\n min-width: 300px !important;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", + "customResources": [], + "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" + }, + { + "name": "Download software", + "icon": "file_download", + "type": "custom", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceSoftware();\n\nfunction getDeviceSoftware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.softwareId !== null) {\n otaPackageService.downloadOtaPackage(data.softwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.softwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.softwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "12533058-42f6-e75f-620c-219c48d01ec0" + }, + { + "name": "Copy checksum/URL", + "icon": "content_copy", + "type": "custom", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_checksum');\nvar checksum = data.data[0][1];\nconsole.log(checksum);\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Software checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Software direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "widgetStyle": {} + }, + "row": 0, + "col": 0, + "id": "cd03188e-cd9d-9601-fd57-da4cb95fc016" + }, + "100b756c-0082-6505-3ae1-3603e6deea48": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "timeseries_table", + "type": "timeseries", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 8, + "sizeY": 6.5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "19f41c21-d9af-e666-8f50-e1748778f955", + "filterId": null, + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current software title", + "color": "#2196f3", + "settings": { + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.5978079905579401, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current software version", + "color": "#4caf50", + "settings": { + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.027392025058568192, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target software title", + "color": "#f44336", + "settings": { + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.9496350796287059, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target software version", + "color": "#ffc107", + "settings": { + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.6734152252264187, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#607d8b", + "settings": { + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "" + }, + "_hash": 0.2983399718643074, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "function capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\nif (value !== '') {\n return capitalize(value);\n}\nreturn value;" + } + ] + } + ], + "timewindow": { + "hideInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "timewindowMs": 2592000000, + "quickInterval": "CURRENT_DAY", + "interval": 1000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": false, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "showTimestamp": true, + "displayPagination": true, + "defaultPageSize": 10, + "enableSearch": true, + "enableStickyHeader": true, + "enableStickyAction": true + }, + "title": "Software history", + "dropShadow": false, + "enableFullscreen": false, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "widgetStyle": {}, + "actions": {}, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "displayTimewindow": true, + "titleTooltip": "" + }, + "row": 0, + "col": 0, + "id": "100b756c-0082-6505-3ae1-3603e6deea48" + }, + "17543c57-af4a-2c1e-bf12-53a7b46791e6": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "html_value_card", + "type": "latest", + "title": "New widget", + "sizeX": 8, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": "", + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "19a0ad1c-b31d-4a29-9d7b-5d87e2a8ea6e", + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "waitingDevicesNumber", + "color": "#4caf50", + "settings": {}, + "_hash": 0.7404827038869322, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "cardHtml": "
\n
\n \n
\n ${waitingDevicesNumber:0}\n
\n
\n Device Waiting\n
\n
\n
", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n border: 1px solid #E0E0E0;\n box-sizing: border-box;\n}\n\n.card .content {\n padding: 20px 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n box-sizing: border-box;\n}\n\n.card .value {\n margin: 18px 0 5px;\n font-weight: 500;\n font-size: 3em;\n line-height: 1.1em;\n text-align: center;\n letter-spacing: -0.02em;\n color: #333333;\n}\n\n.card .description {\n font-size: 1em;\n line-height: 1.1em;\n color: #000000;\n opacity: 0.6;\n text-align: center;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 960px) and (max-width: 1200px) {\n .card .content img {\n height: 28px; \n }\n \n .card .value {\n margin: 12px 0 5px;\n font-size: 2em;\n line-height: 1;\n }\n \n .card .description {\n font-size: 0.8em;\n line-height: 1;\n }\n}" + }, + "title": "New HTML Value Card", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": { + "elementClick": [ + { + "name": "activeDevices", + "icon": "more_horiz", + "type": "openDashboardState", + "targetDashboardStateId": "device_waiting", + "setEntityId": false, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "4d9a77a2-f0a5-690c-a83b-b0e940be788c" + } + ] + }, + "showTitleIcon": false, + "titleIcon": null, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "enableDataExport": false, + "displayTimewindow": true + }, + "id": "17543c57-af4a-2c1e-bf12-53a7b46791e6" + }, + "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "html_value_card", + "type": "latest", + "title": "New widget", + "sizeX": 8, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": "", + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "579f0468-9ce9-7e3e-b34c-88dd3de59897", + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "updatingDevicesNumber", + "color": "#4caf50", + "settings": {}, + "_hash": 0.7404827038869322, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "cardHtml": "
\n
\n \n
\n ${updatingDevicesNumber:0}\n
\n
\n Device Updating\n
\n
\n
", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n border: 1px solid #E0E0E0;\n box-sizing: border-box;\n}\n\n.card .content {\n padding: 20px 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n box-sizing: border-box;\n}\n\n.card .value {\n margin: 18px 0 5px;\n font-weight: 500;\n font-size: 3em;\n line-height: 1.1em;\n text-align: center;\n letter-spacing: -0.02em;\n color: #333333;\n}\n\n.card .description {\n font-size: 1em;\n line-height: 1.1em;\n color: #000000;\n opacity: 0.6;\n text-align: center;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 960px) and (max-width: 1200px) {\n .card .content img {\n height: 28px; \n }\n \n .card .value {\n margin: 12px 0 5px;\n font-size: 2em;\n line-height: 1;\n }\n \n .card .description {\n font-size: 0.8em;\n line-height: 1;\n }\n}" + }, + "title": "New HTML Value Card", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": { + "elementClick": [ + { + "name": "activeDevices", + "icon": "more_horiz", + "type": "openDashboardState", + "targetDashboardStateId": "device_updating", + "setEntityId": false, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "57d39904-2350-b29b-78ed-56b8268814cb" + } + ] + }, + "showTitleIcon": false, + "titleIcon": null, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "enableDataExport": false, + "displayTimewindow": true + }, + "id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6" + }, + "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "html_value_card", + "type": "latest", + "title": "New widget", + "sizeX": 8, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": "", + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "6044e198-df64-cd76-f339-696f220c4943", + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "updatedDevicesNumber", + "color": "#4caf50", + "settings": {}, + "_hash": 0.7404827038869322, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "cardHtml": "
\n
\n \n
\n ${updatedDevicesNumber:0}\n
\n
\n Device Updated\n
\n
\n
", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n border: 1px solid #E0E0E0;\n box-sizing: border-box;\n}\n\n.card .content {\n padding: 20px 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n box-sizing: border-box;\n}\n\n.card .value {\n margin: 18px 0 5px;\n font-weight: 500;\n font-size: 3em;\n line-height: 1.1em;\n text-align: center;\n letter-spacing: -0.02em;\n color: #333333;\n}\n\n.card .description {\n font-size: 1em;\n line-height: 1.1em;\n color: #000000;\n opacity: 0.6;\n text-align: center;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 960px) and (max-width: 1200px) {\n .card .content img {\n height: 28px; \n }\n \n .card .value {\n margin: 12px 0 5px;\n font-size: 2em;\n line-height: 1;\n }\n \n .card .description {\n font-size: 0.8em;\n line-height: 1;\n }\n}" + }, + "title": "New HTML Value Card", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": { + "elementClick": [ + { + "name": "activeDevices", + "icon": "more_horiz", + "type": "openDashboardState", + "targetDashboardStateId": "device_updated", + "setEntityId": false, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "d787c212-8c56-34f0-349a-5aae2ffd1eae" + } + ] + }, + "showTitleIcon": false, + "titleIcon": null, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "enableDataExport": false, + "displayTimewindow": true + }, + "id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81" + }, + "77b10144-b904-edd5-8c7c-8fb75616c6d8": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "html_value_card", + "type": "latest", + "title": "New widget", + "sizeX": 8, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entityCount", + "name": "", + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "bdbc6ea1-95a7-3912-341a-58dc7704a00f", + "dataKeys": [ + { + "name": "count", + "type": "count", + "label": "updatingDevicesNumber", + "color": "#4caf50", + "settings": {}, + "_hash": 0.7404827038869322, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": false, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "cardHtml": "
\n
\n
\n \n \n \n
\n
\n ${updatingDevicesNumber:0}\n
\n \n
\n Device Failed\n
\n
\n
", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n border: 1px solid #E0E0E0;\n box-sizing: border-box;\n}\n\n.card .content {\n padding: 20px 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n box-sizing: border-box;\n}\n\n.card .container-svg {\n height: 40px;\n width: 40px;\n}\n\n.card .value {\n margin: 18px 0 5px;\n font-weight: 500;\n font-size: 3em;\n line-height: 1.1em;\n text-align: center;\n letter-spacing: -0.02em;\n color: #333333;\n}\n\n.card .description {\n font-size: 1em;\n line-height: 1.1em;\n color: #000000;\n opacity: 0.6;\n text-align: center;\n letter-spacing: -0.02em;\n}\n\n@media (min-width: 960px) and (max-width: 1200px) {\n .card .container-svg {\n height: 28px;\n width: 28px;\n }\n \n .card .value {\n margin: 12px 0 5px;\n font-size: 2em;\n line-height: 1;\n }\n \n .card .description {\n font-size: 0.8em;\n line-height: 1;\n }\n}" + }, + "title": "New HTML Value Card", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showLegend": false, + "actions": { + "elementClick": [ + { + "name": "activeDevices", + "icon": "more_horiz", + "type": "openDashboardState", + "targetDashboardStateId": "device_error", + "setEntityId": false, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "0b3d2887-9929-84d5-3795-0763dca15cba" + } + ] + }, + "showTitleIcon": false, + "titleIcon": null, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "enableDataExport": false, + "displayTimewindow": true + }, + "id": "77b10144-b904-edd5-8c7c-8fb75616c6d8" + }, + "21be08bb-ec90-f760-ad6f-e7678f12c401": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "entities_table", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "timewindow": { + "realtime": { + "interval": 1000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "enableSearch": true, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityLabel", + "displayEntityName": false, + "displayEntityType": false, + "enableSelectColumnDisplay": false, + "enableStickyHeader": true, + "enableStickyAction": true, + "entitiesTitle": "Devices", + "displayEntityLabel": true, + "entityLabelColumnTitle": "Device" + }, + "title": "New Entities table", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "19a0ad1c-b31d-4a29-9d7b-5d87e2a8ea6e", + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current SW title", + "color": "#2196f3", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.09545533885166413, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current SW version", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.7206056602328659, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target SW title", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.9934225682766313, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target SW version", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5251724416842531, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_ts", + "type": "timeseries", + "label": "Target SW set time", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellContentFunction": "if (value !== '') {\n return ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');\n}\nreturn '';" + }, + "_hash": 0.31823244858578237, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Progress", + "color": "#9c27b0", + "settings": { + "columnWidth": "30%", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "return {\n 'padding-right': '30px'\n}", + "cellContentFunction": "if (value !== '') {\n var mapProgress = {\n 'QUEUED': 0,\n 'INITIATED': 5,\n 'DOWNLOADING': 10,\n 'DOWNLOADED': 55,\n 'VERIFIED': 60,\n 'UPDATING': 70,\n 'FAILED': 99,\n 'UPDATED': 100\n }\n var color = 'mat-primary';\n var progress = mapProgress[value];\n if (value == 'FAILED') {\n color = 'mat-accent';\n }\n return `
`;\n}" + }, + "_hash": 0.8174211757846257, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#f44336", + "settings": { + "columnWidth": "130px", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "if (value == 'FAILED') {\n return {'color' : '#D93025'};\n}\nreturn {};", + "cellContentFunction": "function icon(value) {\n if (value == 'QUEUED') {\n return '';\n }\n if (value == 'INITIATED' || value == 'DOWNLOADING' || value == 'DOWNLOADED') {\n return '';\n }\n if (value == 'VERIFIED' || value == 'UPDATING' ) {\n return 'update';\n }\n if (value == 'UPDATED') {\n return '';\n }\n if (value == 'FAILED') {\n return 'warning';\n }\n return '';\n}\nfunction capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\n\nreturn icon(value) + '' + capitalize(value) + '';" + }, + "_hash": 0.7764426948615217, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_checksum", + "type": "attribute", + "label": "sw_checksum", + "color": "#3f51b5", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.5594087842471693, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_url", + "type": "attribute", + "label": "sw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.3355829384124256, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "actions": { + "actionCellButton": [ + { + "name": "History software update", + "icon": "history", + "type": "openDashboardState", + "targetDashboardStateId": "device_software_history", + "setEntityId": true, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "98a1406c-3301-bc2f-2c5d-d637ce3b663b" + }, + { + "name": "Edit software", + "icon": "edit", + "type": "customPretty", + "customHtml": "
\n \n

Edit software {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", + "customCss": "form {\n min-width: 300px !important;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", + "customResources": [], + "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" + }, + { + "name": "Download software", + "icon": "file_download", + "type": "custom", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceSoftware();\n\nfunction getDeviceSoftware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.softwareId !== null) {\n otaPackageService.downloadOtaPackage(data.softwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.softwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.softwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "12533058-42f6-e75f-620c-219c48d01ec0" + }, + { + "name": "Copy checksum/URL", + "icon": "content_copy", + "type": "custom", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_checksum');\nvar checksum = data.data[0][1];\nconsole.log(checksum);\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Software checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Software direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "widgetStyle": {} + }, + "row": 0, + "col": 0, + "id": "21be08bb-ec90-f760-ad6f-e7678f12c401" + }, + "e8280043-d3dc-7acb-c2ff-a4522972ff91": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "entities_table", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "timewindow": { + "realtime": { + "interval": 1000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "enableSearch": true, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityLabel", + "displayEntityName": false, + "displayEntityType": false, + "enableSelectColumnDisplay": false, + "enableStickyHeader": true, + "enableStickyAction": true, + "entitiesTitle": "Devices", + "displayEntityLabel": true, + "entityLabelColumnTitle": "Device" + }, + "title": "New Entities table", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "579f0468-9ce9-7e3e-b34c-88dd3de59897", + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current SW title", + "color": "#2196f3", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.09545533885166413, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current SW version", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.7206056602328659, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target SW title", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.9934225682766313, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target SW version", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5251724416842531, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_ts", + "type": "timeseries", + "label": "Target SW set time", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellContentFunction": "if (value !== '') {\n return ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');\n}\nreturn '';" + }, + "_hash": 0.31823244858578237, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Progress", + "color": "#9c27b0", + "settings": { + "columnWidth": "30%", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "return {\n 'padding-right': '30px'\n}", + "cellContentFunction": "if (value !== '') {\n var mapProgress = {\n 'QUEUED': 0,\n 'INITIATED': 5,\n 'DOWNLOADING': 10,\n 'DOWNLOADED': 55,\n 'VERIFIED': 60,\n 'UPDATING': 70,\n 'FAILED': 99,\n 'UPDATED': 100\n }\n var color = 'mat-primary';\n var progress = mapProgress[value];\n if (value == 'FAILED') {\n color = 'mat-accent';\n }\n return `
`;\n}" + }, + "_hash": 0.8174211757846257, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#f44336", + "settings": { + "columnWidth": "130px", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "if (value == 'FAILED') {\n return {'color' : '#D93025'};\n}\nreturn {};", + "cellContentFunction": "function icon(value) {\n if (value == 'QUEUED') {\n return '';\n }\n if (value == 'INITIATED' || value == 'DOWNLOADING' || value == 'DOWNLOADED') {\n return '';\n }\n if (value == 'VERIFIED' || value == 'UPDATING' ) {\n return 'update';\n }\n if (value == 'UPDATED') {\n return '';\n }\n if (value == 'FAILED') {\n return 'warning';\n }\n return '';\n}\nfunction capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\n\nreturn icon(value) + '' + capitalize(value) + '';" + }, + "_hash": 0.7764426948615217, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_checksum", + "type": "attribute", + "label": "sw_checksum", + "color": "#3f51b5", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.5594087842471693, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_url", + "type": "attribute", + "label": "sw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.3355829384124256, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "actions": { + "actionCellButton": [ + { + "name": "History software update", + "icon": "history", + "type": "openDashboardState", + "targetDashboardStateId": "device_software_history", + "setEntityId": true, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "98a1406c-3301-bc2f-2c5d-d637ce3b663b" + }, + { + "name": "Edit software", + "icon": "edit", + "type": "customPretty", + "customHtml": "
\n \n

Edit software {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", + "customCss": "form {\n min-width: 300px !important;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", + "customResources": [], + "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" + }, + { + "name": "Download software", + "icon": "file_download", + "type": "custom", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceSoftware();\n\nfunction getDeviceSoftware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.softwareId !== null) {\n otaPackageService.downloadOtaPackage(data.softwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.softwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.softwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "12533058-42f6-e75f-620c-219c48d01ec0" + }, + { + "name": "Copy checksum/URL", + "icon": "content_copy", + "type": "custom", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_checksum');\nvar checksum = data.data[0][1];\nconsole.log(checksum);\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Software checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Software direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "widgetStyle": {} + }, + "row": 0, + "col": 0, + "id": "e8280043-d3dc-7acb-c2ff-a4522972ff91" + }, + "3624013b-378c-f110-5eba-ae95c25a4dcc": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "entities_table", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "timewindow": { + "realtime": { + "interval": 1000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "enableSearch": true, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityLabel", + "displayEntityName": false, + "displayEntityType": false, + "enableSelectColumnDisplay": false, + "enableStickyHeader": true, + "enableStickyAction": true, + "entitiesTitle": "Devices", + "displayEntityLabel": true, + "entityLabelColumnTitle": "Device" + }, + "title": "New Entities table", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "bdbc6ea1-95a7-3912-341a-58dc7704a00f", + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current SW title", + "color": "#2196f3", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.09545533885166413, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current SW version", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.7206056602328659, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target SW title", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.9934225682766313, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target SW version", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5251724416842531, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_ts", + "type": "timeseries", + "label": "Target SW set time", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellContentFunction": "if (value !== '') {\n return ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');\n}\nreturn '';" + }, + "_hash": 0.31823244858578237, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Progress", + "color": "#9c27b0", + "settings": { + "columnWidth": "30%", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "return {\n 'padding-right': '30px'\n}", + "cellContentFunction": "if (value !== '') {\n var mapProgress = {\n 'QUEUED': 0,\n 'INITIATED': 5,\n 'DOWNLOADING': 10,\n 'DOWNLOADED': 55,\n 'VERIFIED': 60,\n 'UPDATING': 70,\n 'FAILED': 99,\n 'UPDATED': 100\n }\n var color = 'mat-primary';\n var progress = mapProgress[value];\n if (value == 'FAILED') {\n color = 'mat-accent';\n }\n return `
`;\n}" + }, + "_hash": 0.8174211757846257, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#f44336", + "settings": { + "columnWidth": "130px", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "if (value == 'FAILED') {\n return {'color' : '#D93025'};\n}\nreturn {};", + "cellContentFunction": "function icon(value) {\n if (value == 'QUEUED') {\n return '';\n }\n if (value == 'INITIATED' || value == 'DOWNLOADING' || value == 'DOWNLOADED') {\n return '';\n }\n if (value == 'VERIFIED' || value == 'UPDATING' ) {\n return 'update';\n }\n if (value == 'UPDATED') {\n return '';\n }\n if (value == 'FAILED') {\n return 'warning';\n }\n return '';\n}\nfunction capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\n\nreturn icon(value) + '' + capitalize(value) + '';" + }, + "_hash": 0.7764426948615217, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_checksum", + "type": "attribute", + "label": "sw_checksum", + "color": "#3f51b5", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.5594087842471693, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_url", + "type": "attribute", + "label": "sw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.3355829384124256, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "actions": { + "actionCellButton": [ + { + "name": "History software update", + "icon": "history", + "type": "openDashboardState", + "targetDashboardStateId": "device_software_history", + "setEntityId": true, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "98a1406c-3301-bc2f-2c5d-d637ce3b663b" + }, + { + "name": "Edit software", + "icon": "edit", + "type": "customPretty", + "customHtml": "
\n \n

Edit software {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", + "customCss": "form {\n min-width: 300px !important;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", + "customResources": [], + "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" + }, + { + "name": "Download software", + "icon": "file_download", + "type": "custom", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceSoftware();\n\nfunction getDeviceSoftware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.softwareId !== null) {\n otaPackageService.downloadOtaPackage(data.softwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.softwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.softwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "12533058-42f6-e75f-620c-219c48d01ec0" + }, + { + "name": "Copy checksum/URL", + "icon": "content_copy", + "type": "custom", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_checksum');\nvar checksum = data.data[0][1];\nconsole.log(checksum);\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Software checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Software direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "widgetStyle": {} + }, + "row": 0, + "col": 0, + "id": "3624013b-378c-f110-5eba-ae95c25a4dcc" + }, + "d2d13e0d-4e71-889f-9343-ad2f0af9f176": { + "isSystemType": true, + "bundleAlias": "cards", + "typeAlias": "entities_table", + "type": "latest", + "title": "New widget", + "image": null, + "description": null, + "sizeX": 7.5, + "sizeY": 6.5, + "config": { + "timewindow": { + "realtime": { + "interval": 1000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 200 + } + }, + "showTitle": true, + "backgroundColor": "rgb(255, 255, 255)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "4px", + "settings": { + "enableSearch": true, + "displayPagination": true, + "defaultPageSize": 10, + "defaultSortOrder": "entityLabel", + "displayEntityName": false, + "displayEntityType": false, + "enableSelectColumnDisplay": false, + "enableStickyHeader": true, + "enableStickyAction": true, + "entitiesTitle": "Devices", + "displayEntityLabel": true, + "entityLabelColumnTitle": "Device" + }, + "title": "New Entities table", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400, + "padding": "5px 10px 5px 10px" + }, + "useDashboardTimewindow": false, + "showLegend": false, + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "filterId": "6044e198-df64-cd76-f339-696f220c4943", + "dataKeys": [ + { + "name": "current_sw_title", + "type": "timeseries", + "label": "Current SW title", + "color": "#2196f3", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.09545533885166413, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "current_sw_version", + "type": "timeseries", + "label": "Current SW version", + "color": "#4caf50", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.7206056602328659, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_title", + "type": "timeseries", + "label": "Target SW title", + "color": "#ffc107", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.9934225682766313, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_version", + "type": "timeseries", + "label": "Target SW version", + "color": "#607d8b", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled" + }, + "_hash": 0.5251724416842531, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "target_sw_ts", + "type": "timeseries", + "label": "Target SW set time", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellContentFunction": "if (value !== '') {\n return ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');\n}\nreturn '';" + }, + "_hash": 0.31823244858578237, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Progress", + "color": "#9c27b0", + "settings": { + "columnWidth": "30%", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "return {\n 'padding-right': '30px'\n}", + "cellContentFunction": "if (value !== '') {\n var mapProgress = {\n 'QUEUED': 0,\n 'INITIATED': 5,\n 'DOWNLOADING': 10,\n 'DOWNLOADED': 55,\n 'VERIFIED': 60,\n 'UPDATING': 70,\n 'FAILED': 99,\n 'UPDATED': 100\n }\n var color = 'mat-primary';\n var progress = mapProgress[value];\n if (value == 'FAILED') {\n color = 'mat-accent';\n }\n return `
`;\n}" + }, + "_hash": 0.8174211757846257, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_state", + "type": "timeseries", + "label": "Status", + "color": "#f44336", + "settings": { + "columnWidth": "130px", + "useCellStyleFunction": true, + "useCellContentFunction": true, + "defaultColumnVisibility": "visible", + "columnSelectionToDisplay": "enabled", + "cellStyleFunction": "if (value == 'FAILED') {\n return {'color' : '#D93025'};\n}\nreturn {};", + "cellContentFunction": "function icon(value) {\n if (value == 'QUEUED') {\n return '';\n }\n if (value == 'INITIATED' || value == 'DOWNLOADING' || value == 'DOWNLOADED') {\n return '';\n }\n if (value == 'VERIFIED' || value == 'UPDATING' ) {\n return 'update';\n }\n if (value == 'UPDATED') {\n return '';\n }\n if (value == 'FAILED') {\n return 'warning';\n }\n return '';\n}\nfunction capitalize (s) {\n if (typeof s !== 'string') return '';\n return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();\n}\n\nreturn icon(value) + '' + capitalize(value) + '';" + }, + "_hash": 0.7764426948615217, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_checksum", + "type": "attribute", + "label": "sw_checksum", + "color": "#3f51b5", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.5594087842471693, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "sw_url", + "type": "attribute", + "label": "sw_url", + "color": "#e91e63", + "settings": { + "columnWidth": "0px", + "useCellStyleFunction": false, + "cellStyleFunction": "", + "useCellContentFunction": false, + "cellContentFunction": "", + "defaultColumnVisibility": "hidden", + "columnSelectionToDisplay": "disabled" + }, + "_hash": 0.3355829384124256, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "actions": { + "actionCellButton": [ + { + "name": "History software update", + "icon": "history", + "type": "openDashboardState", + "targetDashboardStateId": "device_software_history", + "setEntityId": true, + "stateEntityParamName": null, + "openInSeparateDialog": false, + "dialogTitle": "", + "dialogHideDashboardToolbar": true, + "dialogWidth": null, + "dialogHeight": null, + "openRightLayout": false, + "id": "98a1406c-3301-bc2f-2c5d-d637ce3b663b" + }, + { + "name": "Edit software", + "icon": "edit", + "type": "customPretty", + "customHtml": "
\n \n

Edit software {{entityName}}

\n \n \n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
", + "customCss": "form {\n min-width: 300px !important;\n}", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n\n vm.entityName = entityName;\n vm.entity = {};\n\n vm.editEntityFormGroup = vm.fb.group({\n softwareId: [null]\n });\n\n getEntityInfo();\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n\n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveEntity().subscribe(\n function () {\n // widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n };\n\n\n function getEntityInfo() {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n vm.entity = data;\n vm.editEntityFormGroup.patchValue({\n softwareId: vm.entity.softwareId\n }, {emitEvent: false});\n }\n );\n }\n\n function saveEntity() {\n const formValues = vm.editEntityFormGroup.value;\n vm.entity.softwareId = formValues.softwareId;\n return deviceService.saveDevice(vm.entity);\n }\n}", + "customResources": [], + "id": "23099c1d-454b-25dc-8bc0-7cf33c21c5d5" + }, + { + "name": "Download software", + "icon": "file_download", + "type": "custom", + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet entityService = $injector.get(widgetContext.servicesMap.get('entityService'));\nlet otaPackageService = $injector.get(widgetContext.servicesMap.get('otaPackageService'));\nlet deviceProfileService = $injector.get(widgetContext.servicesMap.get('deviceProfileService'));\n\ngetDeviceSoftware();\n\nfunction getDeviceSoftware() {\n var entityIdValue = entityId.id;\n var data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url === '') {\n entityService.getEntity(entityId.entityType, entityId.id).subscribe(\n function (data) {\n if (data.softwareId !== null) {\n otaPackageService.downloadOtaPackage(data.softwareId.id).subscribe(); \n } else {\n deviceProfileService.getDeviceProfile(data.deviceProfileId.id).subscribe(\n function (deviceProfile) {\n if (deviceProfile.softwareId !== null) {\n otaPackageService.downloadOtaPackage(deviceProfile.softwareId.id).subscribe();\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n });\n }\n }\n );\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "12533058-42f6-e75f-620c-219c48d01ec0" + }, + { + "name": "Copy checksum/URL", + "icon": "content_copy", + "type": "custom", + "customFunction": "function copyToClipboard(text) {\n if (window.clipboardData && window.clipboardData.setData) {\n return window.clipboardData.setData(\"Text\", text);\n\n }\n else if (document.queryCommandSupported && document.queryCommandSupported(\"copy\")) {\n var textarea = document.createElement(\"textarea\");\n textarea.textContent = text;\n textarea.style.position = \"fixed\";\n document.body.appendChild(textarea);\n textarea.select();\n try {\n return document.execCommand(\"copy\");\n }\n catch (ex) {\n console.warn(\"Copy to clipboard failed.\", ex);\n return false;\n }\n document.body.removeChild(textarea);\n }\n}\nvar entityIdValue = entityId.id;\nvar data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_checksum');\nvar checksum = data.data[0][1];\nconsole.log(checksum);\nif (checksum !== '') {\n copyToClipboard(checksum);\n widgetContext.showSuccessToast('Software checksum has been copied to clipboard', 2000, 'top');\n} else {\n data = widgetContext.data.find((el) => el.datasource.entityId === entityIdValue && el.dataKey.name === 'sw_url');\n var url = data.data[0][1];\n if (url !== '') {\n copyToClipboard(url);\n widgetContext.showSuccessToast('Software direct URL has been copied to clipboard', 2000, 'top');\n } else {\n widgetContext.showToast('warn', 'Device ' + entityName +' has not software set.', 2000, 'top');\n }\n}", + "id": "09323079-7111-87f7-90d1-c62cd7d85dc7" + } + ] + }, + "showTitleIcon": false, + "iconColor": "rgba(0, 0, 0, 0.87)", + "iconSize": "24px", + "titleTooltip": "", + "widgetStyle": {} + }, + "row": 0, + "col": 0, + "id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176" + } + }, + "states": { + "default": { + "name": "Device list", + "root": true, + "layouts": { + "main": { + "widgets": { + "cd03188e-cd9d-9601-fd57-da4cb95fc016": { + "sizeX": 19, + "sizeY": 12, + "row": 0, + "col": 0 + }, + "17543c57-af4a-2c1e-bf12-53a7b46791e6": { + "sizeX": 5, + "sizeY": 3, + "row": 0, + "col": 19 + }, + "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": { + "sizeX": 5, + "sizeY": 3, + "row": 3, + "col": 19 + }, + "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": { + "sizeX": 5, + "sizeY": 3, + "row": 9, + "col": 19 + }, + "77b10144-b904-edd5-8c7c-8fb75616c6d8": { + "sizeX": 5, + "sizeY": 3, + "row": 6, + "col": 19 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 12, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70 + } + } + } + }, + "device_software_history": { + "name": "Software history: ${entityName}", + "root": false, + "layouts": { + "main": { + "widgets": { + "100b756c-0082-6505-3ae1-3603e6deea48": { + "sizeX": 24, + "sizeY": 12, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "device_waiting": { + "name": "Device waiting", + "root": false, + "layouts": { + "main": { + "widgets": { + "21be08bb-ec90-f760-ad6f-e7678f12c401": { + "sizeX": 24, + "sizeY": 12, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "device_updating": { + "name": "Device updating", + "root": false, + "layouts": { + "main": { + "widgets": { + "e8280043-d3dc-7acb-c2ff-a4522972ff91": { + "sizeX": 24, + "sizeY": 12, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "device_updated": { + "name": "Device updated", + "root": false, + "layouts": { + "main": { + "widgets": { + "d2d13e0d-4e71-889f-9343-ad2f0af9f176": { + "sizeX": 27, + "sizeY": 12, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + }, + "device_error": { + "name": "Device failed", + "root": false, + "layouts": { + "main": { + "widgets": { + "3624013b-378c-f110-5eba-ae95c25a4dcc": { + "sizeX": 24, + "sizeY": 12, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70 + } + } + } + } + }, + "entityAliases": { + "639da5b4-31f0-0151-6282-c37a3897b7e8": { + "id": "639da5b4-31f0-0151-6282-c37a3897b7e8", + "alias": "All devices", + "filter": { + "type": "entityType", + "resolveMultiple": true, + "entityType": "DEVICE" + } + }, + "19f41c21-d9af-e666-8f50-e1748778f955": { + "id": "19f41c21-d9af-e666-8f50-e1748778f955", + "alias": "State entity", + "filter": { + "type": "stateEntity", + "resolveMultiple": false, + "stateEntityParamName": null, + "defaultStateEntity": null + } + } + }, + "filters": { + "19a0ad1c-b31d-4a29-9d7b-5d87e2a8ea6e": { + "id": "19a0ad1c-b31d-4a29-9d7b-5d87e2a8ea6e", + "filter": "WaitingDevicesFilter", + "keyFilters": [ + { + "key": { + "type": "TIME_SERIES", + "key": "sw_state" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "QUEUED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": true, + "label": "", + "autogeneratedLabel": true, + "order": 0 + } + } + ] + } + ], + "editable": false + }, + "579f0468-9ce9-7e3e-b34c-88dd3de59897": { + "id": "579f0468-9ce9-7e3e-b34c-88dd3de59897", + "filter": "UpdatingDevicesFilter", + "keyFilters": [ + { + "key": { + "type": "TIME_SERIES", + "key": "sw_state" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "OR", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "INITIATED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": false, + "label": "sw_state equel", + "autogeneratedLabel": true, + "order": 0 + } + }, + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "DOWNLOADING", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": false, + "label": "sw_state equal", + "autogeneratedLabel": true, + "order": 0 + } + }, + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "DOWNLOADED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": false, + "label": "sw_state equal", + "autogeneratedLabel": true, + "order": 0 + } + }, + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "VERIFIED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": false, + "label": "sw_state equal", + "autogeneratedLabel": true, + "order": 0 + } + }, + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "UPDATING", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": false, + "label": "sw_state equal", + "autogeneratedLabel": true, + "order": 0 + } + } + ], + "type": "COMPLEX" + }, + "userInfo": { + "editable": true, + "label": "", + "autogeneratedLabel": true, + "order": 0 + } + } + ] + } + ], + "editable": false + }, + "6044e198-df64-cd76-f339-696f220c4943": { + "id": "6044e198-df64-cd76-f339-696f220c4943", + "filter": "UpdetedDevicesFilter", + "keyFilters": [ + { + "key": { + "type": "TIME_SERIES", + "key": "sw_state" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "UPDATED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": true, + "label": "", + "autogeneratedLabel": true, + "order": 0 + } + } + ] + } + ], + "editable": false + }, + "bdbc6ea1-95a7-3912-341a-58dc7704a00f": { + "id": "bdbc6ea1-95a7-3912-341a-58dc7704a00f", + "filter": "FailedDevicesFilter", + "keyFilters": [ + { + "key": { + "type": "TIME_SERIES", + "key": "sw_state" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "EQUAL", + "value": { + "defaultValue": "FAILED", + "dynamicValue": null + }, + "ignoreCase": false, + "type": "STRING" + }, + "userInfo": { + "editable": true, + "label": "", + "autogeneratedLabel": true, + "order": 0 + } + } + ] + } + ], + "editable": false + }, + "8fdb88d0-50ac-2232-fdb7-69c30c16544e": { + "id": "8fdb88d0-50ac-2232-fdb7-69c30c16544e", + "filter": "DeviceSearch", + "keyFilters": [ + { + "key": { + "type": "ENTITY_FIELD", + "key": "name" + }, + "valueType": "STRING", + "predicates": [ + { + "keyFilterPredicate": { + "operation": "CONTAINS", + "value": { + "defaultValue": "" + }, + "ignoreCase": true, + "type": "STRING" + }, + "userInfo": { + "editable": true, + "label": "Device name", + "autogeneratedLabel": false, + "order": 0 + } + } + ] + } + ], + "editable": true + } + }, + "timewindow": { + "displayValue": "", + "hideInterval": false, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY" + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1618998609030, + "endTimeMs": 1619085009030 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": false, + "showEntitiesSelect": false, + "showDashboardTimewindow": true, + "showDashboardExport": false, + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showFilters": true, + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "showUpdateDashboardImage": false + } + }, + "name": "Software" +} \ No newline at end of file diff --git a/application/src/main/data/json/system/oauth2_config_templates/apple_config.json b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json new file mode 100644 index 0000000000..a956920b6b --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json @@ -0,0 +1,24 @@ +{ + "providerId": "Apple", + "additionalInfo": null, + "accessTokenUri": "https://appleid.apple.com/auth/token", + "authorizationUri": "https://appleid.apple.com/auth/authorize?response_mode=form_post", + "scope": ["email","openid","name"], + "jwkSetUri": "https://appleid.apple.com/auth/keys", + "userInfoUri": null, + "clientAuthenticationMethod": "POST", + "userNameAttributeName": "email", + "mapperConfig": { + "type": "APPLE", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "firstName", + "lastNameAttributeKey": "lastName", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "apple-logo", + "loginButtonLabel": "Apple", + "helpLink": "https://developer.apple.com/sign-in-with-apple/get-started/" +} diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 29b8b9f8d3..79f732bca8 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -28,7 +28,7 @@ "alias": "html_card", "name": "HTML Card", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAATFSURBVHja7d3tUxNXFMdx/u7vEkAJsKQK6bilKaBV1Eh5GGNaSyq0WodhRuqMtQJqW5UHCwKGMRJiQpL99cWGEGaaTjtjgaTnvNpz7mYnn8neu/duXtwWFbdWlxs8Vrf21VJcy5TU4FHKrBVbtjJqgshstayWmgFSWm1ZVlPEskEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAG+T9ACrlcLjjyc7lcWflcbfhBfnByOZfL5cpSKZfL+f/2G+WAJ/8dJAHh4GgFeK4BaiOjMYDtyskzAIvSQ+BtI0JmKif3NRBk3PM8Lwwhz/M8LxtAzgf30RoNBJEkjUOk0j4GwJok6XaDQ8LwtSSVXVrP1of4v1w978bu5YNh4fHlfnfwfpDo15Hevm/eHkCyP8Tc/rGV44d8FaLbl/QCRrrqQopXg251bkdSdjBIIpuSNAtAbwXyqgsA595Hh3Sm0+l0Or1QDzJxLWiYhCftdSFJoKMHiPnyvwTOhIHeD9KGA3SfIYBkOsGJtIPz/GNDauIvIfEFGJdKnZwtOPUgOw4kfT0E1vQMmPb12IEZaRJCz1RKBpBJCG+ocAVixw25XArTUdACTBbqdvY56MhLisK8xisDXRyiUi+MStoDnqjUCT9Keg3Oh48MCcXj8Xg8PlwPMqQE/Kzr8HK3LiQBFyozhaI8uClJ8+AUi8CD6qi1DYymUqkksH7Mnf0zLcNIvo2In64LGYXhauLCHUlaAnb3gIUq5FXN7//qmCH98s8RmoXv9KYuZBI+ryZ9cFsKzsvngUdVyAowcDGIjWOGRKRpaINNrdWF3IUeX9LS/PymLsMlSZqCLikM01XIe+CnE3ogRqRtAE9arQtZCVqynbCoWXBWpfddMCaNgLsnLQSj1gVwM5Je3/RPAKIBYPYIJOp5nud5dw8uFIPQxO1eCOf1oRs6Jm71QOtG0FMiydFQAHkKnJlIjTgkTwIyB87uEUglbh5c6G13UGhdkvSyPUicB5J0HYBQe/Bkv+tUPnr/JCDZVq7obyF6d6MNnC+COdTmSCs4A78Hs5dkG3zyYqAy13oec8CJ/XZ6l7qFzfW9apJ/80dNsr5V2yNyGxt7tmY3iEEMYhCDGOTEIJnD6YtBDGKQpocsDrvR5K4kyX96PeoOzu5Lysbj8b2Zvv7SkepphgwBcD4vqRAPVlXR91IauASUVAhWgny6e7ohtEdCwcpdCaDTBYYDCED5sHrxdEOuFZRx4Ya06cC3ZS05sKI04EzM3demA1NlLTqweuo7ewI86U7lHdYQzCgNzEnSHXB9SYPw/amHpKBPGoFzqVQqFYUxpYEX0pHqeINAav4wvXoI+eyweq1BIDHorry+nT6E1FRnGgQyCtFqUxVyo7baGJBHlVG4mFivgRxU9xMbjQIpR4HBqYRLz84hpBwFhqYSLu5Og0C07VZ6dXTvEHKk2iAQZZOdQHcqX3NrSdlb1WrjLHXL26/T/j+s2prdIAYxiEEMYhCDGMQgBjGIQQxiEIMYxCAGMYhBDGIQgxjEIAYxiEEMYhCDGMQgBjGIQQxiEIMY5PRAmmaD4ObYsvndVst+c2yiXWppjm3NS/oTe0OjFEeU1MMAAAAASUVORK5CYII=", - "description": "Useful to inject custom HTML code. Designed to displays static information only.", + "description": "Useful to inject custom HTML code. Designed to display static information only.", "descriptor": { "type": "static", "sizeX": 7.5, diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 8fea056506..1de1bae6f1 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -10,16 +10,16 @@ "alias": "rpc_debug_terminal", "name": "RPC debug terminal", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAWcklEQVR42u2dB5QWRZeGi6QgklFUDJhFMCJGRFHMomMAMStiBvUXRTFgRjGLERMoiIBZ1GVXdNcF4yIe5agYdvVnfkAwIAoiIs4+/72na3u+NN8wAzsD73vmzOnur7rCrbfuvV3VfSuUlZXNnDnzmGOOadKkSRCEKgAKlZSUzJgxA1IFWNWyZUsJRagutGjRAlIFdJVkIVQvevbsGWQBhWpH06ZNJQRBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEITlRK9evc5KwAdAnTt3rlevXvz1oIMOir+efvrp3bp1a9WqVUYOderUOfDAAy+77LJLL7308MMPX2ONNYop99577+ULyaOPPrpwsvfff59kG264Ya0W8uDBg6+55pqq5PDRRx8hh3XXXbfWtPnzzz8vK4/PPvtsjz328F9ffvnljF9///33q6++Ot6+/vrrv/fee+kEX3zxxeabb15biHXqqafecsstK5S4a6655pIlS/7444+11lprtSPWEUcc0alTp/333//hhx/mdNasWa54nFj9+vXj19122+2MM8747rvvuHLiiSe6rnrrrbc4feyxxzp27Aifbr/9dk6nTp1aW4j10ksvkf8uu+yyQoVM/piC1VFjbbbZZtGu+RUXhBMLgxjTQymuvPrqqxzvs88+HH/44Ydps/jJJ59wccstt8wua+ONN7722mvvuOOOQw89NJtYlAIv7777buwp+aSJtd122w0YMIBbUDDRUjMYsL8bbbSRn+63336ctm/f3k/XXnvtiy66iFsw4htssAE/MWzSlWncuDEXUc/kT7L4cXn9+vVxD+65557bbrttr732iunJ7dxzz91mm22o4QEHHECIA27v0aMHgkKFDxs2jIsk23PPPWng0KFD995773jv3wx+3Ldv34svvrh58+a06L777jv77LMbNGiQtgBcf+CBB8hz6623XnWIBd59990CxOrSpQtX3n77bY6HDBnCMfJNZ4gfRl9uuummGQWhz3788UfS//nnn/z/4Ycf0sR6/PHHOcVkLF26lAMkmybW3LlzMSVuaseOHes/jRkzhlPI7aeuLI8//niOGzVq5PxetmzZX3/9VVpayvFdd92Vrs86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", - "description": "Allows to send any RPC command using it's name and parameters to device. Useful for debug.", + "description": "Allows to send any RPC command using its name and parameters to device. Useful for debug.", "descriptor": { "type": "rpc", "sizeX": 9.5, "sizeY": 5.5, "resources": [], "templateHtml": "
", - "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n", - "controllerScript": "var requestTimeout = 500;\nvar multiParams = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var utils = self.ctx.$scope.$injector.get(self.ctx.servicesMap.get('utils'));\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.multiParams) {\n multiParams = self.ctx.settings.multiParams;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = utils.guid();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var cmdObj = $.terminal.parse_command(localCommand);\n if (cmdObj.args) {\n if (!multiParams && cmdObj.args.length > 1) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n }\n else {\n if (cmdObj.args.length) {\n var params = getMultiParams(cmdObj.args);\n }\n performRpc(this, cmdObj.name, params, requestUUID);\n }\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n \n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1 (multiParams===false):]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2 (multiParams===false):]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\":2,\\\\\"key2\\\\\":\\\\\"myVal\\\\\"}\"\\n\\n'; \n commandsListText += '[[b;#fff;]Example 3 (multiParams===true)]\\n'; \n commandsListText += ' [params body] = \"all the string after the method, including spaces\"]\\n';\n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": \"battery level\", \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\nfunction getMultiParams(cmdObj) {\n var params = \"\";\n cmdObj.forEach((element) => {\n try {\n params += \" \" + JSON.strigify(JSON.parse(element));\n } catch (e) {\n params += \" \" + element;\n }\n })\n return params.trim();\n}\n\n \nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"multiParams\": {\n \"title\": \"RPC params All line\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"multiParams\"\n ]\n}", + "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", + "controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } @@ -28,7 +28,7 @@ "alias": "rpc_remote_shell", "name": "RPC remote shell", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAU6klEQVR42u2dCbSW0xrHtyZDOpVkyFCKIplCZpLMIVJIhsgYmSUakLniInOGyFRXaCn3ilws89Q1RrpRSq4k15Tx3N96Ht9e73m/4Xzn9EVH//8666z97u99997v3v/9PM8e3meH8vLyWbNmdevWrUGDBkEQFgNQqGvXrtOmTYNUAVatssoqqhShVGjcuDGkCsgq1YVQWnTv3j1IAwolR1lZmSpBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAShmujRo8fxGfAB0NZbb127du3465577hl/7d2796677tqkSZNUCsstt9wee+zRv3//c845p0uXLvXq1ftLVtQ222zDJ50PP/xwlZ7abbfdeOruu+8mXKtWLcKzZ89eJoj1/vvvl1fEe++9t9122/mvEyZMSP26aNGiQYMGxcfXXHPNl156KXnDBx980KpVq6Xh1aD7FVdcsdVWW4lYfxqx9t9//y233JJauPXWW7mcM2eOCx4n1imnnMKv1Oyxxx47b948Yg4//HCXVc888wyXt99+e7t27eDT8OHDuXzttdeWhlcbOnQohTnmmGNErD+NWC1btox6zWPQiZFYKMR4P5QiZuLEiYR32WUXwq+//npSLb711ltEbrDBBqmMDjnkENTluuuue+mllx533HEe2axZs/POO++mm24666yzVl11VY9s27Ytd3bo0GHvvfe+/PLLhw0btummmxJ/0EEHXXfddRdddFGbNm2SKcPpCy+8kET69evXqFEjj6QzPPXUU5Rk3LhxJ554Yry5c+fOJHjttdfisIDS5qyTpk2bnn322eR1/vnnR+kbiUV2Q4YMufLKK7fffvvkU+uss86AAQMoxhlnnBEdIIhYLWPMiy++WIBYO+64IzHPP/884csuu4wwJEgmiB2GQbbeeuulMpo8eTI3z5w5k/8kS8xOO+20cOFCLr/77jv+z507t3nz5sT37NnTpSb/f/75Z/5///33Dz74IIGffvrJ74/pQ5pffvmFyB9++MGbbf311yceLxcew81Tp071m2l1T8TTueOOO7IrZOONN/7qq6/IlxTQ+9y57777RmKRPsl6qX799VdeNhLom2++ie+CH4S11lpLxPqd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", - "description": "Allows to send emulate remote shell. Requires custom implementation on the target device to work properly.", + "description": "Allows to emulate remote shell. Requires custom implementation on the target device to work properly.", "descriptor": { "type": "rpc", "sizeX": 9.5, @@ -151,4 +151,4 @@ } } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_bundles/digital_gauges.json b/application/src/main/data/json/system/widget_bundles/digital_gauges.json index 755ac603b0..9455ac4100 100644 --- a/application/src/main/data/json/system/widget_bundles/digital_gauges.json +++ b/application/src/main/data/json/system/widget_bundles/digital_gauges.json @@ -100,7 +100,7 @@ "alias": "lcd_bar_gauge", "name": "LCD bar gauge", "image": "data:image/png;base64,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", - "description": "Preconfigured gauge to display any value reading as an bar. Allows to configure value range, gradient colors and other settings.", + "description": "Preconfigured gauge to display any value reading as a bar. Allows to configure value range, gradient colors and other settings.", "descriptor": { "type": "latest", "sizeX": 2, diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index 410e70714b..74260d3021 100644 --- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json @@ -10,7 +10,7 @@ "alias": "device_admin_table", "name": "Device admin table", "image": "data:image/png;base64,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", - "description": "Customized entity table widget with preconfigured actions to create update and delete devices.", + "description": "Customized entity table widget with preconfigured actions to create, update and delete devices.", "descriptor": { "type": "latest", "sizeX": 7.5, @@ -28,7 +28,7 @@ "alias": "asset_admin_table", "name": "Asset admin table", "image": "data:image/png;base64,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", - "description": "Customized entity table widget with preconfigured actions to create update and delete assets.", + "description": "Customized entity table widget with preconfigured actions to create, update and delete assets.", "descriptor": { "type": "latest", "sizeX": 7.5, diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json index 313dd52323..bec97fe3cf 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -10,7 +10,7 @@ "alias": "gateway_configuration", "name": "Gateway Configuration", "image": "data:image/png;base64,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", - "description": "Allows to define widget gateway configuration for a single selected device.", + "description": "Allows to define gateway configuration for a single selected device.", "descriptor": { "type": "static", "sizeX": 8, diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 8a80aa80f1..689452dbf7 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -28,7 +28,7 @@ "alias": "update_multiple_attributes", "name": "Update Multiple Attributes", "image": "data:image/png;base64,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", - "description": "Allows to create an input form and set values for multiple attributes simultaniously.", + "description": "Allows to create an input form and set values for multiple attributes simultaneously.", "descriptor": { "type": "latest", "sizeX": 7.5, diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index aaf958a646..41d5ff55b2 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -46,7 +46,7 @@ "alias": "route_map", "name": "Route Map", "image": "data:image/png;base64,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", - "description": "TTrip animation on the Google maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", + "description": "Trip animation on the Google maps. Allows to visualize location change over time. Use Trip Animation widget for advanced features.", "descriptor": { "type": "timeseries", "sizeX": 8.5, diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index 5647c301cf..8020046a7b 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -79,6 +79,68 @@ CREATE TABLE IF NOT EXISTS ota_package ( CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) ); +CREATE TABLE IF NOT EXISTS oauth2_params ( + id uuid NOT NULL CONSTRAINT oauth2_params_pkey PRIMARY KEY, + enabled boolean, + tenant_id uuid, + created_time bigint NOT NULL +); + +CREATE TABLE IF NOT EXISTS oauth2_registration ( + id uuid NOT NULL CONSTRAINT oauth2_registration_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(2048), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + platforms varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean, + CONSTRAINT fk_registration_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS oauth2_domain ( + id uuid NOT NULL CONSTRAINT oauth2_domain_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + CONSTRAINT fk_domain_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_domain_unq_key UNIQUE (oauth2_params_id, domain_name, domain_scheme) +); + +CREATE TABLE IF NOT EXISTS oauth2_mobile ( + id uuid NOT NULL CONSTRAINT oauth2_mobile_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, + created_time bigint NOT NULL, + pkg_name varchar(255), + app_secret varchar(2048), + CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) +); + ALTER TABLE dashboard ADD COLUMN IF NOT EXISTS image varchar(1000000); diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 5af11d87f5..747a16a5e6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -192,6 +192,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { syncSessionSet.add(key); } }); + log.trace("46) Rpc syncSessionSet [{}] subscription after sent [{}]",syncSessionSet, rpcSubscriptions); syncSessionSet.forEach(rpcSubscriptions::remove); } @@ -454,7 +455,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } else { SessionInfoMetaData sessionMD = sessions.get(sessionId); if (sessionMD == null) { - sessionMD = new SessionInfoMetaData(new SessionInfo(SessionType.SYNC, sessionInfo.getNodeId())); + sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId())); } sessionMD.setSubscribedToAttributes(true); log.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId); @@ -475,7 +476,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } else { SessionInfoMetaData sessionMD = sessions.get(sessionId); if (sessionMD == null) { - sessionMD = new SessionInfoMetaData(new SessionInfo(SessionType.SYNC, sessionInfo.getNodeId())); + sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId())); } sessionMD.setSubscribedToRPC(true); log.debug("[{}] Registering rpc subscription for session [{}]", deviceId, sessionId); diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index bbcb7d656a..26c230455b 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -37,6 +37,9 @@ import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; +import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; +import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; import org.thingsboard.server.utils.MiscUtils; import javax.servlet.http.HttpServletRequest; @@ -46,12 +49,13 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.UUID; @Service @Slf4j public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { - public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; - public static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/"; + private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization"; + private static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/"; private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; private static final char PATH_DELIMITER = '/'; @@ -63,6 +67,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza @Autowired private ClientRegistrationRepository clientRegistrationRepository; + @Autowired + private OAuth2Service oAuth2Service; + + @Autowired + private OAuth2AppTokenFactory oAuth2AppTokenFactory; + @Autowired(required = false) private OAuth2Configuration oauth2Configuration; @@ -71,7 +81,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { String registrationId = this.resolveRegistrationId(request); String redirectUriAction = getAction(request, "login"); - return resolve(request, registrationId, redirectUriAction); + String appPackage = getAppPackage(request); + String appToken = getAppToken(request); + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); } @Override @@ -80,7 +92,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return null; } String redirectUriAction = getAction(request, "authorize"); - return resolve(request, registrationId, redirectUriAction); + String appPackage = getAppPackage(request); + String appToken = getAppToken(request); + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); } private String getAction(HttpServletRequest request, String defaultAction) { @@ -91,8 +105,16 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return action; } + private String getAppPackage(HttpServletRequest request) { + return request.getParameter("pkg"); + } + + private String getAppToken(HttpServletRequest request) { + return request.getParameter("appToken"); + } + @SuppressWarnings("deprecation") - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) { if (registrationId == null) { return null; } @@ -104,6 +126,18 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza Map attributes = new HashMap<>(); attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + if (!StringUtils.isEmpty(appPackage)) { + if (StringUtils.isEmpty(appToken)) { + throw new IllegalArgumentException("Invalid application token."); + } else { + String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage); + if (StringUtils.isEmpty(appSecret)) { + throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package."); + } + String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret); + attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme); + } + } OAuth2AuthorizationRequest.Builder builder; if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 9e6d393b30..d94d26fc87 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -17,7 +17,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.SecurityMode; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -45,14 +44,11 @@ import java.util.Map; public class Lwm2mController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{securityMode}/{bootstrapServerIs}", method = RequestMethod.GET) + @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{isBootstrapServer}", method = RequestMethod.GET) @ResponseBody - public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("securityMode") String strSecurityMode, - @PathVariable("bootstrapServerIs") boolean bootstrapServer) throws ThingsboardException { - checkNotNull(strSecurityMode); + public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("isBootstrapServer") boolean bootstrapServer) throws ThingsboardException { try { - SecurityMode securityMode = SecurityMode.valueOf(strSecurityMode); - return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(securityMode, bootstrapServer); + return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(bootstrapServer); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 6591a1577a..7497e20122 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -22,12 +22,15 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.PlatformType; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; @@ -49,7 +52,9 @@ public class OAuth2Controller extends BaseController { @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) @ResponseBody - public List getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { + public List getOAuth2Clients(HttpServletRequest request, + @RequestParam(required = false) String pkgName, + @RequestParam(required = false) String platform) throws ThingsboardException { try { if (log.isDebugEnabled()) { log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort()); @@ -59,7 +64,13 @@ public class OAuth2Controller extends BaseController { log.debug("Header: {} {}", header, request.getHeader(header)); } } - return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request)); + PlatformType platformType = null; + if (StringUtils.isNotEmpty(platform)) { + try { + platformType = PlatformType.valueOf(platform); + } catch (Exception e) {} + } + return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request), pkgName, platformType); } catch (Exception e) { throw handleException(e); } @@ -68,10 +79,10 @@ public class OAuth2Controller extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") @ResponseBody - public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException { + public OAuth2Info getCurrentOAuth2Info() throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); - return oAuth2Service.findOAuth2Params(); + return oAuth2Service.findOAuth2Info(); } catch (Exception e) { throw handleException(e); } @@ -80,11 +91,11 @@ public class OAuth2Controller extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException { + public OAuth2Info saveOAuth2Info(@RequestBody OAuth2Info oauth2Info) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); - oAuth2Service.saveOAuth2Params(oauth2Params); - return oAuth2Service.findOAuth2Params(); + oAuth2Service.saveOAuth2Info(oauth2Info); + return oAuth2Service.findOAuth2Info(); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 13d39b0b2a..e28c5e37e1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -128,7 +128,7 @@ public class OtaPackageController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST) @ResponseBody - public OtaPackage saveOtaPackageData(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, + public OtaPackageInfo saveOtaPackageData(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @RequestParam(required = false) String checksum, @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @RequestBody MultipartFile file) throws ThingsboardException { @@ -160,7 +160,7 @@ public class OtaPackageController extends BaseController { otaPackage.setContentType(file.getContentType()); otaPackage.setData(ByteBuffer.wrap(bytes)); otaPackage.setDataSize((long) bytes.length); - OtaPackage savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); + OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); logEntityAction(savedOtaPackage.getId(), savedOtaPackage, null, ActionType.UPDATED, null); return savedOtaPackage; } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 267fb31ad7..b3f0d644c7 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -199,6 +199,7 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); dataUpdateService.updateData("3.2.2"); + systemDataLoaderService.createOAuth2Templates(); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index b91c46417c..8c27417f90 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; import org.thingsboard.server.common.data.EntityView; @@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; @@ -45,10 +48,11 @@ import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.oauth2.OAuth2Utils; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.service.install.InstallScripts; import java.util.ArrayList; @@ -88,6 +92,9 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private AlarmDao alarmDao; + @Autowired + private OAuth2Service oAuth2Service; + @Override public void updateData(String fromVersion) throws Exception { switch (fromVersion) { @@ -107,6 +114,7 @@ public class DefaultDataUpdateService implements DataUpdateService { log.info("Updating data from version 3.2.2 to 3.3.0 ..."); tenantsDefaultEdgeRuleChainUpdater.updateEntities(null); tenantsAlarmsCustomerUpdater.updateEntities(null); + updateOAuth2Params(); break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); @@ -362,4 +370,20 @@ public class DefaultDataUpdateService implements DataUpdateService { } } + private void updateOAuth2Params() { + try { + OAuth2ClientsParams oauth2ClientsParams = oAuth2Service.findOAuth2Params(); + if (!oauth2ClientsParams.getDomainsParams().isEmpty()) { + log.info("Updating OAuth2 parameters ..."); + OAuth2Info oAuth2Info = OAuth2Utils.clientParamsToOAuth2Info(oauth2ClientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); + oAuth2Service.saveOAuth2Params(new OAuth2ClientsParams(false, Collections.emptyList())); + log.info("Successfully updated OAuth2 parameters!"); + } + } + catch (Exception e) { + log.error("Failed to update OAuth2 parameters", e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java index 06190cdf70..78f849667f 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java @@ -18,7 +18,6 @@ package org.thingsboard.server.service.lwm2m; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.util.Hex; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -50,40 +49,20 @@ public class LwM2MServerSecurityInfoRepository { private final LwM2MTransportServerConfig serverConfig; private final LwM2MTransportBootstrapConfig bootstrapConfig; - /** - * @param securityMode - * @param bootstrapServer - * @return ServerSecurityConfig more value is default: Important - port, host, publicKey - */ - public ServerSecurityConfig getServerSecurityInfo(SecurityMode securityMode, boolean bootstrapServer) { - ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig, securityMode); + public ServerSecurityConfig getServerSecurityInfo(boolean bootstrapServer) { + ServerSecurityConfig result = getServerSecurityConfig(bootstrapServer ? bootstrapConfig : serverConfig); result.setBootstrapServerIs(bootstrapServer); return result; } - private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig, SecurityMode securityMode) { + private ServerSecurityConfig getServerSecurityConfig(LwM2MSecureServerConfig serverConfig) { ServerSecurityConfig bsServ = new ServerSecurityConfig(); bsServ.setServerId(serverConfig.getId()); - switch (securityMode) { - case NO_SEC: - bsServ.setHost(serverConfig.getHost()); - bsServ.setPort(serverConfig.getPort()); - bsServ.setServerPublicKey(""); - break; - case PSK: - bsServ.setHost(serverConfig.getSecureHost()); - bsServ.setPort(serverConfig.getSecurePort()); - bsServ.setServerPublicKey(""); - break; - case RPK: - case X509: - bsServ.setHost(serverConfig.getSecureHost()); - bsServ.setPort(serverConfig.getSecurePort()); - bsServ.setServerPublicKey(getPublicKey(serverConfig.getCertificateAlias(), this.serverConfig.getPublicX(), this.serverConfig.getPublicY())); - break; - default: - break; - } + bsServ.setHost(serverConfig.getHost()); + bsServ.setPort(serverConfig.getPort()); + bsServ.setSecurityHost(serverConfig.getSecureHost()); + bsServ.setSecurityPort(serverConfig.getSecurePort()); + bsServ.setServerPublicKey(getPublicKey(serverConfig.getCertificateAlias(), this.serverConfig.getPublicX(), this.serverConfig.getPublicY())); return bsServ; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index 4f38cf7a23..3ff3534806 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -33,8 +33,8 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -93,9 +93,9 @@ public abstract class AbstractOAuth2ClientMapper { private final Lock userCreationLock = new ReentrantLock(); - protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) { + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2Registration registration) { - OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + OAuth2MapperConfig config = registration.getMapperConfig(); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); @@ -139,9 +139,9 @@ public abstract class AbstractOAuth2ClientMapper { } } - if (clientRegistration.getAdditionalInfo() != null && - clientRegistration.getAdditionalInfo().has("providerName")) { - additionalInfo.put("authProviderName", clientRegistration.getAdditionalInfo().get("providerName").asText()); + if (registration.getAdditionalInfo() != null && + registration.getAdditionalInfo().has("providerName")) { + additionalInfo.put("authProviderName", registration.getAdditionalInfo().get("providerName").asText()); } user.setAdditionalInfo(additionalInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java new file mode 100644 index 0000000000..93da71169c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.service.security.model.SecurityUser; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +@Service(value = "appleOAuth2ClientMapper") +@Slf4j +public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + private static final String USER = "user"; + private static final String NAME = "name"; + private static final String FIRST_NAME = "firstName"; + private static final String LAST_NAME = "lastName"; + private static final String EMAIL = "email"; + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); + Map attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes()); + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); + + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + } + + private static Map updateAttributesFromRequestParams(HttpServletRequest request, Map attributes) { + Map updated = attributes; + MultiValueMap params = toMultiMap(request.getParameterMap()); + String userValue = params.getFirst(USER); + if (StringUtils.hasText(userValue)) { + JsonNode user = null; + try { + user = JacksonUtil.toJsonNode(userValue); + } catch (Exception e) {} + if (user != null) { + updated = new HashMap<>(attributes); + if (user.has(NAME)) { + JsonNode name = user.get(NAME); + if (name.isObject()) { + JsonNode firstName = name.get(FIRST_NAME); + if (firstName != null && firstName.isTextual()) { + updated.put(FIRST_NAME, firstName.asText()); + } + JsonNode lastName = name.get(LAST_NAME); + if (lastName != null && lastName.isTextual()) { + updated.put(LAST_NAME, lastName.asText()); + } + } + } + if (user.has(EMAIL)) { + JsonNode email = user.get(EMAIL); + if (email != null && email.isTextual()) { + updated.put(EMAIL, email.asText()); + } + } + } + } + return updated; + } + + private static MultiValueMap toMultiMap(Map map) { + MultiValueMap params = new LinkedMultiValueMap<>(map.size()); + map.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + params.add(key, value); + } + } + }); + return params; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 940bf8ad0a..d2532d0240 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -18,11 +18,12 @@ package org.thingsboard.server.service.security.auth.oauth2; import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.Map; @Service(value = "basicOAuth2ClientMapper") @@ -30,12 +31,12 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { - OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index cb08bc9f96..778f7416ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -23,12 +23,14 @@ import org.springframework.security.oauth2.client.authentication.OAuth2Authentic import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + @Service(value = "customOAuth2ClientMapper") @Slf4j public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @@ -39,10 +41,10 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { - OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); - return getOrCreateSecurityUserFromOAuth2User(oauth2User, clientRegistration); + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); } private synchronized OAuth2User getOAuth2User(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2CustomMapperConfig custom) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java index 8e41c4a747..3810f36757 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java @@ -23,12 +23,13 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Map; import java.util.Optional; @@ -46,13 +47,13 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { - OAuth2MapperConfig config = clientRegistration.getMapperConfig(); + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); Map attributes = token.getPrincipal().getAttributes(); OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); - return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration); + return getOrCreateSecurityUserFromOAuth2User(oAuth2User, registration); } private synchronized String getEmail(String emailUrl, String oauth2Token) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index 965b34d8b0..39957602d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -16,9 +16,12 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration); + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index 606b4f9b1d..df9e5e05ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -37,6 +37,10 @@ public class OAuth2ClientMapperProvider { @Qualifier("githubOAuth2ClientMapper") private OAuth2ClientMapper githubOAuth2ClientMapper; + @Autowired + @Qualifier("appleOAuth2ClientMapper") + private OAuth2ClientMapper appleOAuth2ClientMapper; + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { switch (oauth2MapperType) { case CUSTOM: @@ -45,6 +49,8 @@ public class OAuth2ClientMapperProvider { return basicOAuth2ClientMapper; case GITHUB: return githubOAuth2ClientMapper; + case APPLE: + return appleOAuth2ClientMapper; default: throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index b6345f1618..95b4643f4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.security.auth.oauth2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,7 +36,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @Component(value = "oauth2AuthenticationFailureHandler") -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; @@ -51,9 +52,19 @@ public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationF public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + String baseUrl; + String errorPrefix; + OAuth2AuthorizationRequest authorizationRequest = httpCookieOAuth2AuthorizationRequestRepository.loadAuthorizationRequest(request); + String callbackUrlScheme = authorizationRequest.getAttribute(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME); + if (!StringUtils.isEmpty(callbackUrlScheme)) { + baseUrl = callbackUrlScheme + ":"; + errorPrefix = "/?error="; + } else { + baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + errorPrefix = "/login?loginError="; + } httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); - getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" + + getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 72a6c65f7c..303a430e77 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -20,12 +20,14 @@ import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; @@ -72,17 +74,24 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + OAuth2AuthorizationRequest authorizationRequest = httpCookieOAuth2AuthorizationRequestRepository.loadAuthorizationRequest(request); + String callbackUrlScheme = authorizationRequest.getAttribute(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME); + String baseUrl; + if (!StringUtils.isEmpty(callbackUrlScheme)) { + baseUrl = callbackUrlScheme + ":"; + } else { + baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + } try { OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; - OAuth2ClientRegistrationInfo clientRegistration = oAuth2Service.findClientRegistrationInfo(UUID.fromString(token.getAuthorizedClientRegistrationId())); + OAuth2Registration registration = oAuth2Service.findRegistration(UUID.fromString(token.getAuthorizedClientRegistrationId())); OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient( token.getAuthorizedClientRegistrationId(), token.getPrincipal().getName()); - OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(clientRegistration.getMapperConfig().getType()); - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), - clientRegistration); + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), + registration); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); @@ -91,7 +100,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); } catch (Exception e) { clearAuthenticationAttributes(request, response); - getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" + + String errorPrefix; + if (!StringUtils.isEmpty(callbackUrlScheme)) { + errorPrefix = "/?error="; + } else { + errorPrefix = "/login?loginError="; + } + getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java new file mode 100644 index 0000000000..aa5b32f055 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/TbOAuth2ParameterNames.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +public interface TbOAuth2ParameterNames { + + String CALLBACK_URL_SCHEME = "callback_url_scheme"; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java new file mode 100644 index 0000000000..fba1598a7f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.model.token; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.micrometer.core.instrument.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class OAuth2AppTokenFactory { + + private static final String CALLBACK_URL_SCHEME = "callbackUrlScheme"; + + private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5); + + public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { + Jws jwsClaims; + try { + jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); + } + catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { + throw new IllegalArgumentException("Invalid Application token: ", ex); + } catch (ExpiredJwtException expiredEx) { + throw new IllegalArgumentException("Application token expired", expiredEx); + } + Claims claims = jwsClaims.getBody(); + Date expiration = claims.getExpiration(); + if (expiration == null) { + throw new IllegalArgumentException("Application token must have expiration date"); + } + long timeDiff = expiration.getTime() - System.currentTimeMillis(); + if (timeDiff > MAX_EXPIRATION_TIME_DIFF_MS) { + throw new IllegalArgumentException("Application token expiration time can't be longer than 5 minutes"); + } + if (!claims.getIssuer().equals(appPackage)) { + throw new IllegalArgumentException("Application token issuer doesn't match application package"); + } + String callbackUrlScheme = claims.get(CALLBACK_URL_SCHEME, String.class); + if (StringUtils.isEmpty(callbackUrlScheme)) { + throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme"); + } + return callbackUrlScheme; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 78658fe8fd..54721ba2fa 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -656,7 +656,7 @@ transport: bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port: "${LWM2M_BIND_PORT_BS:5687}" security: - bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_address: "${LWM2M_BIND_ADDRESS_SECURITY_BS:0.0.0.0}" bind_port: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java index a11d1b624a..6aa341cef6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java @@ -141,10 +141,12 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); Assert.assertEquals(FILE_NAME, savedFirmware.getFileName()); Assert.assertEquals(CONTENT_TYPE, savedFirmware.getContentType()); + Assert.assertEquals(CHECKSUM_ALGORITHM, savedFirmware.getChecksumAlgorithm().name()); + Assert.assertEquals(CHECKSUM, savedFirmware.getChecksum()); } @Test @@ -189,11 +191,12 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); OtaPackage foundFirmware = doGet("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString(), OtaPackage.class); Assert.assertNotNull(foundFirmware); - Assert.assertEquals(savedFirmware, foundFirmware); + Assert.assertEquals(savedFirmware, new OtaPackageInfo(foundFirmware)); + Assert.assertEquals(DATA, foundFirmware.getData()); } @Test @@ -228,8 +231,8 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes if (i > 100) { MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); - otaPackages.add(new OtaPackageInfo(savedFirmware)); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + otaPackages.add(savedFirmware); } else { otaPackages.add(savedFirmwareInfo); } @@ -271,7 +274,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes if (i > 100) { MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); - OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + OtaPackageInfo savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); savedFirmwareInfo = new OtaPackageInfo(savedFirmware); otaPackagesWithData.add(savedFirmwareInfo); } @@ -318,11 +321,11 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes return doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class); } - protected OtaPackage savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception { + protected OtaPackageInfo savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception { MockMultipartHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.multipart(urlTemplate, params); postRequest.file(content); setJwtToken(postRequest); - return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackage.class); + return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java index 3f9411d0aa..9764137cf2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java @@ -16,20 +16,31 @@ package org.thingsboard.server.dao.oauth2; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams; import java.util.List; import java.util.UUID; public interface OAuth2Service { - List getOAuth2Clients(String domainScheme, String domainName); + List getOAuth2Clients(String domainScheme, String domainName, String pkgName, PlatformType platformType); + @Deprecated void saveOAuth2Params(OAuth2ClientsParams oauth2Params); + @Deprecated OAuth2ClientsParams findOAuth2Params(); - OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id); + void saveOAuth2Info(OAuth2Info oauth2Info); - List findAllClientRegistrationInfos(); + OAuth2Info findOAuth2Info(); + + OAuth2Registration findRegistration(UUID id); + + List findAllRegistrations(); + + String findAppSecret(UUID registrationId, String pkgName); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 09c9f084cb..f578cd15c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -31,5 +32,6 @@ public class AlarmRule implements Serializable { // Advanced @NoXss private String alarmDetails; + private DashboardId dashboardId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java new file mode 100644 index 0000000000..220232f16d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2DomainId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2DomainId extends UUIDBased { + + @JsonCreator + public OAuth2DomainId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2DomainId fromString(String oauth2DomainId) { + return new OAuth2DomainId(UUID.fromString(oauth2DomainId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java new file mode 100644 index 0000000000..934d1f72a6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2MobileId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2MobileId extends UUIDBased { + + @JsonCreator + public OAuth2MobileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2MobileId fromString(String oauth2MobileId) { + return new OAuth2MobileId(UUID.fromString(oauth2MobileId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ParamsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ParamsId.java new file mode 100644 index 0000000000..3aa0c5bb3d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ParamsId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2ParamsId extends UUIDBased { + + @JsonCreator + public OAuth2ParamsId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2ParamsId fromString(String oauth2ParamsId) { + return new OAuth2ParamsId(UUID.fromString(oauth2ParamsId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java new file mode 100644 index 0000000000..0019ef1a04 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2RegistrationId.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class OAuth2RegistrationId extends UUIDBased { + + @JsonCreator + public OAuth2RegistrationId(@JsonProperty("id") UUID id) { + super(id); + } + + public static OAuth2RegistrationId fromString(String oauth2RegistrationId) { + return new OAuth2RegistrationId(UUID.fromString(oauth2RegistrationId)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationId.java similarity index 89% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationId.java index 88d4245dc7..616fc3b29c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationId.java @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.id; +package org.thingsboard.server.common.data.id.deprecated; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.id.UUIDBased; import java.util.UUID; +@Deprecated public class OAuth2ClientRegistrationId extends UUIDBased { @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationInfoId.java similarity index 89% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationInfoId.java index 2e34eaf5ac..6ba959318c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2ClientRegistrationInfoId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/deprecated/OAuth2ClientRegistrationInfoId.java @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.id; +package org.thingsboard.server.common.data.id.deprecated; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.id.UUIDBased; import java.util.UUID; +@Deprecated public class OAuth2ClientRegistrationInfoId extends UUIDBased { @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java index 8777e08066..8220702ec8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java @@ -20,7 +20,9 @@ import lombok.Data; @Data public class ServerSecurityConfig { String host; + String securityHost; Integer port; + Integer securityPort; String serverPublicKey; boolean bootstrapServerIs = true; Integer clientHoldOffTime = 1; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java index 3f91e14bd9..4811e153d9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.data.oauth2; public enum MapperType { - BASIC, CUSTOM, GITHUB; + BASIC, CUSTOM, GITHUB, APPLE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index f75fb2aaa7..b46f23484f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -20,10 +20,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; -import org.thingsboard.server.common.data.id.TenantId; import java.util.List; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java new file mode 100644 index 0000000000..0dee447023 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Domain.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.OAuth2DomainId; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2Domain extends BaseData { + + private OAuth2ParamsId oauth2ParamsId; + private String domainName; + private SchemeType domainScheme; + + public OAuth2Domain(OAuth2Domain domain) { + super(domain); + this.oauth2ParamsId = domain.oauth2ParamsId; + this.domainName = domain.domainName; + this.domainScheme = domain.domainScheme; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java new file mode 100644 index 0000000000..9d9bd939da --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@EqualsAndHashCode +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OAuth2DomainInfo { + private SchemeType scheme; + private String name; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java new file mode 100644 index 0000000000..72f4b06161 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.*; + +import java.util.List; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2Info { + private boolean enabled; + private List oauth2ParamsInfos; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java new file mode 100644 index 0000000000..53a6d41e5c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.OAuth2MobileId; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2Mobile extends BaseData { + + private OAuth2ParamsId oauth2ParamsId; + private String pkgName; + private String appSecret; + + public OAuth2Mobile(OAuth2Mobile mobile) { + super(mobile); + this.oauth2ParamsId = mobile.oauth2ParamsId; + this.pkgName = mobile.pkgName; + this.appSecret = mobile.appSecret; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java new file mode 100644 index 0000000000..c04a1a9fd1 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@EqualsAndHashCode +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OAuth2MobileInfo { + private String pkgName; + private String appSecret; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java new file mode 100644 index 0000000000..570fe6cb20 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Params.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.id.TenantId; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString +@NoArgsConstructor +public class OAuth2Params extends BaseData { + + private boolean enabled; + private TenantId tenantId; + + public OAuth2Params(OAuth2Params oauth2Params) { + super(oauth2Params); + this.enabled = oauth2Params.enabled; + this.tenantId = oauth2Params.tenantId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java new file mode 100644 index 0000000000..1a1d729d6b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@EqualsAndHashCode +@Data +@ToString +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2ParamsInfo { + + private List domainInfos; + private List mobileInfos; + private List clientRegistrations; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java new file mode 100644 index 0000000000..095462b16f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Registration.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.id.OAuth2RegistrationId; + +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +public class OAuth2Registration extends SearchTextBasedWithAdditionalInfo implements HasName { + + private OAuth2ParamsId oauth2ParamsId; + private OAuth2MapperConfig mapperConfig; + private String clientId; + private String clientSecret; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String loginButtonLabel; + private String loginButtonIcon; + private List platforms; + + public OAuth2Registration(OAuth2Registration registration) { + super(registration); + this.oauth2ParamsId = registration.oauth2ParamsId; + this.mapperConfig = registration.mapperConfig; + this.clientId = registration.clientId; + this.clientSecret = registration.clientSecret; + this.authorizationUri = registration.authorizationUri; + this.accessTokenUri = registration.accessTokenUri; + this.scope = registration.scope; + this.userInfoUri = registration.userInfoUri; + this.userNameAttributeName = registration.userNameAttributeName; + this.jwkSetUri = registration.jwkSetUri; + this.clientAuthenticationMethod = registration.clientAuthenticationMethod; + this.loginButtonLabel = registration.loginButtonLabel; + this.loginButtonIcon = registration.loginButtonIcon; + this.platforms = registration.platforms; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getName() { + return loginButtonLabel; + } + + @Override + public String getSearchText() { + return getName(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java index 2598013ab6..3c9a1c1974 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ClientRegistrationDto.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.oauth2; import com.fasterxml.jackson.databind.JsonNode; import lombok.*; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; import java.util.List; @@ -27,7 +26,7 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder -public class ClientRegistrationDto { +public class OAuth2RegistrationInfo { private OAuth2MapperConfig mapperConfig; private String clientId; private String clientSecret; @@ -40,5 +39,6 @@ public class ClientRegistrationDto { private String clientAuthenticationMethod; private String loginButtonLabel; private String loginButtonIcon; + private List platforms; private JsonNode additionalInfo; } diff --git a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.scss b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/PlatformType.java similarity index 85% rename from ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.scss rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/PlatformType.java index 1c55a86d8b..9c775e86e7 100644 --- a/ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.scss +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/PlatformType.java @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host ::ng-deep { - textarea.mat-input-element.cdk-textarea-autosize { - box-sizing: content-box; - } +package org.thingsboard.server.common.data.oauth2; + +public enum PlatformType { + WEB, ANDROID, IOS } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ClientRegistrationDto.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ClientRegistrationDto.java new file mode 100644 index 0000000000..4f491a2d8a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ClientRegistrationDto.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.oauth2.deprecated; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.*; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; + +import java.util.List; + +@Deprecated +@EqualsAndHashCode +@Data +@ToString(exclude = {"clientSecret"}) +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ClientRegistrationDto { + private OAuth2MapperConfig mapperConfig; + private String clientId; + private String clientSecret; + private String authorizationUri; + private String accessTokenUri; + private List scope; + private String userInfoUri; + private String userNameAttributeName; + private String jwkSetUri; + private String clientAuthenticationMethod; + private String loginButtonLabel; + private String loginButtonIcon; + private JsonNode additionalInfo; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/DomainInfo.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/DomainInfo.java index fd6b93100c..5078c4483f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/DomainInfo.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import lombok.*; +import org.thingsboard.server.common.data.oauth2.SchemeType; +@Deprecated @EqualsAndHashCode @Data @ToString diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ExtendedOAuth2ClientRegistrationInfo.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ExtendedOAuth2ClientRegistrationInfo.java index 83647283a0..d071a6f1d4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/ExtendedOAuth2ClientRegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/ExtendedOAuth2ClientRegistrationInfo.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; +@Deprecated @EqualsAndHashCode(callSuper = true) @Data public class ExtendedOAuth2ClientRegistrationInfo extends OAuth2ClientRegistrationInfo { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistration.java similarity index 81% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistration.java index d0fa6648c8..cfac2de329 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistration.java @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.BaseData; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationId; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.SchemeType; +@Deprecated @EqualsAndHashCode(callSuper = true) @Data @ToString diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistrationInfo.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistrationInfo.java index f70000693c..a99f56689b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientRegistrationInfo.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @@ -22,10 +22,12 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; import java.util.List; +@Deprecated @EqualsAndHashCode(callSuper = true) @Data @ToString(exclude = {"clientSecret"}) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsDomainParams.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsDomainParams.java index 1570f71107..ff17fe2819 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsDomainParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsDomainParams.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import lombok.*; import java.util.List; -import java.util.Set; +@Deprecated @EqualsAndHashCode @Data @ToString diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsParams.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java rename to common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsParams.java index 57c7351295..4f7735ef04 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientsParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/deprecated/OAuth2ClientsParams.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.oauth2; +package org.thingsboard.server.common.data.oauth2.deprecated; import lombok.*; import java.util.List; -import java.util.Set; +@Deprecated @EqualsAndHashCode @Data @ToString diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index faf88c5620..acea8d6382 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -310,10 +310,12 @@ message SessionCloseNotificationProto { message SubscribeToAttributeUpdatesMsg { bool unsubscribe = 1; + SessionType sessionType = 2; } message SubscribeToRPCMsg { bool unsubscribe = 1; + SessionType sessionType = 2; } message ToDeviceRpcRequestMsg { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index cd80aa42df..33f122fa3f 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -482,13 +482,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource { String title = exchange.getQueryParameter("title"); String version = exchange.getQueryParameter("version"); if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) { + String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString(); if (msg.getTitle().equals(title) && msg.getVersion().equals(version)) { - String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString(); String strChunkSize = exchange.getQueryParameter("size"); String strChunk = exchange.getQueryParameter("chunk"); int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize); int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk); exchange.respond(CoAP.ResponseCode.CONTENT, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk)); + } + else if (firmwareId != null) { + sendOtaData(exchange, firmwareId); } else { exchange.respond(CoAP.ResponseCode.BAD_REQUEST); } @@ -504,6 +507,20 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } + private void sendOtaData(CoapExchange exchange, String firmwareId) { + Response response = new Response(CoAP.ResponseCode.CONTENT); + byte[] fwData = transportContext.getOtaPackageDataCache().get(firmwareId); + if (fwData != null && fwData.length > 0) { + response.setPayload(fwData); + if (exchange.getRequestOptions().getBlock2() != null) { + int chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); + boolean moreFlag = fwData.length > chunkSize; + response.getOptions().setBlock2(chunkSize, moreFlag, 0); + } + exchange.respond(response); + } + } + private static class CoapSessionListener implements SessionMsgListener { private final CoapTransportResource coapTransportResource; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index b22cfde859..8823881ae7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -102,7 +102,7 @@ public class LwM2MTransportBootstrapService { builder.setLocalSecureAddress(bootstrapConfig.getSecureHost(), bootstrapConfig.getSecurePort()); /** Create CoAP Config */ - builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort())); + builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); /** Define model provider (Create Models )*/ diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java new file mode 100644 index 0000000000..2c82facd23 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/AbstractLwM2mTransportResource.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.leshan.core.californium.LwM2mCoapResource; +import org.thingsboard.server.common.transport.TransportServiceCallback; + +@Slf4j +public abstract class AbstractLwM2mTransportResource extends LwM2mCoapResource { + + public AbstractLwM2mTransportResource(String name) { + super(name); + } + + @Override + public void handleGET(CoapExchange exchange) { + processHandleGet(exchange); + } + + @Override + public void handlePOST(CoapExchange exchange) { + processHandlePost(exchange); + } + + protected abstract void processHandleGet(CoapExchange exchange); + + protected abstract void processHandlePost(CoapExchange exchange); + + public static class CoapOkCallback implements TransportServiceCallback { + private final CoapExchange exchange; + private final CoAP.ResponseCode onSuccessResponse; + private final CoAP.ResponseCode onFailureResponse; + + public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { + this.exchange = exchange; + this.onSuccessResponse = onSuccessResponse; + this.onFailureResponse = onFailureResponse; + } + + @Override + public void onSuccess(Void msg) { + Response response = new Response(onSuccessResponse); + response.setAcknowledged(isConRequest()); + exchange.respond(response); + } + + @Override + public void onError(Throwable e) { + exchange.respond(onFailureResponse); + } + + private boolean isConRequest() { + return exchange.advanced().getRequest().isConfirmable(); + } + } + + public static class CoapNoOpCallback implements TransportServiceCallback { + private final CoapExchange exchange; + + CoapNoOpCallback(CoapExchange exchange) { + this.exchange = exchange; + } + + @Override + public void onSuccess(Void msg) { + } + + @Override + public void onError(Throwable e) { + exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); + } + } + + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java index 6c5bbb5970..bff3b9d60d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java @@ -31,7 +31,6 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.observation.Observation; -import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.server.registration.Registration; @@ -42,10 +41,10 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.ota.OtaPackageKey; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUtil; -import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; @@ -63,7 +62,8 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientStateExce import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; -import org.thingsboard.server.transport.lwm2m.server.client.Lwm2mClientRpcRequest; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientRpcRequest; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mFwSwUpdate; import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; @@ -89,20 +89,20 @@ import java.util.stream.Collectors; import static org.eclipse.californium.core.coap.CoAP.ResponseCode.BAD_REQUEST; import static org.eclipse.leshan.core.attributes.Attribute.OBJECT_VERSION; -import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; -import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INITIATED; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getValueFromKvProto; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.DEVICE_ATTRIBUTES_REQUEST; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_5_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_RESULT_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_STATE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_TELEMETRY; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_VALUE; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LWM2M_STRATEGY_2; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_WARN; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL_ALL; @@ -110,8 +110,8 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.L import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_ATTRIBUTES; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_ID; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_RESULT_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertJsonArrayToSet; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertOtaUpdateValueToString; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getAckCallback; @@ -140,6 +140,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler public final LwM2mClientContext clientContext; public final LwM2mTransportRequest lwM2mTransportRequest; private final Map rpcSubscriptions; + public final Map firmwareUpdateState; public DefaultLwM2MTransportMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, LwM2mTransportServerHelper helper, LwM2mClientContext clientContext, @@ -155,6 +156,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler this.context = context; this.adaptor = adaptor; this.rpcSubscriptions = new ConcurrentHashMap<>(); + this.firmwareUpdateState = new ConcurrentHashMap<>(); this.sessionStore = sessionStore; } @@ -183,39 +185,39 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler */ public void onRegistered(Registration registration, Collection previousObservations) { registrationExecutor.submit(() -> { + LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); try { log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); - LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); if (lwM2MClient != null) { this.clientContext.register(lwM2MClient, registration); + this.sendLogsToThingsboard(lwM2MClient, LOG_LW2M_INFO + ": Client registered with registration id: " + registration.getId()); SessionInfoProto sessionInfo = lwM2MClient.getSession(); - if (sessionInfo != null) { - transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); - TransportProtos.TransportToDeviceActorMsg msg = TransportProtos.TransportToDeviceActorMsg.newBuilder() - .setSessionInfo(sessionInfo) - .setSessionEvent(DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN)) - .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build()) - .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder().build()) - .build(); - transportService.process(msg, null); - this.getInfoFirmwareUpdate(lwM2MClient, null); - this.getInfoSoftwareUpdate(lwM2MClient, null); - this.initLwM2mFromClientValue(registration, lwM2MClient); - this.sendLogsToThingsboard(lwM2MClient, LOG_LW2M_INFO + ": Client create after Registration"); - } else { - log.error("Client: [{}] onRegistered [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); - } + transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); + log.warn("40) sessionId [{}] Registering rpc subscription after Registration client", new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); + TransportProtos.TransportToDeviceActorMsg msg = TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(sessionInfo) + .setSessionEvent(DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN)) + .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setSessionType(TransportProtos.SessionType.ASYNC).build()) + .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder().setSessionType(TransportProtos.SessionType.ASYNC).build()) + .build(); + transportService.process(msg, null); + this.getInfoFirmwareUpdate(lwM2MClient, null); + this.getInfoSoftwareUpdate(lwM2MClient, null); + this.initClientTelemetry(lwM2MClient); } else { - log.error("Client: [{}] onRegistered [{}] name [{}] lwM2MClient ", registration.getId(), registration.getEndpoint(), null); + log.error("Client: [{}] onRegistered [{}] name [{}] lwM2MClient ", registration.getId(), registration.getEndpoint(), null); } } catch (LwM2MClientStateException stateException) { if (LwM2MClientState.UNREGISTERED.equals(stateException.getState())) { log.info("[{}] retry registration due to race condition: [{}].", registration.getEndpoint(), stateException.getState()); // Race condition detected and the client was in progress of unregistration while new registration arrived. Let's try again. onRegistered(registration, previousObservations); + } else { + this.sendLogsToThingsboard(lwM2MClient, LOG_LW2M_WARN + ": Client registration failed due to invalid state: " + stateException.getState()); } } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable registration.", registration.getEndpoint(), t); + this.sendLogsToThingsboard(lwM2MClient, LOG_LW2M_WARN + ": Client registration failed due to: " + t.getMessage()); } }); } @@ -232,17 +234,17 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler clientContext.updateRegistration(lwM2MClient, registration); TransportProtos.SessionInfoProto sessionInfo = lwM2MClient.getSession(); this.reportActivityAndRegister(sessionInfo); - if (registration.getQueueMode()) { + if (registration.usesQueueMode()) { LwM2mQueuedRequest request; while ((request = lwM2MClient.getQueuedRequests().poll()) != null) { request.send(); } } } catch (LwM2MClientStateException stateException) { - if (LwM2MClientState.UNREGISTERED.equals(stateException.getState())) { - log.info("[{}] update registration failed because client was already unregistered: [{}].", registration.getEndpoint(), stateException.getState()); + if (LwM2MClientState.REGISTERED.equals(stateException.getState())) { + log.info("[{}] update registration failed because client has different registration id: [{}] {}.", registration.getEndpoint(), stateException.getState(), stateException.getMessage()); } else { - log.info("[{}] update registration: [{}] {}.", registration.getEndpoint(), stateException.getState(), stateException.getMessage()); + onRegistered(registration, Collections.emptyList()); } } catch (Throwable t) { log.error("[{}] endpoint [{}] error Unable update registration.", registration.getEndpoint(), t); @@ -263,9 +265,9 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler clientContext.unregister(client, registration); SessionInfoProto sessionInfo = client.getSession(); if (sessionInfo != null) { + this.doCloseSession(sessionInfo); transportService.deregisterSession(sessionInfo); sessionStore.remove(registration.getEndpoint()); - this.doCloseSession(sessionInfo); log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); } else { log.error("Client close session: [{}] unReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); @@ -292,8 +294,11 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler @Override public void setCancelObservationsAll(Registration registration) { if (registration != null) { - this.lwM2mTransportRequest.sendAllRequest(registration, null, OBSERVE_CANCEL_ALL, - null, null, this.config.getTimeout(), null); + LwM2mClient client = clientContext.getClientByEndpoint(registration.getEndpoint()); + if (client != null && client.getRegistration() != null && client.getRegistration().getId().equals(registration.getId())) { + this.lwM2mTransportRequest.sendAllRequest(client, null, OBSERVE_CANCEL_ALL, + null, null, this.config.getTimeout(), null); + } } } @@ -305,7 +310,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * @param response - observe */ @Override - public void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, Lwm2mClientRpcRequest rpcRequest) { + public void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, LwM2mClientRpcRequest rpcRequest) { if (response.getContent() != null) { LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, this.config.getModelProvider()); @@ -328,7 +333,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } private void sendRpcRequestAfterReadResponse(Registration registration, LwM2mClient lwM2MClient, String pathIdVer, ReadResponse response, - Lwm2mClientRpcRequest rpcRequest) { + LwM2mClientRpcRequest rpcRequest) { Object value = null; if (response.getContent() instanceof LwM2mObject) { value = lwM2MClient.objectToString((LwM2mObject) response.getContent(), this.converter, pathIdVer); @@ -445,7 +450,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler /** * #1 del from rpcSubscriptions by timeout - * #2 if not present in rpcSubscriptions by requestId: create new Lwm2mClientRpcRequest, after success - add requestId, timeout + * #2 if not present in rpcSubscriptions by requestId: create new LwM2mClientRpcRequest, after success - add requestId, timeout */ @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg, SessionInfoProto sessionInfo) { @@ -457,38 +462,53 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler UUID requestUUID = new UUID(toDeviceRpcRequestMsg.getRequestIdMSB(), toDeviceRpcRequestMsg.getRequestIdLSB()); if (!this.rpcSubscriptions.containsKey(requestUUID)) { this.rpcSubscriptions.put(requestUUID, toDeviceRpcRequestMsg.getExpirationTime()); - Lwm2mClientRpcRequest lwm2mClientRpcRequest = null; + LwM2mClientRpcRequest lwm2mClientRpcRequest = null; try { - Registration registration = clientContext.getClientBySessionInfo(sessionInfo).getRegistration(); - lwm2mClientRpcRequest = new Lwm2mClientRpcRequest(lwM2mTypeOper, bodyParams, toDeviceRpcRequestMsg.getRequestId(), sessionInfo, registration, this); - if (lwm2mClientRpcRequest.getErrorMsg() != null) { - lwm2mClientRpcRequest.setResponseCode(BAD_REQUEST.name()); - this.onToDeviceRpcResponse(lwm2mClientRpcRequest.getDeviceRpcResponseResultMsg(), sessionInfo); + LwM2mClient client = clientContext.getClientBySessionInfo(sessionInfo); + Registration registration = client.getRegistration(); + if(registration != null) { + lwm2mClientRpcRequest = new LwM2mClientRpcRequest(lwM2mTypeOper, bodyParams, toDeviceRpcRequestMsg.getRequestId(), sessionInfo, registration, this); + if (lwm2mClientRpcRequest.getErrorMsg() != null) { + lwm2mClientRpcRequest.setResponseCode(BAD_REQUEST.name()); + this.onToDeviceRpcResponse(lwm2mClientRpcRequest.getDeviceRpcResponseResultMsg(), sessionInfo); + } else { + lwM2mTransportRequest.sendAllRequest(client, lwm2mClientRpcRequest.getTargetIdVer(), lwm2mClientRpcRequest.getTypeOper(), + null, + lwm2mClientRpcRequest.getValue() == null ? lwm2mClientRpcRequest.getParams() : lwm2mClientRpcRequest.getValue(), + this.config.getTimeout(), lwm2mClientRpcRequest); + } } else { - lwM2mTransportRequest.sendAllRequest(registration, lwm2mClientRpcRequest.getTargetIdVer(), lwm2mClientRpcRequest.getTypeOper(), - null, - lwm2mClientRpcRequest.getValue() == null ? lwm2mClientRpcRequest.getParams() : lwm2mClientRpcRequest.getValue(), - this.config.getTimeout(), lwm2mClientRpcRequest); + this.sendErrorRpcResponse(lwm2mClientRpcRequest, "registration == null", sessionInfo); } } catch (Exception e) { - if (lwm2mClientRpcRequest == null) { - lwm2mClientRpcRequest = new Lwm2mClientRpcRequest(); - } - lwm2mClientRpcRequest.setResponseCode(BAD_REQUEST.name()); - if (lwm2mClientRpcRequest.getErrorMsg() == null) { - lwm2mClientRpcRequest.setErrorMsg(e.getMessage()); - } - this.onToDeviceRpcResponse(lwm2mClientRpcRequest.getDeviceRpcResponseResultMsg(), sessionInfo); + this.sendErrorRpcResponse(lwm2mClientRpcRequest, e.getMessage(), sessionInfo); } } } + private void sendErrorRpcResponse(LwM2mClientRpcRequest lwm2mClientRpcRequest, String msgError, SessionInfoProto sessionInfo) { + if (lwm2mClientRpcRequest == null) { + lwm2mClientRpcRequest = new LwM2mClientRpcRequest(); + } + lwm2mClientRpcRequest.setResponseCode(BAD_REQUEST.name()); + if (lwm2mClientRpcRequest.getErrorMsg() == null) { + lwm2mClientRpcRequest.setErrorMsg(msgError); + } + this.onToDeviceRpcResponse(lwm2mClientRpcRequest.getDeviceRpcResponseResultMsg(), sessionInfo); + } + private void checkRpcRequestTimeout() { - Set rpcSubscriptionsToRemove = rpcSubscriptions.entrySet().stream().filter(kv -> System.currentTimeMillis() > kv.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet()); - rpcSubscriptionsToRemove.forEach(rpcSubscriptions::remove); + log.warn("4.1) before rpcSubscriptions.size(): [{}]", rpcSubscriptions.size()); + if (rpcSubscriptions.size() > 0) { + Set rpcSubscriptionsToRemove = rpcSubscriptions.entrySet().stream().filter(kv -> System.currentTimeMillis() > kv.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet()); + log.warn("4.2) System.currentTimeMillis(): [{}]", System.currentTimeMillis()); + log.warn("4.3) rpcSubscriptionsToRemove: [{}]", rpcSubscriptionsToRemove); + rpcSubscriptionsToRemove.forEach(rpcSubscriptions::remove); + } + log.warn("4.4) after rpcSubscriptions.size(): [{}]", rpcSubscriptions.size()); } - public void sentRpcResponse(Lwm2mClientRpcRequest rpcRequest, String requestCode, String msg, String typeMsg) { + public void sentRpcResponse(LwM2mClientRpcRequest rpcRequest, String requestCode, String msg, String typeMsg) { rpcRequest.setResponseCode(requestCode); if (LOG_LW2M_ERROR.equals(typeMsg)) { rpcRequest.setInfoMsg(null); @@ -519,17 +539,6 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler log.info("[{}] toServerRpcResponse", toServerResponse); } - /** - * Trigger Server path = "/1/0/8" - *

- * Trigger bootStrap path = "/1/0/9" - have to implemented on client - */ - @Override - public void doTrigger(Registration registration, String path) { - lwM2mTransportRequest.sendAllRequest(registration, path, EXECUTE, - ContentFormat.TLV.getName(), null, this.config.getTimeout(), null); - } - /** * Deregister session in transport * @@ -572,7 +581,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * @param registrationId - Id of Registration LwM2M Client */ @Override - public void sendLogsToThingsboard2(String registrationId, String logMsg) { + public void sendLogsToThingsboard(String registrationId, String logMsg) { sendLogsToThingsboard(clientContext.getClientByRegistrationId(registrationId), logMsg); } @@ -595,24 +604,23 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * - Read Request to the client after registration to read all resource values for all objects * - then Observe Request to the client marked as observe from the profile configuration. * - * @param registration - Registration LwM2M Client - * @param lwM2MClient - object with All parameters off client + * @param lwM2MClient - object with All parameters off client */ - private void initLwM2mFromClientValue(Registration registration, LwM2mClient lwM2MClient) { - LwM2mClientProfile lwM2MClientProfile = clientContext.getProfile(registration); - Set clientObjects = clientContext.getSupportedIdVerInClient(registration); + private void initClientTelemetry(LwM2mClient lwM2MClient) { + LwM2mClientProfile lwM2MClientProfile = clientContext.getProfile(lwM2MClient.getProfileId()); + Set clientObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); if (clientObjects != null && clientObjects.size() > 0) { - if (LWM2M_STRATEGY_2 == LwM2mTransportUtil.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { + if (LwM2mTransportUtil.LwM2MClientStrategy.CLIENT_STRATEGY_2.code == lwM2MClientProfile.getClientStrategy()) { // #2 lwM2MClient.getPendingReadRequests().addAll(clientObjects); - clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, READ, ContentFormat.TLV.getName(), + clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(lwM2MClient, path, READ, null, this.config.getTimeout(), null)); } // #1 - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, READ, clientObjects); - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, OBSERVE, clientObjects); - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, WRITE_ATTRIBUTES, clientObjects); - this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, DISCOVER, clientObjects); + this.initReadAttrTelemetryObserveToClient(lwM2MClient, READ, clientObjects); + this.initReadAttrTelemetryObserveToClient(lwM2MClient, OBSERVE, clientObjects); + this.initReadAttrTelemetryObserveToClient(lwM2MClient, WRITE_ATTRIBUTES, clientObjects); + this.initReadAttrTelemetryObserveToClient(lwM2MClient, DISCOVER, clientObjects); } } @@ -659,60 +667,18 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler /** version != null * set setClient_fw_info... = value **/ - if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getFwUpdate() != null && lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getFwUpdate().initReadValue(this, this.lwM2mTransportRequest, path); } - if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getSwUpdate() != null && lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getSwUpdate().initReadValue(this, this.lwM2mTransportRequest, path); } - /** - * Before operation Execute (FwUpdate) inspection Update Result : - * - after finished operation Write result: success (FwUpdate): fw_state = DOWNLOADED - * - before start operation Execute (FwUpdate) Update Result = 0 - Initial value - * - start Execute (FwUpdate) - * After finished operation Execute (FwUpdate) inspection Update Result : - * - after start operation Execute (FwUpdate): fw_state = UPDATING - * - after success finished operation Execute (FwUpdate) Update Result == 1 ("Firmware updated successfully") - * - finished operation Execute (FwUpdate) - */ - if (lwM2MClient.getFwUpdate() != null - && (convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path))) { - if (DOWNLOADED.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) - && lwM2MClient.getFwUpdate().conditionalFwExecuteStart()) { - lwM2MClient.getFwUpdate().executeFwSwWare(this, this.lwM2mTransportRequest); - } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) - && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterSuccess()) { - lwM2MClient.getFwUpdate().finishFwSwUpdate(this, true); - } else if (UPDATING.name().equals(lwM2MClient.getFwUpdate().getStateUpdate()) - && lwM2MClient.getFwUpdate().conditionalFwExecuteAfterError()) { - lwM2MClient.getFwUpdate().finishFwSwUpdate(this, false); - } - } - - /** - * Before operation Execute (SwUpdate) inspection Update Result : - * - after finished operation Write result: success (SwUpdate): fw_state = DOWNLOADED - * - before operation Execute (SwUpdate) Update Result = 3 - Successfully Downloaded and package integrity verified - * - start Execute (SwUpdate) - * After finished operation Execute (SwUpdate) inspection Update Result : - * - after start operation Execute (SwUpdate): fw_state = UPDATING - * - after success finished operation Execute (SwUpdate) Update Result == 2 "Software successfully installed."" - * - after success finished operation Execute (SwUpdate) Update Result == 2 "Software successfully installed."" - * - finished operation Execute (SwUpdate) - */ - if (lwM2MClient.getSwUpdate() != null - && (convertPathFromObjectIdToIdVer(SW_RESULT_ID, registration).equals(path))) { - if (DOWNLOADED.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) - && lwM2MClient.getSwUpdate().conditionalSwUpdateExecute()) { - lwM2MClient.getSwUpdate().executeFwSwWare(this, this.lwM2mTransportRequest); - } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) - && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterSuccess()) { - lwM2MClient.getSwUpdate().finishFwSwUpdate(this, true); - } else if (UPDATING.name().equals(lwM2MClient.getSwUpdate().getStateUpdate()) - && lwM2MClient.getSwUpdate().conditionalSwExecuteAfterError()) { - lwM2MClient.getSwUpdate().finishFwSwUpdate(this, false); - } + if ((convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path)) || + (convertPathFromObjectIdToIdVer(FW_STATE_ID, registration).equals(path))) { + LwM2mFwSwUpdate fwUpdate = lwM2MClient.getFwUpdate(clientContext); + log.warn("93) path: [{}] value: [{}]", path, lwM2mResource.getValue()); + fwUpdate.updateStateOta(this, lwM2mTransportRequest, registration, path, ((Long) lwM2mResource.getValue()).intValue()); } Set paths = new HashSet<>(); paths.add(path); @@ -749,15 +715,8 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } } - /** - * Start observe/read: Attr/Telemetry - * #1 - Analyze: path in resource profile == client resource - * - * @param registration - - */ - private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, - LwM2mTypeOper typeOper, Set clientObjects) { - LwM2mClientProfile lwM2MClientProfile = clientContext.getProfile(registration); + private void initReadAttrTelemetryObserveToClient(LwM2mClient lwM2MClient, LwM2mTypeOper typeOper, Set clientObjects) { + LwM2mClientProfile lwM2MClientProfile = clientContext.getProfile(lwM2MClient.getProfileId()); Set result = null; ConcurrentHashMap params = null; if (READ.equals(typeOper)) { @@ -777,28 +736,42 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler params = this.getPathForWriteAttributes(lwM2MClientProfile.getPostAttributeLwm2mProfile()); result = params.keySet(); } - if (result != null && !result.isEmpty()) { - // #1 - Set pathSend = result.stream().filter(target -> { - return target.split(LWM2M_SEPARATOR_PATH).length < 3 ? - clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1]) : - clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1] + "/" + target.split(LWM2M_SEPARATOR_PATH)[2]); - } + sendRequestsToClient(lwM2MClient, typeOper, clientObjects, result, params); + } + + private void sendRequestsToClient(LwM2mClient lwM2MClient, LwM2mTypeOper operationType, Set supportedObjectIds, Set desiredObjectIds, ConcurrentHashMap params) { + if (desiredObjectIds != null && !desiredObjectIds.isEmpty()) { + Set targetObjectIds = desiredObjectIds.stream().filter(target -> isSupportedTargetId(supportedObjectIds, target) ).collect(Collectors.toUnmodifiableSet()); - if (!pathSend.isEmpty()) { - lwM2MClient.getPendingReadRequests().addAll(pathSend); - ConcurrentHashMap finalParams = params; - pathSend.forEach(target -> { - lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TEXT.getName(), - finalParams != null ? finalParams.get(target) : null, this.config.getTimeout(), null); + if (!targetObjectIds.isEmpty()) { + //TODO: remove this side effect? + lwM2MClient.getPendingReadRequests().addAll(targetObjectIds); + targetObjectIds.forEach(target -> { + Object additionalParams = params != null ? params.get(target) : null; + lwM2mTransportRequest.sendAllRequest(lwM2MClient, target, operationType, additionalParams, this.config.getTimeout(), null); }); - if (OBSERVE.equals(typeOper)) { + if (OBSERVE.equals(operationType)) { lwM2MClient.initReadValue(this, null); } } } } + private boolean isSupportedTargetId(Set supportedIds, String targetId) { + String[] targetIdParts = targetId.split(LWM2M_SEPARATOR_PATH); + if (targetIdParts.length <= 1) { + return false; + } + String targetIdSearch = targetIdParts[0]; + for (int i = 1; i < targetIdParts.length; i++) { + targetIdSearch += "/" + targetIdParts[i]; + if (supportedIds.contains(targetIdSearch)) { + return true; + } + } + return false; + } + private ConcurrentHashMap getPathForWriteAttributes(JsonObject objectJson) { ConcurrentHashMap pathAttributes = new Gson().fromJson(objectJson.toString(), new TypeToken>() { @@ -867,8 +840,9 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler valueKvProto = new JsonObject(); Object finalvalueKvProto = valueKvProto; Gson gson = new GsonBuilder().create(); + ResourceModel.Type finalCurrentType = currentType; resourceValue.getInstances().forEach((k, v) -> { - Object val = this.converter.convertValue(v, currentType, expectedType, + Object val = this.converter.convertValue(v, finalCurrentType, expectedType, new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); JsonElement element = gson.toJsonTree(val, val.getClass()); ((JsonObject) finalvalueKvProto).add(String.valueOf(k), element); @@ -878,6 +852,9 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler valueKvProto = this.converter.convertValue(resourceValue.getValue(), currentType, expectedType, new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))); } + LwM2mOtaConvert lwM2mOtaConvert = convertOtaUpdateValueToString(pathIdVer, valueKvProto, currentType); + valueKvProto = lwM2mOtaConvert.getValue(); + currentType = lwM2mOtaConvert.getCurrentType(); return valueKvProto != null ? this.helper.getKvAttrTelemetryToThingsboard(currentType, resourceName, valueKvProto, resourceValue.isMultiInstances()) : null; } } catch (Exception e) { @@ -1023,7 +1000,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler if (sendAttrToThingsboard.getPathPostParametersAdd().size() > 0) { // update value in Resources clients.forEach(client -> { - this.readObserveFromProfile(client.getRegistration(), sendAttrToThingsboard.getPathPostParametersAdd(), READ); + this.readObserveFromProfile(client, sendAttrToThingsboard.getPathPostParametersAdd(), READ); }); } // #4.2 del @@ -1050,12 +1027,12 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler clients.forEach(client -> { Registration registration = client.getRegistration(); if (postObserveAnalyzer.getPathPostParametersAdd().size() > 0) { - this.readObserveFromProfile(registration, postObserveAnalyzer.getPathPostParametersAdd(), OBSERVE); + this.readObserveFromProfile(client, postObserveAnalyzer.getPathPostParametersAdd(), OBSERVE); } // 5.3 del // send Request cancel observe to Client if (postObserveAnalyzer.getPathPostParametersDel().size() > 0) { - this.cancelObserveFromProfile(registration, postObserveAnalyzer.getPathPostParametersDel()); + this.cancelObserveFromProfile(client, postObserveAnalyzer.getPathPostParametersDel()); } }); } @@ -1092,19 +1069,18 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler * Update Resource value after change RezAttrTelemetry in config Profile * send response Read to Client and add path to pathResAttrTelemetry in LwM2MClient.getAttrTelemetryObserveValue() * - * @param registration - Registration LwM2M Client - * @param targets - path Resources == [ "/2/0/0", "/2/0/1"] + * @param targets - path Resources == [ "/2/0/0", "/2/0/1"] */ - private void readObserveFromProfile(Registration registration, Set targets, LwM2mTypeOper typeOper) { + private void readObserveFromProfile(LwM2mClient client, Set targets, LwM2mTypeOper typeOper) { targets.forEach(target -> { LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(target)); if (pathIds.isResource()) { if (READ.equals(typeOper)) { - lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, - ContentFormat.TLV.getName(), null, this.config.getTimeout(), null); + lwM2mTransportRequest.sendAllRequest(client, target, typeOper, + null, this.config.getTimeout(), null); } else if (OBSERVE.equals(typeOper)) { - lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, - null, null, this.config.getTimeout(), null); + lwM2mTransportRequest.sendAllRequest(client, target, typeOper, + null, this.config.getTimeout(), null); } } }); @@ -1153,13 +1129,12 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler // #6.2 if (analyzerParameters.getPathPostParametersAdd().size() > 0) { clients.forEach(client -> { - Registration registration = client.getRegistration(); - Set clientObjects = clientContext.getSupportedIdVerInClient(registration); + Set clientObjects = clientContext.getSupportedIdVerInClient(client); Set pathSend = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) .collect(Collectors.toUnmodifiableSet()); if (!pathSend.isEmpty()) { ConcurrentHashMap finalParams = lwm2mAttributesNew; - pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, WRITE_ATTRIBUTES, ContentFormat.TEXT.getName(), + pathSend.forEach(target -> lwM2mTransportRequest.sendAllRequest(client, target, WRITE_ATTRIBUTES, finalParams.get(target), this.config.getTimeout(), null)); } }); @@ -1168,7 +1143,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler if (analyzerParameters.getPathPostParametersDel().size() > 0) { clients.forEach(client -> { Registration registration = client.getRegistration(); - Set clientObjects = clientContext.getSupportedIdVerInClient(registration); + Set clientObjects = clientContext.getSupportedIdVerInClient(client); Set pathSend = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) .collect(Collectors.toUnmodifiableSet()); if (!pathSend.isEmpty()) { @@ -1176,8 +1151,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler Map params = (Map) lwm2mAttributesOld.get(target); params.clear(); params.put(OBJECT_VERSION, ""); - lwM2mTransportRequest.sendAllRequest(registration, target, WRITE_ATTRIBUTES, ContentFormat.TEXT.getName(), - params, this.config.getTimeout(), null); + lwM2mTransportRequest.sendAllRequest(client, target, WRITE_ATTRIBUTES, params, this.config.getTimeout(), null); }); } }); @@ -1185,12 +1159,10 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } - private void cancelObserveFromProfile(Registration registration, Set paramAnallyzer) { - LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); + private void cancelObserveFromProfile(LwM2mClient lwM2mClient, Set paramAnallyzer) { paramAnallyzer.forEach(pathIdVer -> { - if (this.getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer) != null) { - lwM2mTransportRequest.sendAllRequest(registration, pathIdVer, OBSERVE_CANCEL, null, - null, this.config.getTimeout(), null); + if (this.getResourceValueFromLwM2MClient(lwM2mClient, pathIdVer) != null) { + lwM2mTransportRequest.sendAllRequest(lwM2mClient, pathIdVer, OBSERVE_CANCEL, null, this.config.getTimeout(), null); } } ); @@ -1198,9 +1170,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler private void updateResourcesValueToClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { if (valueNew != null && (valueOld == null || !valueNew.toString().equals(valueOld.toString()))) { - lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, WRITE_REPLACE, - ContentFormat.TLV.getName(), valueNew, - this.config.getTimeout(), null); + lwM2mTransportRequest.sendAllRequest(lwM2MClient, path, WRITE_REPLACE, valueNew, this.config.getTimeout(), null); } else { log.error("Failed update resource [{}] [{}]", path, valueNew); String logMsg = String.format("%s: Failed update resource path - %s value - %s. Value is not changed or bad", @@ -1349,8 +1319,8 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } } - public void getInfoFirmwareUpdate(LwM2mClient lwM2MClient, Lwm2mClientRpcRequest rpcRequest) { - if (lwM2MClient.getRegistration().getSupportedVersion(FW_ID) != null) { + public void getInfoFirmwareUpdate(LwM2mClient lwM2MClient, LwM2mClientRpcRequest rpcRequest) { + if (lwM2MClient.getRegistration().getSupportedVersion(FW_5_ID) != null) { SessionInfoProto sessionInfo = this.getSessionInfo(lwM2MClient); if (sessionInfo != null) { DefaultLwM2MTransportMsgHandler handler = this; @@ -1360,18 +1330,33 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler public void onSuccess(TransportProtos.GetOtaPackageResponseMsg response) { if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus()) && response.getType().equals(OtaPackageType.FIRMWARE.name())) { - log.warn("7) firmware start with ver: [{}]", response.getVersion()); - lwM2MClient.getFwUpdate().setRpcRequest(rpcRequest); - lwM2MClient.getFwUpdate().setCurrentVersion(response.getVersion()); - lwM2MClient.getFwUpdate().setCurrentTitle(response.getTitle()); - lwM2MClient.getFwUpdate().setCurrentId(new OtaPackageId(new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB())).getId()); - if (rpcRequest == null) { - lwM2MClient.getFwUpdate().sendReadObserveInfo(lwM2mTransportRequest); + LwM2mFwSwUpdate fwUpdate = lwM2MClient.getFwUpdate(clientContext); + if (rpcRequest != null) { + fwUpdate.setStateUpdate(INITIATED.name()); + } + if (!FAILED.name().equals(fwUpdate.getStateUpdate())) { + log.warn("7) firmware start with ver: [{}]", response.getVersion()); + fwUpdate.setRpcRequest(rpcRequest); + fwUpdate.setCurrentVersion(response.getVersion()); + fwUpdate.setCurrentTitle(response.getTitle()); + fwUpdate.setCurrentId(new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB())); + if (rpcRequest == null) { + fwUpdate.sendReadObserveInfo(lwM2mTransportRequest); + } else { + fwUpdate.writeFwSwWare(handler, lwM2mTransportRequest); + } } else { - lwM2MClient.getFwUpdate().writeFwSwWare(handler, lwM2mTransportRequest); + String msgError = String.format("OtaPackage device: %s, version: %s, stateUpdate: %s", + lwM2MClient.getDeviceName(), response.getVersion(), fwUpdate.getStateUpdate()); + log.warn("7_1 [{}]", msgError); } } else { - log.trace("OtaPackage [{}] [{}]", lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); + String msgError = String.format("OtaPackage device: %s, responseStatus: %s", + lwM2MClient.getDeviceName(), response.getResponseStatus().toString()); + log.trace(msgError); + if (rpcRequest != null) { + sendErrorRpcResponse(rpcRequest, msgError, sessionInfo); + } } } @@ -1384,7 +1369,7 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler } } - public void getInfoSoftwareUpdate(LwM2mClient lwM2MClient, Lwm2mClientRpcRequest rpcRequest) { + public void getInfoSoftwareUpdate(LwM2mClient lwM2MClient, LwM2mClientRpcRequest rpcRequest) { if (lwM2MClient.getRegistration().getSupportedVersion(SW_ID) != null) { SessionInfoProto sessionInfo = this.getSessionInfo(lwM2MClient); if (sessionInfo != null) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 865168aed6..9be164023d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -28,7 +28,6 @@ import org.eclipse.leshan.server.californium.registration.CaliforniumRegistratio import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.security.EditableSecurityStore; import org.springframework.stereotype.Component; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; @@ -60,14 +59,13 @@ import java.security.spec.InvalidParameterSpecException; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CCM_8; import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FIRMWARE_UPDATE_COAP_RECOURSE; @Slf4j @Component @@ -83,7 +81,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final LwM2mTransportServerHelper helper; - private final LwM2mTransportMsgHandler handler; + private final DefaultLwM2MTransportMsgHandler handler; private final CaliforniumRegistrationStore registrationStore; private final TbSecurityStore securityStore; private final LwM2mClientContext lwM2mClientContext; @@ -98,6 +96,17 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { new LWM2MGenerationPSkRPkECC(); } this.server = getLhServer(); + /** + * Add a resource to the server. + * CoapResource -> + * path = FW_PACKAGE or SW_PACKAGE + * nameFile = "BC68JAR01A09_TO_BC68JAR01A10.bin" + * "coap://host:port/{path}/{token}/{nameFile}" + */ + + + LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(handler, FIRMWARE_UPDATE_COAP_RECOURSE); + this.server.coap().getServer().add(otaCoapResource); this.startLhServer(); this.context.setServer(server); } @@ -128,7 +137,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { builder.setEncoder(new DefaultLwM2mNodeEncoder(LwM2mValueConverterImpl.getInstance())); /* Create CoAP Config */ - builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort())); + builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); /* Define model provider (Create Models )*/ LwM2mModelProvider modelProvider = new LwM2mVersionedModelProvider(this.lwM2mClientContext, this.helper, this.context); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java index 280cb7dde4..00f70dbcf7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java @@ -16,10 +16,14 @@ package org.thingsboard.server.transport.lwm2m.server; import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.network.config.NetworkConfigDefaults; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; + +import static org.eclipse.californium.core.network.config.NetworkConfigDefaults.DEFAULT_BLOCKWISE_STATUS_LIFETIME; public class LwM2mNetworkConfig { - public static NetworkConfig getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort) { + public static NetworkConfig getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) { NetworkConfig coapConfig = new NetworkConfig(); coapConfig.setInt(NetworkConfig.Keys.COAP_PORT,serverPortNoSec); coapConfig.setInt(NetworkConfig.Keys.COAP_SECURE_PORT,serverSecurePort); @@ -49,8 +53,15 @@ public class LwM2mNetworkConfig { - value of true indicate that the server will response with block2 option event if no further blocks are required. */ coapConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true); - - coapConfig.setInt(NetworkConfig.Keys.BLOCKWISE_STATUS_LIFETIME, 300000); + /** + * The maximum amount of time (in milliseconds) allowed between + * transfers of individual blocks in a blockwise transfer before the + * blockwise transfer state is discarded. + *

+ * The default value of this property is + * {@link NetworkConfigDefaults#DEFAULT_BLOCKWISE_STATUS_LIFETIME} = 5 * 60 * 1000; // 5 mins [ms]. + */ + coapConfig.setLong(NetworkConfig.Keys.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME); /** !!! REQUEST_ENTITY_TOO_LARGE CODE=4.13 The maximum size of a resource body (in bytes) that will be accepted diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mOtaConvert.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mOtaConvert.java new file mode 100644 index 0000000000..7b7c56adb3 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mOtaConvert.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.Data; +import org.eclipse.leshan.core.model.ResourceModel; + +@Data +public class LwM2mOtaConvert { + private ResourceModel.Type currentType; + private Object value; +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java index f1839e718a..b69825843e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -87,7 +87,7 @@ public class LwM2mServerListener { @Override public void cancelled(Observation observation) { String msg = String.format("%s: Canceled Observation %s.", LOG_LW2M_INFO, observation.getPath()); - service.sendLogsToThingsboard2(observation.getRegistrationId(), msg); + service.sendLogsToThingsboard(observation.getRegistrationId(), msg); log.warn(msg); } @@ -109,7 +109,7 @@ public class LwM2mServerListener { String msg = String.format("%s: Successful start newObservation %s.", LOG_LW2M_INFO, observation.getPath()); log.warn(msg); - service.sendLogsToThingsboard2(registration.getId(), msg); + service.sendLogsToThingsboard(registration.getId(), msg); } }; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java new file mode 100644 index 0000000000..1236c8b6d2 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.network.Exchange; +import org.eclipse.californium.core.observe.ObserveRelation; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.Resource; +import org.eclipse.californium.core.server.resources.ResourceObserver; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FIRMWARE_UPDATE_COAP_RECOURSE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SOFTWARE_UPDATE_COAP_RECOURSE; + +@Slf4j +public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource { + private final ConcurrentMap tokenToObserveRelationMap = new ConcurrentHashMap<>(); + private final ConcurrentMap tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>(); + private final LwM2mTransportMsgHandler handler; + + public LwM2mTransportCoapResource(LwM2mTransportMsgHandler handler, String name) { + super(name); + this.handler = handler; + this.setObservable(true); // enable observing + this.addObserver(new CoapResourceObserver()); + } + + + @Override + public void checkObserveRelation(Exchange exchange, Response response) { + String token = getTokenFromRequest(exchange.getRequest()); + final ObserveRelation relation = exchange.getRelation(); + if (relation == null || relation.isCanceled()) { + return; // because request did not try to establish a relation + } + if (CoAP.ResponseCode.isSuccess(response.getCode())) { + + if (!relation.isEstablished()) { + relation.setEstablished(); + addObserveRelation(relation); + } + AtomicInteger notificationCounter = tokenToObserveNotificationSeqMap.computeIfAbsent(token, s -> new AtomicInteger(0)); + response.getOptions().setObserve(notificationCounter.getAndIncrement()); + } // ObserveLayer takes care of the else case + } + + + @Override + protected void processHandleGet(CoapExchange exchange) { + log.warn("90) processHandleGet [{}]", exchange); + if (exchange.getRequestOptions().getUriPath().size() >= 2 && + (FIRMWARE_UPDATE_COAP_RECOURSE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-2)) || + SOFTWARE_UPDATE_COAP_RECOURSE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-2)))) { + this.sendOtaData(exchange); + } + } + + @Override + protected void processHandlePost(CoapExchange exchange) { + log.warn("2) processHandleGet [{}]", exchange); + } + + /** + * Override the default behavior so that requests to sub resources (typically /{name}/{token}) are handled by + * /name resource. + */ + @Override + public Resource getChild(String name) { + return this; + } + + + private String getTokenFromRequest(Request request) { + return (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getAddress().getHostAddress() : "null") + + ":" + (request.getSourceContext() != null ? request.getSourceContext().getPeerAddress().getPort() : -1) + ":" + request.getTokenString(); + } + + public class CoapResourceObserver implements ResourceObserver { + + @Override + public void changedName(String old) { + + } + + @Override + public void changedPath(String old) { + + } + + @Override + public void addedChild(Resource child) { + + } + + @Override + public void removedChild(Resource child) { + + } + + @Override + public void addedObserveRelation(ObserveRelation relation) { + + } + + @Override + public void removedObserveRelation(ObserveRelation relation) { + + } + } + + private void sendOtaData(CoapExchange exchange) { + String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-1 + ); + UUID currentId = UUID.fromString(idStr); + Response response = new Response(CoAP.ResponseCode.CONTENT); + byte[] fwData = this.getOtaData(currentId); + log.warn("91) read softWare data (length): [{}]", fwData.length); + if (fwData != null && fwData.length > 0) { + response.setPayload(fwData); + if (exchange.getRequestOptions().getBlock2() != null) { + int chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); + boolean moreFlag = fwData.length > chunkSize; + response.getOptions().setBlock2(chunkSize, moreFlag, 0); + log.warn("92) with blokc2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), fwData.length, chunkSize, moreFlag); + } + else { + log.warn("92) with block1 Send currentId: [{}], length: [{}], ", currentId.toString(), fwData.length); + } + exchange.respond(response); + } + } + + private byte[] getOtaData(UUID currentId) { + return ((DefaultLwM2MTransportMsgHandler) handler).otaPackageDataCache.get(currentId.toString()); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java index 36f1209ff3..ee3b07da34 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportMsgHandler.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; -import org.thingsboard.server.transport.lwm2m.server.client.Lwm2mClientRpcRequest; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientRpcRequest; import java.util.Collection; import java.util.Optional; @@ -40,7 +40,7 @@ public interface LwM2mTransportMsgHandler { void setCancelObservationsAll(Registration registration); - void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, Lwm2mClientRpcRequest rpcRequest); + void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response, LwM2mClientRpcRequest rpcRequest); void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo); @@ -58,15 +58,13 @@ public interface LwM2mTransportMsgHandler { void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse); - void doTrigger(Registration registration, String path); - void doDisconnect(TransportProtos.SessionInfoProto sessionInfo); void onAwakeDev(Registration registration); void sendLogsToThingsboard(LwM2mClient client, String msg); - void sendLogsToThingsboard2(String registrationId, String msg); + void sendLogsToThingsboard(String registrationId, String msg); LwM2MTransportServerConfig getConfig(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java index d382b0b734..3568000ead 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -55,7 +55,7 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.client.Lwm2mClientRpcRequest; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientRpcRequest; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PostConstruct; @@ -75,7 +75,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWN import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getContentFormatByResourceModelType; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.DEFAULT_TIMEOUT; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_5_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; @@ -116,21 +116,19 @@ public class LwM2mTransportRequest { new NamedThreadFactory(String.format("LwM2M %s channel response after request", RESPONSE_REQUEST_CHANNEL))); } - /** - * Device management and service enablement, including Read, Write, Execute, Discover, Create, Delete and Write-Attributes - * - * @param registration - - * @param targetIdVer - - * @param typeOper - - * @param contentFormatName - - */ + public void sendAllRequest(LwM2mClient lwM2MClient, String targetIdVer, LwM2mTypeOper typeOper, Object params, long timeoutInMs, LwM2mClientRpcRequest lwm2mClientRpcRequest) { + sendAllRequest(lwM2MClient, targetIdVer, typeOper, lwM2MClient.getDefaultContentFormat(), params, timeoutInMs, lwm2mClientRpcRequest); + } - public void sendAllRequest(Registration registration, String targetIdVer, LwM2mTypeOper typeOper, - String contentFormatName, Object params, long timeoutInMs, Lwm2mClientRpcRequest lwm2mClientRpcRequest) { - LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()); + + public void sendAllRequest(LwM2mClient lwM2MClient, String targetIdVer, LwM2mTypeOper typeOper, + ContentFormat contentFormat, Object params, long timeoutInMs, LwM2mClientRpcRequest lwm2mClientRpcRequest) { + Registration registration = lwM2MClient.getRegistration(); try { String target = convertPathFromIdVerToObjectId(targetIdVer); - ContentFormat contentFormat = contentFormatName != null ? ContentFormat.fromName(contentFormatName.toUpperCase()) : ContentFormat.DEFAULT; + if(contentFormat == null){ + contentFormat = ContentFormat.DEFAULT; + } LwM2mPath resultIds = target != null ? new LwM2mPath(target) : null; if (!OBSERVE_CANCEL.name().equals(typeOper.name()) && resultIds != null && registration != null && resultIds.getObjectId() >= 0 && lwM2MClient != null) { if (lwM2MClient.isValidObjectVersion(targetIdVer)) { @@ -143,7 +141,7 @@ public class LwM2mTransportRequest { } catch (ClientSleepingException e) { SimpleDownlinkRequest finalRequest = request; long finalTimeoutInMs = timeoutInMs; - Lwm2mClientRpcRequest finalRpcRequest = lwm2mClientRpcRequest; + LwM2mClientRpcRequest finalRpcRequest = lwm2mClientRpcRequest; lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs, finalRpcRequest)); } catch (Exception e) { log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper.name(), e); @@ -163,12 +161,12 @@ public class LwM2mTransportRequest { if (lwm2mClientRpcRequest != null) { ResourceModel resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()); String errorMsg = resourceModel == null ? String.format("Path %s not found in object version", targetIdVer) : "SendRequest - null"; - this.handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } } else if (lwm2mClientRpcRequest != null) { String errorMsg = String.format("Path %s not found in object version", targetIdVer); - this.handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); + handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR); } } else { switch (typeOper) { @@ -188,7 +186,7 @@ public class LwM2mTransportRequest { this.handler.sendLogsToThingsboard(lwM2MClient, msg); if (lwm2mClientRpcRequest != null) { String valueMsg = String.format("Paths - %s", paths); - this.handler.sentRpcResponse(lwm2mClientRpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); + handler.sentRpcResponse(lwm2mClientRpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE); } break; case OBSERVE_CANCEL: @@ -208,7 +206,7 @@ public class LwM2mTransportRequest { break; // lwm2mClientRpcRequest != null case FW_UPDATE: - this.handler.getInfoFirmwareUpdate(lwM2MClient, lwm2mClientRpcRequest); + handler.getInfoFirmwareUpdate(lwM2MClient, lwm2mClientRpcRequest); break; } } @@ -225,7 +223,7 @@ public class LwM2mTransportRequest { private SimpleDownlinkRequest createRequest(Registration registration, LwM2mClient lwM2MClient, LwM2mTypeOper typeOper, ContentFormat contentFormat, String target, String targetIdVer, - LwM2mPath resultIds, Object params, Lwm2mClientRpcRequest rpcRequest) { + LwM2mPath resultIds, Object params, LwM2mClientRpcRequest rpcRequest) { SimpleDownlinkRequest request = null; switch (typeOper) { case READ: @@ -330,7 +328,7 @@ public class LwM2mTransportRequest { @SuppressWarnings({"error sendRequest"}) private void sendRequest(Registration registration, LwM2mClient lwM2MClient, SimpleDownlinkRequest request, - long timeoutInMs, Lwm2mClientRpcRequest rpcRequest) { + long timeoutInMs, LwM2mClientRpcRequest rpcRequest) { context.getServer().send(registration, request, timeoutInMs, (ResponseCallback) response -> { if (!lwM2MClient.isInit()) { @@ -354,13 +352,13 @@ public class LwM2mTransportRequest { /** Not Found set setClient_fw_info... = empty **/ - if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getFwUpdate() != null && lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); } - if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getSwUpdate() != null && lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); } - if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteFwSWUpdateError(registration, request, response.getErrorMessage()); } if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) { @@ -371,13 +369,13 @@ public class LwM2mTransportRequest { /** version == null set setClient_fw_info... = empty **/ - if (lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getFwUpdate() != null && lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString()); } - if (lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { + if (lwM2MClient.getSwUpdate() != null && lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) { lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString()); } - if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteFwSWUpdateError(registration, request, e.getMessage()); } if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) { @@ -398,7 +396,7 @@ public class LwM2mTransportRequest { private WriteRequest getWriteRequestSingleResource(ContentFormat contentFormat, Integer objectId, Integer instanceId, Integer resourceId, Object value, ResourceModel.Type type, - LwM2mClient client, Lwm2mClientRpcRequest rpcRequest) { + LwM2mClient client, LwM2mClientRpcRequest rpcRequest) { try { if (type != null) { switch (type) { @@ -444,7 +442,7 @@ public class LwM2mTransportRequest { } private void handleResponse(LwM2mClient lwM2mClient, final String path, LwM2mResponse response, - SimpleDownlinkRequest request, Lwm2mClientRpcRequest rpcRequest) { + SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) { responseRequestExecutor.submit(() -> { try { this.sendResponse(lwM2mClient, path, response, request, rpcRequest); @@ -457,12 +455,11 @@ public class LwM2mTransportRequest { /** * processing a response from a client * - * @param registration - * @param path - * @param response - */ private void sendResponse(LwM2mClient lwM2mClient, String path, LwM2mResponse response, - SimpleDownlinkRequest request, Lwm2mClientRpcRequest rpcRequest) { + SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) { Registration registration = lwM2mClient.getRegistration(); String pathIdVer = convertPathFromObjectIdToIdVer(path, registration); String msgLog = ""; @@ -510,7 +507,7 @@ public class LwM2mTransportRequest { } } - private void infoWriteResponse(LwM2mClient lwM2mClient, LwM2mResponse response, SimpleDownlinkRequest request, Lwm2mClientRpcRequest rpcRequest) { + private void infoWriteResponse(LwM2mClient lwM2mClient, LwM2mResponse response, SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) { try { Registration registration = lwM2mClient.getRegistration(); LwM2mNode node = ((WriteRequest) request).getNode(); @@ -548,7 +545,7 @@ public class LwM2mTransportRequest { } if (msg != null) { handler.sendLogsToThingsboard(lwM2mClient, msg); - if (request.getPath().toString().equals(FW_PACKAGE_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { + if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) { this.afterWriteSuccessFwSwUpdate(registration, request); if (rpcRequest != null) { rpcRequest.setInfoMsg(msg); @@ -570,7 +567,7 @@ public class LwM2mTransportRequest { */ private void afterWriteSuccessFwSwUpdate(Registration registration, SimpleDownlinkRequest request) { LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); - if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { + if (request.getPath().toString().equals(FW_PACKAGE_5_ID) && lwM2MClient.getFwUpdate() != null) { lwM2MClient.getFwUpdate().setStateUpdate(DOWNLOADED.name()); lwM2MClient.getFwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); } @@ -585,7 +582,7 @@ public class LwM2mTransportRequest { */ private void afterWriteFwSWUpdateError(Registration registration, SimpleDownlinkRequest request, String msgError) { LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId()); - if (request.getPath().toString().equals(FW_PACKAGE_ID) && lwM2MClient.getFwUpdate() != null) { + if (request.getPath().toString().equals(FW_PACKAGE_5_ID) && lwM2MClient.getFwUpdate() != null) { lwM2MClient.getFwUpdate().setStateUpdate(FAILED.name()); lwM2MClient.getFwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); } @@ -605,7 +602,7 @@ public class LwM2mTransportRequest { } } - private void afterObserveCancel(LwM2mClient lwM2mClient, int observeCancelCnt, String observeCancelMsg, Lwm2mClientRpcRequest rpcRequest) { + private void afterObserveCancel(LwM2mClient lwM2mClient, int observeCancelCnt, String observeCancelMsg, LwM2mClientRpcRequest rpcRequest) { handler.sendLogsToThingsboard(lwM2mClient, observeCancelMsg); log.warn("[{}]", observeCancelMsg); if (rpcRequest != null) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index 4334c8ac0f..a52fc0158d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -65,13 +65,11 @@ import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType. public class LwM2mTransportServerHelper { private final LwM2mTransportContext context; - private final LwM2MJsonAdaptor adaptor; private final AtomicInteger atomicTs = new AtomicInteger(0); public long getTS() { - int addTs = atomicTs.getAndIncrement() >= 1000 ? atomicTs.getAndSet(0) : atomicTs.get(); - return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) * 1000L + addTs; + return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) * 1000L + (atomicTs.getAndIncrement() % 1000); } /** diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java index d420176ffa..1d1feab85e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java @@ -43,11 +43,11 @@ import org.eclipse.leshan.server.registration.Registration; import org.nustaq.serialization.FSTConfiguration; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageKey; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; import org.thingsboard.server.common.data.ota.OtaPackageUtil; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; @@ -77,18 +77,23 @@ import static org.eclipse.leshan.core.model.ResourceModel.Type.OBJLNK; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; import static org.eclipse.leshan.core.model.ResourceModel.Type.STRING; import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; +import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; -import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @Slf4j public class LwM2mTransportUtil { + public static final String EVENT_AWAKE = "AWAKE"; + public static final String RESPONSE_REQUEST_CHANNEL = "RESP_REQ"; + public static final String RESPONSE_CHANNEL = "RESP"; + public static final String OBSERVE_CHANNEL = "OBSERVE"; + public static final String TRANSPORT_DEFAULT_LWM2M_VERSION = "1.0"; public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; public static final String BOOTSTRAP = "bootstrap"; @@ -116,9 +121,6 @@ public class LwM2mTransportUtil { public static final String LOG_LW2M_WARN = "warn"; public static final String LOG_LW2M_VALUE = "value"; - public static final int LWM2M_STRATEGY_1 = 1; - public static final int LWM2M_STRATEGY_2 = 2; - public static final String CLIENT_NOT_AUTHORIZED = "Client not authorized"; public static final String LWM2M_VERSION_DEFAULT = "1.0"; @@ -132,18 +134,23 @@ public class LwM2mTransportUtil { public static final String FINISH_VALUE_KEY = ","; public static final String START_JSON_KEY = "{"; public static final String FINISH_JSON_KEY = "}"; - // public static final String contentFormatNameKey = "contentFormatName"; public static final String INFO_KEY = "info"; - // public static final String TIME_OUT_IN_MS = "timeOutInMs"; public static final String RESULT_KEY = "result"; public static final String ERROR_KEY = "error"; public static final String METHOD_KEY = "methodName"; + // Firmware + public static final String FIRMWARE_UPDATE_COAP_RECOURSE = "firmwareUpdateCoapRecourse"; public static final String FW_UPDATE = "Firmware update"; - public static final Integer FW_ID = 5; + public static final Integer FW_5_ID = 5; + public static final Integer FW_19_ID = 19; + // Package W - public static final String FW_PACKAGE_ID = "/5/0/0"; + public static final String FW_PACKAGE_5_ID = "/5/0/0"; + public static final String FW_PACKAGE_19_ID = "/19/0/0"; + // Package URI + public static final String FW_PACKAGE_URI_ID = "/5/0/1"; // State R public static final String FW_STATE_ID = "/5/0/3"; // Update Result R @@ -151,15 +158,28 @@ public class LwM2mTransportUtil { // PkgName R public static final String FW_NAME_ID = "/5/0/6"; // PkgVersion R - public static final String FW_VER_ID = "/5/0/7"; + public static final String FW_5_VER_ID = "/5/0/7"; + /** + * Quectel@Hi15RM1-HLB_V1.0@BC68JAR01A10,V150R100C20B300SP7,V150R100C20B300SP7@8 + * BC68JAR01A10 + * # Request prodct type number + * ATI + * Quectel + * BC68 + * Revision:BC68JAR01A10 + */ + public static final String FW_3_VER_ID = "/3/0/3"; // Update E public static final String FW_UPDATE_ID = "/5/0/2"; // Software + public static final String SOFTWARE_UPDATE_COAP_RECOURSE = "softwareUpdateCoapRecourse"; public static final String SW_UPDATE = "Software update"; public static final Integer SW_ID = 9; // Package W public static final String SW_PACKAGE_ID = "/9/0/2"; + // Package URI + public static final String SW_PACKAGE_URI_ID = "/9/0/3"; // Update State R public static final String SW_UPDATE_STATE_ID = "/9/0/7"; // Update Result R @@ -232,7 +252,7 @@ public class LwM2mTransportUtil { DELETE(11, "Delete"), // only for RPC - FW_UPDATE(12,"FirmwareUpdate"); + FW_UPDATE(12, "FirmwareUpdate"); // FW_READ_INFO(12, "FirmwareReadInfo"), // SW_READ_INFO(15, "SoftwareReadInfo"), @@ -355,20 +375,11 @@ public class LwM2mTransportUtil { * FirmwareUpdateStatus { * DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED */ - public static OtaPackageUpdateStatus EqualsFwSateToFirmwareUpdateStatus(StateFw stateFw, UpdateResultFw updateResultFw) { + public static OtaPackageUpdateStatus equalsFwSateFwResultToFirmwareUpdateStatus(StateFw stateFw, UpdateResultFw updateResultFw) { switch (updateResultFw) { case INITIAL: - switch (stateFw) { - case IDLE: - return VERIFIED; - case DOWNLOADING: - return DOWNLOADING; - case DOWNLOADED: - return DOWNLOADED; - case UPDATING: - return UPDATING; - } - case UPDATE_SUCCESSFULLY: + return equalsFwSateToFirmwareUpdateStatus(stateFw); + case UPDATE_SUCCESSFULLY: return UPDATED; case NOT_ENOUGH: case OUT_OFF_MEMORY: @@ -384,6 +395,41 @@ public class LwM2mTransportUtil { } } + public static OtaPackageUpdateStatus equalsFwResultToFirmwareUpdateStatus(UpdateResultFw updateResultFw) { + switch (updateResultFw) { + case INITIAL: + return VERIFIED; + case UPDATE_SUCCESSFULLY: + return UPDATED; + case NOT_ENOUGH: + case OUT_OFF_MEMORY: + case CONNECTION_LOST: + case INTEGRITY_CHECK_FAILURE: + case UNSUPPORTED_TYPE: + case INVALID_URI: + case UPDATE_FAILED: + case UNSUPPORTED_PROTOCOL: + return FAILED; + default: + throw new CodecException("Invalid value stateFw %s for FirmwareUpdateStatus.", updateResultFw.name()); + } + } + + public static OtaPackageUpdateStatus equalsFwSateToFirmwareUpdateStatus(StateFw stateFw) { + switch (stateFw) { + case IDLE: + return VERIFIED; + case DOWNLOADING: + return DOWNLOADING; + case DOWNLOADED: + return DOWNLOADED; + case UPDATING: + return UPDATING; + default: + throw new CodecException("Invalid value stateFw %d for FirmwareUpdateStatus.", stateFw); + } + } + /** * SW Update State R * 0: INITIAL Before downloading. (see 5.1.2.1) @@ -497,6 +543,101 @@ public class LwM2mTransportUtil { } } + public enum LwM2MClientStrategy { + CLIENT_STRATEGY_1(1, "Read only resources marked as observation"), + CLIENT_STRATEGY_2(2, "Read all client resources"); + + public int code; + public String type; + + LwM2MClientStrategy(int code, String type) { + this.code = code; + this.type = type; + } + + public static LwM2MClientStrategy fromStrategyClientByType(String type) { + for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client Strategy type : %s", type)); + } + + public static LwM2MClientStrategy fromStrategyClientByCode(int code) { + for (LwM2MClientStrategy to : LwM2MClientStrategy.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported Client Strategy code : %s", code)); + } + } + + public enum LwM2MFirmwareUpdateStrategy { + OBJ_5_BINARY(1, "ObjectId 5, Binary"), + OBJ_5_TEMP_URL(2, "ObjectId 5, URI"), + OBJ_19_BINARY(3, "ObjectId 19, Binary"); + + public int code; + public String type; + + LwM2MFirmwareUpdateStrategy(int code, String type) { + this.code = code; + this.type = type; + } + + public static LwM2MFirmwareUpdateStrategy fromStrategyFwByType(String type) { + for (LwM2MFirmwareUpdateStrategy to : LwM2MFirmwareUpdateStrategy.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW State type : %s", type)); + } + + public static LwM2MFirmwareUpdateStrategy fromStrategyFwByCode(int code) { + for (LwM2MFirmwareUpdateStrategy to : LwM2MFirmwareUpdateStrategy.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported FW Strategy code : %s", code)); + } + } + + public enum LwM2MSoftwareUpdateStrategy { + BINARY(1, "ObjectId 9, Binary"), + TEMP_URL(2, "ObjectId 9, URI"); + + public int code; + public String type; + + LwM2MSoftwareUpdateStrategy(int code, String type) { + this.code = code; + this.type = type; + } + + public static LwM2MSoftwareUpdateStrategy fromStrategySwByType(String type) { + for (LwM2MSoftwareUpdateStrategy to : LwM2MSoftwareUpdateStrategy.values()) { + if (to.type.equals(type)) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW Strategy type : %s", type)); + } + + public static LwM2MSoftwareUpdateStrategy fromStrategySwByCode(int code) { + for (LwM2MSoftwareUpdateStrategy to : LwM2MSoftwareUpdateStrategy.values()) { + if (to.code == code) { + return to; + } + } + throw new IllegalArgumentException(String.format("Unsupported SW Strategy code : %s", code)); + } + + } + /** * FirmwareUpdateStatus { * DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED @@ -534,10 +675,6 @@ public class LwM2mTransportUtil { } } - public static final String EVENT_AWAKE = "AWAKE"; - public static final String RESPONSE_REQUEST_CHANNEL = "RESP_REQ"; - public static final String RESPONSE_CHANNEL = "RESP"; - public static final String OBSERVE_CHANNEL = "OBSERVE"; public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath resourcePath) throws CodecException { @@ -558,6 +695,36 @@ public class LwM2mTransportUtil { } } + public static LwM2mOtaConvert convertOtaUpdateValueToString (String pathIdVer, Object value, ResourceModel.Type currentType) { + String path = convertPathFromIdVerToObjectId(pathIdVer); + LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert(); + if (path != null) { + if (FW_STATE_ID.equals(path)) { + lwM2mOtaConvert.setCurrentType(STRING); + lwM2mOtaConvert.setValue(StateFw.fromStateFwByCode(((Long) value).intValue()).type); + return lwM2mOtaConvert; + } + else if (FW_RESULT_ID.equals(path)) { + lwM2mOtaConvert.setCurrentType(STRING); + lwM2mOtaConvert.setValue(UpdateResultFw.fromUpdateResultFwByCode(((Long) value).intValue()).type); + return lwM2mOtaConvert; + } + else if (SW_UPDATE_STATE_ID.equals(path)) { + lwM2mOtaConvert.setCurrentType(STRING); + lwM2mOtaConvert.setValue(UpdateStateSw.fromUpdateStateSwByCode(((Long) value).intValue()).type); + return lwM2mOtaConvert; + } + else if (SW_RESULT_ID.equals(path)) { + lwM2mOtaConvert.setCurrentType(STRING); + lwM2mOtaConvert.setValue(UpdateResultSw.fromUpdateResultSwByCode(((Long) value).intValue()).type); + return lwM2mOtaConvert; + } + } + lwM2mOtaConvert.setCurrentType(currentType); + lwM2mOtaConvert.setValue(value); + return lwM2mOtaConvert; + } + public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { if (content instanceof LwM2mObject) { return (LwM2mObject) content; @@ -633,11 +800,6 @@ public class LwM2mTransportUtil { return null; } - public static int getClientOnlyObserveAfterConnect(LwM2mClientProfile profile) { - return profile.getPostClientLwM2mSettings().getAsJsonObject().has("clientOnlyObserveAfterConnect") ? - profile.getPostClientLwM2mSettings().getAsJsonObject().get("clientOnlyObserveAfterConnect").getAsInt() : 1; - } - private static boolean getValidateCredentialsBodyFromThingsboard(JsonObject objectMsg) { return (objectMsg != null && !objectMsg.isJsonNull() && @@ -914,7 +1076,7 @@ public class LwM2mTransportUtil { case GREATER_THAN: case LESSER_THAN: case STEP: - if (value.getClass().getSimpleName().equals("String") ) { + if (value.getClass().getSimpleName().equals("String")) { value = Double.valueOf((String) value); } return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); @@ -928,11 +1090,11 @@ public class LwM2mTransportUtil { return new Attribute(key, attrValue); } catch (Exception e) { log.error("CreateAttribute, not valid parameter key: [{}], attrValue: [{}], error: [{}]", key, attrValue, e.getMessage()); - return null; + return null; } } - public static boolean isFwSwWords (String pathName) { + public static boolean isFwSwWords(String pathName) { return OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.VERSION).equals(pathName) || OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.TITLE).equals(pathName) || OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.CHECKSUM).equals(pathName) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 1fc876cc45..78f9ffb349 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.security.SecurityInfo; @@ -69,7 +70,8 @@ public class LwM2mClient implements Cloneable { @Getter private final String endpoint; private final Lock lock; - @Getter @Setter + @Getter + @Setter private LwM2MClientState state; @Getter private final Map resources; @@ -92,8 +94,6 @@ public class LwM2mClient implements Cloneable { @Getter private UUID deviceId; @Getter - private UUID sessionId; - @Getter private SessionInfoProto session; @Getter private UUID profileId; @@ -133,8 +133,6 @@ public class LwM2mClient implements Cloneable { this.credentials = credentials; this.profileId = profileId; this.init = false; - this.fwUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.FIRMWARE); - this.swUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.SOFTWARE); if (this.credentials != null && this.credentials.hasDeviceInfo()) { this.session = createSession(nodeId, sessionId, credentials); this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB()); @@ -385,5 +383,31 @@ public class LwM2mClient implements Cloneable { } } + public ContentFormat getDefaultContentFormat() { + if (registration == null) { + return ContentFormat.DEFAULT; + } else if (registration.getLwM2mVersion().equals("1.0")) { + return ContentFormat.TLV; + } else { + return ContentFormat.TEXT; + } + } + + public LwM2mFwSwUpdate getFwUpdate (LwM2mClientContext clientContext) { + if (this.fwUpdate == null) { + LwM2mClientProfile lwM2mClientProfile = clientContext.getProfile(this.getProfileId()); + this.fwUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.FIRMWARE, lwM2mClientProfile.getFwUpdateStrategy()); + } + return this.fwUpdate; + } + + public LwM2mFwSwUpdate getSwUpdate (LwM2mClientContext clientContext) { + if (this.swUpdate == null) { + LwM2mClientProfile lwM2mClientProfile = clientContext.getProfile(this.getProfileId()); + this.swUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.SOFTWARE, lwM2mClientProfile.getSwUpdateStrategy()); + } + return this.fwUpdate; + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index e2e24ad1ff..613a8b7971 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.server.client; import org.eclipse.leshan.server.registration.Registration; -import org.eclipse.leshan.server.security.SecurityInfo; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; @@ -40,15 +39,6 @@ public interface LwM2mClientContext { void unregister(LwM2mClient client, Registration registration) throws LwM2MClientStateException; - -// LwM2mClient getOrRegister(Registration registration); - -// LwM2mClient registerOrUpdate(Registration registration); - -// LwM2mClient fetchClientByEndpoint(String endpoint); - -// Registration getRegistration(String registrationId); - Collection getLwM2mClients(); Map getProfiles(); @@ -61,7 +51,7 @@ public interface LwM2mClientContext { LwM2mClientProfile profileUpdate(DeviceProfile deviceProfile); - Set getSupportedIdVerInClient(Registration registration); + Set getSupportedIdVerInClient(LwM2mClient registration); LwM2mClient getClientByDeviceId(UUID deviceId); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index aa204ae868..02d3425ede 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -36,6 +36,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import static org.eclipse.leshan.core.SecurityMode.NO_SEC; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; @@ -96,12 +97,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { if (!LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) { throw new LwM2MClientStateException(lwM2MClient.getState(), "Client is in invalid state."); } - Registration currentRegistration = lwM2MClient.getRegistration(); - if (currentRegistration.getId().equals(registration.getId())) { - lwM2MClient.setRegistration(registration); - } else { - throw new LwM2MClientStateException(lwM2MClient.getState(), "Client has different registration."); - } + lwM2MClient.setRegistration(registration); } finally { lwM2MClient.unlock(); } @@ -120,7 +116,6 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { lwM2MClient.setState(LwM2MClientState.UNREGISTERED); lwM2mClientsByEndpoint.remove(lwM2MClient.getEndpoint()); this.securityStore.remove(lwM2MClient.getEndpoint()); - this.lwM2mClientsByRegistrationId.remove(registration.getId()); UUID profileId = lwM2MClient.getProfileId(); if (profileId != null) { Optional otherClients = lwM2mClientsByRegistrationId.values().stream().filter(e -> e.getProfileId().equals(profileId)).findFirst(); @@ -143,14 +138,19 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public LwM2mClient getClientBySessionInfo(TransportProtos.SessionInfoProto sessionInfo) { - LwM2mClient lwM2mClient = lwM2mClientsByEndpoint.values().stream().filter(c -> + LwM2mClient lwM2mClient = null; + Predicate isClientFilter = c -> (new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())) - .equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB()))) - - ).findAny().orElse(null); + .equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB()))); + if (this.lwM2mClientsByEndpoint.size() > 0) { + lwM2mClient = this.lwM2mClientsByEndpoint.values().stream().filter(isClientFilter).findAny().orElse(null); + } + if (lwM2mClient == null && this.lwM2mClientsByRegistrationId.size() > 0) { + lwM2mClient = this.lwM2mClientsByRegistrationId.values().stream().filter(isClientFilter).findAny().orElse(null); + } if (lwM2mClient == null) { log.warn("Device TimeOut? lwM2mClient is null."); - log.warn("SessionInfo input [{}], lwM2mClientsByEndpoint size: [{}]", sessionInfo, lwM2mClientsByEndpoint.values().size()); + log.warn("SessionInfo input [{}], lwM2mClientsByEndpoint size: [{}] lwM2mClientsByRegistrationId: [{}]", sessionInfo, lwM2mClientsByEndpoint.values(), lwM2mClientsByRegistrationId.values()); log.error("", new RuntimeException()); } return lwM2mClient; @@ -205,19 +205,13 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } } - /** - * if isVer - ok or default ver=DEFAULT_LWM2M_VERSION - * - * @param registration - - * @return - all objectIdVer in client - */ @Override - public Set getSupportedIdVerInClient(Registration registration) { + public Set getSupportedIdVerInClient(LwM2mClient client) { Set clientObjects = ConcurrentHashMap.newKeySet(); - Arrays.stream(registration.getObjectLinks()).forEach(url -> { - LwM2mPath pathIds = new LwM2mPath(url.getUrl()); + Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> { + LwM2mPath pathIds = new LwM2mPath(link.getUrl()); if (!pathIds.isRoot()) { - clientObjects.add(convertPathFromObjectIdToIdVer(url.getUrl(), registration)); + clientObjects.add(convertPathFromObjectIdToIdVer(link.getUrl(), client.getRegistration())); } }); return (clientObjects.size() > 0) ? clientObjects : null; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java index 19af453f3c..67edbf2747 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java @@ -20,15 +20,23 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MClientStrategy; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MSoftwareUpdateStrategy; @Data public class LwM2mClientProfile { + private final String clientStrategyStr = "clientStrategy"; + private final String fwUpdateStrategyStr = "fwUpdateStrategy"; + private final String swUpdateStrategyStr = "swUpdateStrategy"; private TenantId tenantId; /** - * {"clientLwM2mSettings": { - * clientUpdateValueAfterConnect: false; - * } + * "clientLwM2mSettings": { + * "fwUpdateStrategy": "1", + * "swUpdateStrategy": "1", + * "clientStrategy": "1" + * } **/ private JsonObject postClientLwM2mSettings; @@ -85,5 +93,21 @@ public class LwM2mClientProfile { } } + public int getClientStrategy() { + return this.postClientLwM2mSettings.getAsJsonObject().has(this.clientStrategyStr) ? + Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.clientStrategyStr).getAsString()) : + LwM2MClientStrategy.CLIENT_STRATEGY_1.code; + } + public int getFwUpdateStrategy() { + return this.postClientLwM2mSettings.getAsJsonObject().has(this.fwUpdateStrategyStr) ? + Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.fwUpdateStrategyStr).getAsString()) : + LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY.code; + } + + public int getSwUpdateStrategy() { + return this.postClientLwM2mSettings.getAsJsonObject().has(this.swUpdateStrategyStr) ? + Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.swUpdateStrategyStr).getAsString()) : + LwM2MSoftwareUpdateStrategy.BINARY.code; + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientRpcRequest.java similarity index 98% rename from common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientRpcRequest.java index 40dace0b5e..33525426e3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/Lwm2mClientRpcRequest.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientRpcRequest.java @@ -57,7 +57,7 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.v @Slf4j @Data -public class Lwm2mClientRpcRequest { +public class LwM2mClientRpcRequest { private Registration registration; private TransportProtos.SessionInfoProto sessionInfo; @@ -75,10 +75,10 @@ public class Lwm2mClientRpcRequest { private String infoMsg; private String responseCode; - public Lwm2mClientRpcRequest() { + public LwM2mClientRpcRequest() { } - public Lwm2mClientRpcRequest(LwM2mTypeOper lwM2mTypeOper, String bodyParams, int requestId, + public LwM2mClientRpcRequest(LwM2mTypeOper lwM2mTypeOper, String bodyParams, int requestId, TransportProtos.SessionInfoProto sessionInfo, Registration registration, DefaultLwM2MTransportMsgHandler handler) { this.registration = registration; this.sessionInfo = sessionInfo; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java index 7a93d15988..6828761fb0 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mFwSwUpdate.java @@ -20,6 +20,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; import org.thingsboard.server.gen.transport.TransportProtos; @@ -32,21 +33,32 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; import static org.thingsboard.server.common.data.ota.OtaPackageKey.STATE; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INITIATED; +import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.common.data.ota.OtaPackageUtil.getAttributeKey; -import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FIRMWARE_UPDATE_COAP_RECOURSE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_3_VER_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_5_VER_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_NAME_ID; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_19_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_5_ID; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_URI_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_RESULT_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_STATE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE_ID; -import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_VER_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_19_BINARY; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE; @@ -59,6 +71,7 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.S import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_UPDATE_STATE_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_VER_ID; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.equalsFwSateToFirmwareUpdateStatus; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.splitCamelCaseString; @Slf4j @@ -106,23 +119,29 @@ public class LwM2mFwSwUpdate { private final List pendingInfoRequestsStart; @Getter @Setter - private volatile Lwm2mClientRpcRequest rpcRequest; + private volatile LwM2mClientRpcRequest rpcRequest; + @Getter + @Setter + private volatile int updateStrategy; - public LwM2mFwSwUpdate(LwM2mClient lwM2MClient, OtaPackageType type) { + public LwM2mFwSwUpdate(LwM2mClient lwM2MClient, OtaPackageType type, int updateStrategy) { this.lwM2MClient = lwM2MClient; this.pendingInfoRequestsStart = new CopyOnWriteArrayList<>(); this.type = type; this.stateUpdate = null; + this.updateStrategy = updateStrategy; this.initPathId(); } private void initPathId() { if (FIRMWARE.equals(this.type)) { - this.pathPackageId = FW_PACKAGE_ID; + this.pathPackageId = LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY.code == this.updateStrategy ? + FW_PACKAGE_5_ID : LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL.code == this.updateStrategy ? + FW_PACKAGE_URI_ID : FW_PACKAGE_19_ID; this.pathStateId = FW_STATE_ID; this.pathResultId = FW_RESULT_ID; this.pathNameId = FW_NAME_ID; - this.pathVerId = FW_VER_ID; + this.pathVerId = FW_5_VER_ID; this.pathInstallId = FW_UPDATE_ID; this.wUpdate = FW_UPDATE; } else if (SOFTWARE.equals(this.type)) { @@ -143,13 +162,13 @@ public class LwM2mFwSwUpdate { } if (this.pendingInfoRequestsStart.size() == 0) { this.infoFwSwUpdate = false; - if (!OtaPackageUpdateStatus.DOWNLOADING.name().equals(this.stateUpdate)) { - boolean conditionalStart = this.type.equals(FIRMWARE) ? this.conditionalFwUpdateStart() : - this.conditionalSwUpdateStart(); - if (conditionalStart) { - this.writeFwSwWare(handler, request); - } +// if (!FAILED.name().equals(this.stateUpdate)) { + boolean conditionalStart = this.type.equals(FIRMWARE) ? this.conditionalFwUpdateStart(handler) : + this.conditionalSwUpdateStart(handler); + if (conditionalStart) { + this.writeFwSwWare(handler, request); } +// } } } @@ -159,32 +178,41 @@ public class LwM2mFwSwUpdate { */ public void writeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { if (this.currentId != null) { - this.stateUpdate = OtaPackageUpdateStatus.DOWNLOADING.name(); + this.stateUpdate = OtaPackageUpdateStatus.INITIATED.name(); this.sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null); - int chunkSize = 0; - int chunk = 0; - byte[] firmwareChunk = handler.otaPackageDataCache.get(this.currentId.toString(), chunkSize, chunk); String targetIdVer = convertPathFromObjectIdToIdVer(this.pathPackageId, this.lwM2MClient.getRegistration()); String fwMsg = String.format("%s: Start type operation %s paths: %s", LOG_LW2M_INFO, - LwM2mTransportUtil.LwM2mTypeOper.FW_UPDATE.name(), FW_PACKAGE_ID); - handler.sendLogsToThingsboard(lwM2MClient, fwMsg); + LwM2mTransportUtil.LwM2mTypeOper.FW_UPDATE.name(), this.pathPackageId); + handler.sendLogsToThingsboard(fwMsg, lwM2MClient.getRegistration().getId()); log.warn("8) Start firmware Update. Send save to: [{}] ver: [{}] path: [{}]", this.lwM2MClient.getDeviceName(), this.currentVersion, targetIdVer); - request.sendAllRequest(this.lwM2MClient.getRegistration(), targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE.getName(), - firmwareChunk, handler.config.getTimeout(), this.rpcRequest); - } - else { + if (LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY.code == this.updateStrategy) { + int chunkSize = 0; + int chunk = 0; + byte[] firmwareChunk = handler.otaPackageDataCache.get(this.currentId.toString(), chunkSize, chunk); + request.sendAllRequest(this.lwM2MClient, targetIdVer, WRITE_REPLACE, ContentFormat.OPAQUE, + firmwareChunk, handler.config.getTimeout(), this.rpcRequest); + } else if (LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL.code == this.updateStrategy) { + String apiFont = "coap://176.36.143.9:5685"; + String uri = apiFont + "/" + FIRMWARE_UPDATE_COAP_RECOURSE + "/" + this.currentId.toString(); + log.warn("89) coapUri: [{}]", uri); + request.sendAllRequest(this.lwM2MClient, targetIdVer, WRITE_REPLACE, null, + uri, handler.config.getTimeout(), this.rpcRequest); + } else if (LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_19_BINARY.code == this.updateStrategy) { + + } + } else { String msgError = "FirmWareId is null."; log.warn("6) [{}]", msgError); if (this.rpcRequest != null) { handler.sentRpcResponse(this.rpcRequest, CONTENT.name(), msgError, LOG_LW2M_ERROR); } - log.error (msgError); + log.error(msgError); this.sendLogs(handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError); } } public void sendLogs(DefaultLwM2MTransportMsgHandler handler, String typeOper, String typeInfo, String msgError) { - this.sendSateOnThingsBoard(handler); +// this.sendSateOnThingsBoard(handler); String msg = String.format("%s: %s, %s, pkgVer: %s: pkgName - %s state - %s.", typeInfo, this.wUpdate, typeOper, this.currentVersion, this.currentTitle, this.stateUpdate); if (LOG_LW2M_ERROR.equals(typeInfo)) { @@ -200,32 +228,49 @@ public class LwM2mFwSwUpdate { * send execute */ public void executeFwSwWare(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { - this.setStateUpdate(UPDATING.name()); this.sendLogs(handler, EXECUTE.name(), LOG_LW2M_INFO, null); - request.sendAllRequest(this.lwM2MClient.getRegistration(), this.pathInstallId, EXECUTE, ContentFormat.TLV.getName(), - null, 0, this.rpcRequest); + request.sendAllRequest(this.lwM2MClient, this.pathInstallId, EXECUTE, null, 0, this.rpcRequest); } /** - * Firmware start: + * Firmware start: Check if the version has changed and launch a new update. + * -ObjectId 5, Binary or ObjectId 5, URI * -- If the result of the update - errors (more than 1) - This means that the previous. the update failed. * - We launch the update regardless of the state of the firmware and its version. - * -- If the result of the update is not errors (equal to 1 or 0) and ver is not empty - This means that before the update has passed. - * -- If the result of the update is not errors and is empty - This means that there has not been an update yet. - * - Check if the version has changed and launch a new update. + * -- If the result of the update - errors (more than 1) - This means that the previous. the update failed. + * * ObjectId 5, Binary + * -- If the result of the update is not errors (equal to 1 or 0) and ver in Object 5 is not empty - it means that the previous update has passed. + * Compare current versions by equals. + * * ObjectId 5, URI + * -- If the result of the update is not errors (equal to 1 or 0) and ver in Object 5 is not empty - it means that the previous update has passed. + * Compare current versions by contains. */ - private boolean conditionalFwUpdateStart() { + private boolean conditionalFwUpdateStart(DefaultLwM2MTransportMsgHandler handler) { Long updateResultFw = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); + String ver5 = (String) this.lwM2MClient.getResourceValue(null, this.pathVerId); + String pathName = (String) this.lwM2MClient.getResourceValue(null, this.pathNameId); + String ver3 = (String) this.lwM2MClient.getResourceValue(null, FW_3_VER_ID); // #1/#2 - return updateResultFw > LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code || - ( - (updateResultFw <= LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code - ) && - ( - (this.currentVersion != null && !this.currentVersion.equals(this.lwM2MClient.getResourceValue(null, this.pathVerId))) || - (this.currentTitle != null && !this.currentTitle.equals(this.lwM2MClient.getResourceValue(null, this.pathNameId))) - ) - ); + String fwMsg = null; + if ((this.currentVersion != null && ( + ver5 != null && ver5.equals(this.currentVersion) || + ver3 != null && ver3.contains(this.currentVersion) + )) || + (this.currentTitle != null && pathName != null && this.currentTitle.equals(pathName))) { + fwMsg = String.format("%s: The update was interrupted. The device has the same version: %s.", LOG_LW2M_ERROR, + this.currentVersion); + } + else if (updateResultFw != null && updateResultFw > LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code) { + fwMsg = String.format("%s: The update was interrupted. The device has the status UpdateResult: error (%d).", LOG_LW2M_ERROR, + updateResultFw); + } + if (fwMsg != null) { + handler.sendLogsToThingsboard(fwMsg, lwM2MClient.getRegistration().getId()); + return false; + } + else { + return true; + } } @@ -249,7 +294,7 @@ public class LwM2mFwSwUpdate { /** * After operation Execute success inspection Update Result : - * > 1 error: "Firmware updated successfully" + * > 1 error: "Firmware updated successfully" */ public boolean conditionalFwExecuteAfterError() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); @@ -265,7 +310,7 @@ public class LwM2mFwSwUpdate { * - If Update Result is not errors and ver is not empty - This means that before unInstall update * * - Check if the version has changed and launch a new update. */ - private boolean conditionalSwUpdateStart() { + private boolean conditionalSwUpdateStart(DefaultLwM2MTransportMsgHandler handler) { Long updateResultSw = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); // #1/#2 return updateResultSw >= LwM2mTransportUtil.UpdateResultSw.NOT_ENOUGH_STORAGE.code || @@ -293,7 +338,7 @@ public class LwM2mFwSwUpdate { * -- inspection Update Result: * ---- FW если Update Result == 1 ("Firmware updated successfully") или SW если Update Result == 2 ("Software successfully installed.") * -- fw_state/sw_state = UPDATED - * + *

* After finish operation Execute (error): * -- inspection updateResult and send to thingsboard info about error * --- send to telemetry ( key - this is name Update Result in model) ( @@ -326,7 +371,7 @@ public class LwM2mFwSwUpdate { /** * After operation Execute success inspection Update Result : - * >= 50 - error "NOT_ENOUGH_STORAGE" + * >= 50 - error "NOT_ENOUGH_STORAGE" */ public boolean conditionalSwExecuteAfterError() { Long updateResult = (Long) this.lwM2MClient.getResourceValue(null, this.pathResultId); @@ -334,10 +379,10 @@ public class LwM2mFwSwUpdate { } private void observeStateUpdate(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request) { - request.sendAllRequest(lwM2MClient.getRegistration(), + request.sendAllRequest(lwM2MClient, convertPathFromObjectIdToIdVer(this.pathStateId, this.lwM2MClient.getRegistration()), OBSERVE, null, null, 0, null); - request.sendAllRequest(lwM2MClient.getRegistration(), + request.sendAllRequest(lwM2MClient, convertPathFromObjectIdToIdVer(this.pathResultId, this.lwM2MClient.getRegistration()), OBSERVE, null, null, 0, null); } @@ -355,18 +400,75 @@ public class LwM2mFwSwUpdate { public void sendReadObserveInfo(LwM2mTransportRequest request) { this.infoFwSwUpdate = true; - this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( - this.pathVerId, this.lwM2MClient.getRegistration())); - this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( - this.pathNameId, this.lwM2MClient.getRegistration())); this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( this.pathStateId, this.lwM2MClient.getRegistration())); this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( this.pathResultId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + FW_3_VER_ID, this.lwM2MClient.getRegistration())); + if (LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY.code == this.updateStrategy || + LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy.OBJ_19_BINARY.code == this.updateStrategy || + SOFTWARE.equals(this.type)) { + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathVerId, this.lwM2MClient.getRegistration())); + this.pendingInfoRequestsStart.add(convertPathFromObjectIdToIdVer( + this.pathNameId, this.lwM2MClient.getRegistration())); + } this.pendingInfoRequestsStart.forEach(pathIdVer -> { - request.sendAllRequest(this.lwM2MClient.getRegistration(), pathIdVer, OBSERVE, ContentFormat.TLV.getName(), - null, 0, this.rpcRequest); + request.sendAllRequest(this.lwM2MClient, pathIdVer, OBSERVE, null, 0, this.rpcRequest); }); } + + /** + * Before operation Execute (FwUpdate) inspection Update Result : + * - after finished operation Write result: success (FwUpdate): fw_state = DOWNLOADED + * - before start operation Execute (FwUpdate) Update Result = 0 - Initial value + * - start Execute (FwUpdate) + * After finished operation Execute (FwUpdate) inspection Update Result : + * - after start operation Execute (FwUpdate): fw_state = UPDATING + * - after success finished operation Execute (FwUpdate) Update Result == 1 ("Firmware updated successfully") + * - finished operation Execute (FwUpdate) + */ + public void updateStateOta(DefaultLwM2MTransportMsgHandler handler, LwM2mTransportRequest request, + Registration registration, String path, int value) { + if (OBJ_5_BINARY.code == this.getUpdateStrategy()) { + if ((convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path))) { + if (DOWNLOADED.name().equals(this.getStateUpdate()) + && this.conditionalFwExecuteStart()) { + this.executeFwSwWare(handler, request); + } else if (UPDATING.name().equals(this.getStateUpdate()) + && this.conditionalFwExecuteAfterSuccess()) { + this.finishFwSwUpdate(handler, true); + } else if (UPDATING.name().equals(this.getStateUpdate()) + && this.conditionalFwExecuteAfterError()) { + this.finishFwSwUpdate(handler, false); + } + } + } else if (OBJ_5_TEMP_URL.code == this.getUpdateStrategy()) { + if (this.currentId != null && (convertPathFromObjectIdToIdVer(FW_STATE_ID, registration).equals(path))) { + String state = equalsFwSateToFirmwareUpdateStatus(LwM2mTransportUtil.StateFw.fromStateFwByCode(value)).name(); + if (StringUtils.isNotEmpty(state) && !FAILED.name().equals(this.stateUpdate) && !state.equals(this.stateUpdate)) { + this.stateUpdate = state; + this.sendSateOnThingsBoard(handler); + } + if (value == LwM2mTransportUtil.StateFw.DOWNLOADED.code) { + this.executeFwSwWare(handler, request); + } + handler.firmwareUpdateState.put(lwM2MClient.getEndpoint(), value); + } + if ((convertPathFromObjectIdToIdVer(FW_RESULT_ID, registration).equals(path))) { + if (this.currentId != null && value == LwM2mTransportUtil.UpdateResultFw.INITIAL.code) { + this.setStateUpdate(INITIATED.name()); + } else if (this.currentId != null && value == LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code) { + this.setStateUpdate(UPDATED.name()); + } else if (value > LwM2mTransportUtil.UpdateResultFw.UPDATE_SUCCESSFULLY.code) { + this.setStateUpdate(FAILED.name()); + } + this.sendSateOnThingsBoard(handler); + } + } else if (OBJ_19_BINARY.code == this.getUpdateStrategy()) { + + } + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 7882ad2410..891c466151 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -259,8 +259,10 @@ public class GatewaySessionHandler { transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() .setSessionInfo(deviceSessionInfo) .setSessionEvent(DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.OPEN)) - .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build()) - .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder().build()) + .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC).build()) + .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC).build()) .build(), null); } futureToSet.set(devices.get(deviceName)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index e41289e97b..7ec1e662af 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -434,7 +434,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) { throw new DataValidationException("Can't assign firmware with type: " + firmware.getType()); } - if (firmware.getData() == null) { + if (firmware.getData() == null && !firmware.hasUrl()) { throw new DataValidationException("Can't assign firmware with empty data!"); } if (!firmware.getDeviceProfileId().equals(deviceProfile.getId())) { @@ -450,7 +450,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D if (!software.getType().equals(OtaPackageType.SOFTWARE)) { throw new DataValidationException("Can't assign software with type: " + software.getType()); } - if (software.getData() == null) { + if (software.getData() == null && !software.hasUrl()) { throw new DataValidationException("Can't assign software with empty data!"); } if (!software.getDeviceProfileId().equals(deviceProfile.getId())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 34aa462a6d..f1470ff5ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -716,7 +716,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe if (!firmware.getType().equals(OtaPackageType.FIRMWARE)) { throw new DataValidationException("Can't assign firmware with type: " + firmware.getType()); } - if (firmware.getData() == null) { + if (firmware.getData() == null && !firmware.hasUrl()) { throw new DataValidationException("Can't assign firmware with empty data!"); } if (!firmware.getDeviceProfileId().equals(device.getDeviceProfileId())) { @@ -732,7 +732,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe if (!software.getType().equals(OtaPackageType.SOFTWARE)) { throw new DataValidationException("Can't assign software with type: " + software.getType()); } - if (software.getData() == null) { + if (software.getData() == null && !software.hasUrl()) { throw new DataValidationException("Can't assign software with empty data!"); } if (!software.getDeviceProfileId().equals(device.getDeviceProfileId())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index bfb530dd35..87cee53991 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -408,10 +408,20 @@ public class ModelConstants { /** * OAuth2 client registration constants. */ - public static final String OAUTH2_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + + public static final String OAUTH2_PARAMS_COLUMN_FAMILY_NAME = "oauth2_params"; + public static final String OAUTH2_PARAMS_ENABLED_PROPERTY = "enabled"; + public static final String OAUTH2_PARAMS_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + + public static final String OAUTH2_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_registration"; + public static final String OAUTH2_DOMAIN_COLUMN_FAMILY_NAME = "oauth2_domain"; + public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile"; + public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; + public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name"; + public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret"; + public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; - public static final String OAUTH2_CLIENT_REGISTRATION_TO_DOMAIN_COLUMN_FAMILY_NAME = "oauth2_client_registration_to_domain"; public static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_COLUMN_FAMILY_NAME = "oauth2_client_registration_template"; public static final String OAUTH2_ENABLED_PROPERTY = "enabled"; public static final String OAUTH2_TEMPLATE_PROVIDER_ID_PROPERTY = "provider_id"; @@ -422,8 +432,8 @@ public class ModelConstants { public static final String OAUTH2_CLIENT_SECRET_PROPERTY = "client_secret"; public static final String OAUTH2_AUTHORIZATION_URI_PROPERTY = "authorization_uri"; public static final String OAUTH2_TOKEN_URI_PROPERTY = "token_uri"; - public static final String OAUTH2_REDIRECT_URI_TEMPLATE_PROPERTY = "redirect_uri_template"; public static final String OAUTH2_SCOPE_PROPERTY = "scope"; + public static final String OAUTH2_PLATFORMS_PROPERTY = "platforms"; public static final String OAUTH2_USER_INFO_URI_PROPERTY = "user_info_uri"; public static final String OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY = "user_name_attribute_name"; public static final String OAUTH2_JWK_SET_URI_PROPERTY = "jwk_set_uri"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java new file mode 100644 index 0000000000..f1dbe6fff7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2DomainEntity.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.OAuth2DomainId; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.OAUTH2_DOMAIN_COLUMN_FAMILY_NAME) +public class OAuth2DomainEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) + private UUID oauth2ParamsId; + + @Column(name = ModelConstants.OAUTH2_DOMAIN_NAME_PROPERTY) + private String domainName; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_DOMAIN_SCHEME_PROPERTY) + private SchemeType domainScheme; + + public OAuth2DomainEntity() { + super(); + } + + public OAuth2DomainEntity(OAuth2Domain domain) { + if (domain.getId() != null) { + this.setUuid(domain.getId().getId()); + } + this.setCreatedTime(domain.getCreatedTime()); + if (domain.getOauth2ParamsId() != null) { + this.oauth2ParamsId = domain.getOauth2ParamsId().getId(); + } + this.domainName = domain.getDomainName(); + this.domainScheme = domain.getDomainScheme(); + } + + @Override + public OAuth2Domain toData() { + OAuth2Domain domain = new OAuth2Domain(); + domain.setId(new OAuth2DomainId(id)); + domain.setCreatedTime(createdTime); + domain.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); + domain.setDomainName(domainName); + domain.setDomainScheme(domainScheme); + return domain; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java new file mode 100644 index 0000000000..a6517011b0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.OAuth2MobileId; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.OAUTH2_MOBILE_COLUMN_FAMILY_NAME) +public class OAuth2MobileEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) + private UUID oauth2ParamsId; + + @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) + private String pkgName; + + @Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY) + private String appSecret; + + public OAuth2MobileEntity() { + super(); + } + + public OAuth2MobileEntity(OAuth2Mobile mobile) { + if (mobile.getId() != null) { + this.setUuid(mobile.getId().getId()); + } + this.setCreatedTime(mobile.getCreatedTime()); + if (mobile.getOauth2ParamsId() != null) { + this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); + } + this.pkgName = mobile.getPkgName(); + this.appSecret = mobile.getAppSecret(); + } + + @Override + public OAuth2Mobile toData() { + OAuth2Mobile mobile = new OAuth2Mobile(); + mobile.setId(new OAuth2MobileId(id)); + mobile.setCreatedTime(createdTime); + mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); + mobile.setPkgName(pkgName); + mobile.setAppSecret(appSecret); + return mobile; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java new file mode 100644 index 0000000000..f2c893f6f3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ParamsEntity.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.OAUTH2_PARAMS_COLUMN_FAMILY_NAME) +@NoArgsConstructor +public class OAuth2ParamsEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_PARAMS_ENABLED_PROPERTY) + private Boolean enabled; + + @Column(name = ModelConstants.OAUTH2_PARAMS_TENANT_ID_PROPERTY) + private UUID tenantId; + + public OAuth2ParamsEntity(OAuth2Params oauth2Params) { + if (oauth2Params.getId() != null) { + this.setUuid(oauth2Params.getUuidId()); + } + this.setCreatedTime(oauth2Params.getCreatedTime()); + this.enabled = oauth2Params.isEnabled(); + if (oauth2Params.getTenantId() != null) { + this.tenantId = oauth2Params.getTenantId().getId(); + } + } + + @Override + public OAuth2Params toData() { + OAuth2Params oauth2Params = new OAuth2Params(); + oauth2Params.setId(new OAuth2ParamsId(id)); + oauth2Params.setCreatedTime(createdTime); + oauth2Params.setTenantId(new TenantId(tenantId)); + oauth2Params.setEnabled(enabled); + return oauth2Params; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java new file mode 100644 index 0000000000..ca32bad322 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java @@ -0,0 +1,221 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import io.micrometer.core.instrument.util.StringUtils; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.id.OAuth2RegistrationId; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Collectors; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.OAUTH2_REGISTRATION_COLUMN_FAMILY_NAME) +public class OAuth2RegistrationEntity extends BaseSqlEntity { + + @Column(name = ModelConstants.OAUTH2_PARAMS_ID_PROPERTY) + private UUID oauth2ParamsId; + @Column(name = ModelConstants.OAUTH2_CLIENT_ID_PROPERTY) + private String clientId; + @Column(name = ModelConstants.OAUTH2_CLIENT_SECRET_PROPERTY) + private String clientSecret; + @Column(name = ModelConstants.OAUTH2_AUTHORIZATION_URI_PROPERTY) + private String authorizationUri; + @Column(name = ModelConstants.OAUTH2_TOKEN_URI_PROPERTY) + private String tokenUri; + @Column(name = ModelConstants.OAUTH2_SCOPE_PROPERTY) + private String scope; + @Column(name = ModelConstants.OAUTH2_PLATFORMS_PROPERTY) + private String platforms; + @Column(name = ModelConstants.OAUTH2_USER_INFO_URI_PROPERTY) + private String userInfoUri; + @Column(name = ModelConstants.OAUTH2_USER_NAME_ATTRIBUTE_NAME_PROPERTY) + private String userNameAttributeName; + @Column(name = ModelConstants.OAUTH2_JWK_SET_URI_PROPERTY) + private String jwkSetUri; + @Column(name = ModelConstants.OAUTH2_CLIENT_AUTHENTICATION_METHOD_PROPERTY) + private String clientAuthenticationMethod; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_LABEL_PROPERTY) + private String loginButtonLabel; + @Column(name = ModelConstants.OAUTH2_LOGIN_BUTTON_ICON_PROPERTY) + private String loginButtonIcon; + @Column(name = ModelConstants.OAUTH2_ALLOW_USER_CREATION_PROPERTY) + private Boolean allowUserCreation; + @Column(name = ModelConstants.OAUTH2_ACTIVATE_USER_PROPERTY) + private Boolean activateUser; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_MAPPER_TYPE_PROPERTY) + private MapperType type; + @Column(name = ModelConstants.OAUTH2_EMAIL_ATTRIBUTE_KEY_PROPERTY) + private String emailAttributeKey; + @Column(name = ModelConstants.OAUTH2_FIRST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String firstNameAttributeKey; + @Column(name = ModelConstants.OAUTH2_LAST_NAME_ATTRIBUTE_KEY_PROPERTY) + private String lastNameAttributeKey; + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_STRATEGY_PROPERTY) + private TenantNameStrategyType tenantNameStrategy; + @Column(name = ModelConstants.OAUTH2_TENANT_NAME_PATTERN_PROPERTY) + private String tenantNamePattern; + @Column(name = ModelConstants.OAUTH2_CUSTOMER_NAME_PATTERN_PROPERTY) + private String customerNamePattern; + @Column(name = ModelConstants.OAUTH2_DEFAULT_DASHBOARD_NAME_PROPERTY) + private String defaultDashboardName; + @Column(name = ModelConstants.OAUTH2_ALWAYS_FULL_SCREEN_PROPERTY) + private Boolean alwaysFullScreen; + @Column(name = ModelConstants.OAUTH2_MAPPER_URL_PROPERTY) + private String url; + @Column(name = ModelConstants.OAUTH2_MAPPER_USERNAME_PROPERTY) + private String username; + @Column(name = ModelConstants.OAUTH2_MAPPER_PASSWORD_PROPERTY) + private String password; + @Column(name = ModelConstants.OAUTH2_MAPPER_SEND_TOKEN_PROPERTY) + private Boolean sendToken; + + @Type(type = "json") + @Column(name = ModelConstants.OAUTH2_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + public OAuth2RegistrationEntity() { + super(); + } + + public OAuth2RegistrationEntity(OAuth2Registration registration) { + if (registration.getId() != null) { + this.setUuid(registration.getId().getId()); + } + this.setCreatedTime(registration.getCreatedTime()); + if (registration.getOauth2ParamsId() != null) { + this.oauth2ParamsId = registration.getOauth2ParamsId().getId(); + } + this.clientId = registration.getClientId(); + this.clientSecret = registration.getClientSecret(); + this.authorizationUri = registration.getAuthorizationUri(); + this.tokenUri = registration.getAccessTokenUri(); + this.scope = registration.getScope().stream().reduce((result, element) -> result + "," + element).orElse(""); + this.platforms = registration.getPlatforms() != null ? registration.getPlatforms().stream().map(Enum::name).reduce((result, element) -> result + "," + element).orElse("") : ""; + this.userInfoUri = registration.getUserInfoUri(); + this.userNameAttributeName = registration.getUserNameAttributeName(); + this.jwkSetUri = registration.getJwkSetUri(); + this.clientAuthenticationMethod = registration.getClientAuthenticationMethod(); + this.loginButtonLabel = registration.getLoginButtonLabel(); + this.loginButtonIcon = registration.getLoginButtonIcon(); + this.additionalInfo = registration.getAdditionalInfo(); + OAuth2MapperConfig mapperConfig = registration.getMapperConfig(); + if (mapperConfig != null) { + this.allowUserCreation = mapperConfig.isAllowUserCreation(); + this.activateUser = mapperConfig.isActivateUser(); + this.type = mapperConfig.getType(); + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig != null) { + this.emailAttributeKey = basicConfig.getEmailAttributeKey(); + this.firstNameAttributeKey = basicConfig.getFirstNameAttributeKey(); + this.lastNameAttributeKey = basicConfig.getLastNameAttributeKey(); + this.tenantNameStrategy = basicConfig.getTenantNameStrategy(); + this.tenantNamePattern = basicConfig.getTenantNamePattern(); + this.customerNamePattern = basicConfig.getCustomerNamePattern(); + this.defaultDashboardName = basicConfig.getDefaultDashboardName(); + this.alwaysFullScreen = basicConfig.isAlwaysFullScreen(); + } + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig != null) { + this.url = customConfig.getUrl(); + this.username = customConfig.getUsername(); + this.password = customConfig.getPassword(); + this.sendToken = customConfig.isSendToken(); + } + } + } + + @Override + public OAuth2Registration toData() { + OAuth2Registration registration = new OAuth2Registration(); + registration.setId(new OAuth2RegistrationId(id)); + registration.setCreatedTime(createdTime); + registration.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); + registration.setAdditionalInfo(additionalInfo); + registration.setMapperConfig( + OAuth2MapperConfig.builder() + .allowUserCreation(allowUserCreation) + .activateUser(activateUser) + .type(type) + .basic( + (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ? + OAuth2BasicMapperConfig.builder() + .emailAttributeKey(emailAttributeKey) + .firstNameAttributeKey(firstNameAttributeKey) + .lastNameAttributeKey(lastNameAttributeKey) + .tenantNameStrategy(tenantNameStrategy) + .tenantNamePattern(tenantNamePattern) + .customerNamePattern(customerNamePattern) + .defaultDashboardName(defaultDashboardName) + .alwaysFullScreen(alwaysFullScreen) + .build() + : null + ) + .custom( + type == MapperType.CUSTOM ? + OAuth2CustomMapperConfig.builder() + .url(url) + .username(username) + .password(password) + .sendToken(sendToken) + .build() + : null + ) + .build() + ); + registration.setClientId(clientId); + registration.setClientSecret(clientSecret); + registration.setAuthorizationUri(authorizationUri); + registration.setAccessTokenUri(tokenUri); + registration.setScope(Arrays.asList(scope.split(","))); + registration.setPlatforms(StringUtils.isNotEmpty(platforms) ? Arrays.stream(platforms.split(",")) + .map(str -> PlatformType.valueOf(str)).collect(Collectors.toList()) : Collections.emptyList()); + registration.setUserInfoUri(userInfoUri); + registration.setUserNameAttributeName(userNameAttributeName); + registration.setJwkSetUri(jwkSetUri); + registration.setClientAuthenticationMethod(clientAuthenticationMethod); + registration.setLoginButtonLabel(loginButtonLabel); + registration.setLoginButtonIcon(loginButtonIcon); + return registration; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/AbstractOAuth2ClientRegistrationInfoEntity.java similarity index 97% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/AbstractOAuth2ClientRegistrationInfoEntity.java index 4799087457..616f128920 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/AbstractOAuth2ClientRegistrationInfoEntity.java @@ -13,22 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sql.deprecated; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationInfoId; import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.sql.deprecated.OAuth2ClientRegistrationInfoEntity; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.*; import java.util.Arrays; +@Deprecated @Data @EqualsAndHashCode(callSuper = true) @TypeDef(name = "json", typeClass = JsonStringType.class) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/ExtendedOAuth2ClientRegistrationInfoEntity.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/ExtendedOAuth2ClientRegistrationInfoEntity.java index 129bcf730b..beb525e2e0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ExtendedOAuth2ClientRegistrationInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/ExtendedOAuth2ClientRegistrationInfoEntity.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sql.deprecated; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.SchemeType; +@Deprecated @Data @EqualsAndHashCode(callSuper = true) public class ExtendedOAuth2ClientRegistrationInfoEntity extends AbstractOAuth2ClientRegistrationInfoEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationEntity.java similarity index 89% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationEntity.java index 505f2facf1..9de0b9daea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationEntity.java @@ -13,22 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sql.deprecated; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationId; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationInfoId; import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistration; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.*; -import java.util.Arrays; import java.util.UUID; +@Deprecated @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationInfoEntity.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationInfoEntity.java index 1a379baf23..fbb9ec3fca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/deprecated/OAuth2ClientRegistrationInfoEntity.java @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.model.sql; +package org.thingsboard.server.dao.model.sql.deprecated; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.TypeDef; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Entity; import javax.persistence.Table; +@Deprecated @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 7361dc21d2..feba5c0158 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -21,7 +21,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import java.util.UUID; @@ -34,25 +34,25 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep @Override public ClientRegistration findByRegistrationId(String registrationId) { - OAuth2ClientRegistrationInfo oAuth2ClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(UUID.fromString(registrationId)); - return oAuth2ClientRegistrationInfo == null ? - null : toSpringClientRegistration(oAuth2ClientRegistrationInfo); + OAuth2Registration registration = oAuth2Service.findRegistration(UUID.fromString(registrationId)); + return registration == null ? + null : toSpringClientRegistration(registration); } - private ClientRegistration toSpringClientRegistration(OAuth2ClientRegistrationInfo localClientRegistration){ - String registrationId = localClientRegistration.getUuidId().toString(); + private ClientRegistration toSpringClientRegistration(OAuth2Registration registration){ + String registrationId = registration.getUuidId().toString(); return ClientRegistration.withRegistrationId(registrationId) - .clientName(localClientRegistration.getName()) - .clientId(localClientRegistration.getClientId()) - .authorizationUri(localClientRegistration.getAuthorizationUri()) - .clientSecret(localClientRegistration.getClientSecret()) - .tokenUri(localClientRegistration.getAccessTokenUri()) - .scope(localClientRegistration.getScope()) + .clientName(registration.getName()) + .clientId(registration.getClientId()) + .authorizationUri(registration.getAuthorizationUri()) + .clientSecret(registration.getClientSecret()) + .tokenUri(registration.getAccessTokenUri()) + .scope(registration.getScope()) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .userInfoUri(localClientRegistration.getUserInfoUri()) - .userNameAttributeName(localClientRegistration.getUserNameAttributeName()) - .jwkSetUri(localClientRegistration.getJwkSetUri()) - .clientAuthenticationMethod(new ClientAuthenticationMethod(localClientRegistration.getClientAuthenticationMethod())) + .userInfoUri(registration.getUserInfoUri()) + .userNameAttributeName(registration.getUserNameAttributeName()) + .jwkSetUri(registration.getJwkSetUri()) + .clientAuthenticationMethod(new ClientAuthenticationMethod(registration.getClientAuthenticationMethod())) .redirectUri(defaultRedirectUriTemplate) .build(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java new file mode 100644 index 0000000000..78c237d943 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2DomainDao extends Dao { + + List findByOAuth2ParamsId(UUID oauth2ParamsId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java new file mode 100644 index 0000000000..3bccb61b0b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2MobileDao.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2MobileDao extends Dao { + + List findByOAuth2ParamsId(UUID oauth2ParamsId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ParamsDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ParamsDao.java new file mode 100644 index 0000000000..5821431444 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ParamsDao.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.dao.Dao; + +public interface OAuth2ParamsDao extends Dao { + void deleteAll(); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java new file mode 100644 index 0000000000..87f0d56460 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.oauth2; + +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2RegistrationDao extends Dao { + + List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(List domainSchemes, String domainName, String pkgName, PlatformType platformType); + + List findByOAuth2ParamsId(UUID oauth2ParamsId); + + String findAppSecret(UUID id, String pkgName); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index acde25f8ed..6d7dde1f21 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -19,14 +19,44 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; +import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; +import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistration; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsDomainParams; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; +import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; import javax.transaction.Transactional; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -45,10 +75,18 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se private OAuth2ClientRegistrationInfoDao clientRegistrationInfoDao; @Autowired private OAuth2ClientRegistrationDao clientRegistrationDao; + @Autowired + private OAuth2ParamsDao oauth2ParamsDao; + @Autowired + private OAuth2RegistrationDao oauth2RegistrationDao; + @Autowired + private OAuth2DomainDao oauth2DomainDao; + @Autowired + private OAuth2MobileDao oauth2MobileDao; @Override - public List getOAuth2Clients(String domainSchemeStr, String domainName) { - log.trace("Executing getOAuth2Clients [{}://{}]", domainSchemeStr, domainName); + public List getOAuth2Clients(String domainSchemeStr, String domainName, String pkgName, PlatformType platformType) { + log.trace("Executing getOAuth2Clients [{}://{}] pkgName=[{}] platformType=[{}]", domainSchemeStr, domainName, pkgName, platformType); if (domainSchemeStr == null) { throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } @@ -59,12 +97,14 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se throw new IncorrectParameterException(INCORRECT_DOMAIN_SCHEME); } validateString(domainName, INCORRECT_DOMAIN_NAME + domainName); - return clientRegistrationInfoDao.findByDomainSchemesAndDomainName(Arrays.asList(domainScheme, SchemeType.MIXED), domainName).stream() - .filter(OAuth2ClientRegistrationInfo::isEnabled) + return oauth2RegistrationDao.findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType( + Arrays.asList(domainScheme, SchemeType.MIXED), domainName, pkgName, platformType) + .stream() .map(OAuth2Utils::toClientInfo) .collect(Collectors.toList()); } + @Deprecated @Override @Transactional public void saveOAuth2Params(OAuth2ClientsParams oauth2Params) { @@ -85,6 +125,33 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se }); } + @Override + @Transactional + public void saveOAuth2Info(OAuth2Info oauth2Info) { + log.trace("Executing saveOAuth2Info [{}]", oauth2Info); + oauth2InfoValidator.accept(oauth2Info); + oauth2ParamsDao.deleteAll(); + oauth2Info.getOauth2ParamsInfos().forEach(oauth2ParamsInfo -> { + OAuth2Params oauth2Params = OAuth2Utils.infoToOAuth2Params(oauth2Info); + OAuth2Params savedOauth2Params = oauth2ParamsDao.save(TenantId.SYS_TENANT_ID, oauth2Params); + oauth2ParamsInfo.getClientRegistrations().forEach(registrationInfo -> { + OAuth2Registration registration = OAuth2Utils.toOAuth2Registration(savedOauth2Params.getId(), registrationInfo); + oauth2RegistrationDao.save(TenantId.SYS_TENANT_ID, registration); + }); + oauth2ParamsInfo.getDomainInfos().forEach(domainInfo -> { + OAuth2Domain domain = OAuth2Utils.toOAuth2Domain(savedOauth2Params.getId(), domainInfo); + oauth2DomainDao.save(TenantId.SYS_TENANT_ID, domain); + }); + if (oauth2ParamsInfo.getMobileInfos() != null) { + oauth2ParamsInfo.getMobileInfos().forEach(mobileInfo -> { + OAuth2Mobile mobile = OAuth2Utils.toOAuth2Mobile(savedOauth2Params.getId(), mobileInfo); + oauth2MobileDao.save(TenantId.SYS_TENANT_ID, mobile); + }); + } + }); + } + + @Deprecated @Override public OAuth2ClientsParams findOAuth2Params() { log.trace("Executing findOAuth2Params"); @@ -93,16 +160,42 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se } @Override - public OAuth2ClientRegistrationInfo findClientRegistrationInfo(UUID id) { - log.trace("Executing findClientRegistrationInfo [{}]", id); + public OAuth2Info findOAuth2Info() { + log.trace("Executing findOAuth2Info"); + OAuth2Info oauth2Info = new OAuth2Info(); + List oauth2ParamsList = oauth2ParamsDao.find(TenantId.SYS_TENANT_ID); + oauth2Info.setEnabled(oauth2ParamsList.stream().anyMatch(param -> param.isEnabled())); + List oauth2ParamsInfos = new ArrayList<>(); + oauth2Info.setOauth2ParamsInfos(oauth2ParamsInfos); + oauth2ParamsList.stream().sorted(Comparator.comparing(BaseData::getUuidId)).forEach(oauth2Params -> { + List registrations = oauth2RegistrationDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); + List domains = oauth2DomainDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); + List mobiles = oauth2MobileDao.findByOAuth2ParamsId(oauth2Params.getId().getId()); + oauth2ParamsInfos.add(OAuth2Utils.toOAuth2ParamsInfo(registrations, domains, mobiles)); + }); + return oauth2Info; + } + + @Override + public OAuth2Registration findRegistration(UUID id) { + log.trace("Executing findRegistration [{}]", id); + validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); + return oauth2RegistrationDao.findById(null, id); + } + + @Override + public String findAppSecret(UUID id, String pkgName) { + log.trace("Executing findAppSecret [{}][{}]", id, pkgName); validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); - return clientRegistrationInfoDao.findById(null, id); + validateString(pkgName, "Incorrect package name"); + return oauth2RegistrationDao.findAppSecret(id, pkgName); } + @Override - public List findAllClientRegistrationInfos() { - log.trace("Executing findAllClientRegistrationInfos"); - return clientRegistrationInfoDao.findAll(); + public List findAllRegistrations() { + log.trace("Executing findAllRegistrations"); + return oauth2RegistrationDao.find(TenantId.SYS_TENANT_ID); } private final Consumer clientParamsValidator = oauth2Params -> { @@ -212,4 +305,136 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se } } }; + + private final Consumer oauth2InfoValidator = oauth2Info -> { + if (oauth2Info == null + || oauth2Info.getOauth2ParamsInfos() == null) { + throw new DataValidationException("OAuth2 param infos should be specified!"); + } + for (OAuth2ParamsInfo oauth2Params : oauth2Info.getOauth2ParamsInfos()) { + if (oauth2Params.getDomainInfos() == null + || oauth2Params.getDomainInfos().isEmpty()) { + throw new DataValidationException("List of domain configuration should be specified!"); + } + for (OAuth2DomainInfo domainInfo : oauth2Params.getDomainInfos()) { + if (StringUtils.isEmpty(domainInfo.getName())) { + throw new DataValidationException("Domain name should be specified!"); + } + if (domainInfo.getScheme() == null) { + throw new DataValidationException("Domain scheme should be specified!"); + } + } + oauth2Params.getDomainInfos().stream() + .collect(Collectors.groupingBy(OAuth2DomainInfo::getName)) + .forEach((domainName, domainInfos) -> { + if (domainInfos.size() > 1 && domainInfos.stream().anyMatch(domainInfo -> domainInfo.getScheme() == SchemeType.MIXED)) { + throw new DataValidationException("MIXED scheme type shouldn't be combined with another scheme type!"); + } + domainInfos.stream() + .collect(Collectors.groupingBy(OAuth2DomainInfo::getScheme)) + .forEach((schemeType, domainInfosBySchemeType) -> { + if (domainInfosBySchemeType.size() > 1) { + throw new DataValidationException("Domain name and protocol must be unique within OAuth2 parameters!"); + } + }); + }); + if (oauth2Params.getMobileInfos() != null) { + for (OAuth2MobileInfo mobileInfo : oauth2Params.getMobileInfos()) { + if (StringUtils.isEmpty(mobileInfo.getPkgName())) { + throw new DataValidationException("Package should be specified!"); + } + if (StringUtils.isEmpty(mobileInfo.getAppSecret())) { + throw new DataValidationException("Application secret should be specified!"); + } + if (mobileInfo.getAppSecret().length() < 16) { + throw new DataValidationException("Application secret should be at least 16 characters!"); + } + } + oauth2Params.getMobileInfos().stream() + .collect(Collectors.groupingBy(OAuth2MobileInfo::getPkgName)) + .forEach((pkgName, mobileInfos) -> { + if (mobileInfos.size() > 1) { + throw new DataValidationException("Mobile app package name must be unique within OAuth2 parameters!"); + } + }); + } + if (oauth2Params.getClientRegistrations() == null || oauth2Params.getClientRegistrations().isEmpty()) { + throw new DataValidationException("Client registrations should be specified!"); + } + for (OAuth2RegistrationInfo clientRegistration : oauth2Params.getClientRegistrations()) { + if (StringUtils.isEmpty(clientRegistration.getClientId())) { + throw new DataValidationException("Client ID should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientSecret())) { + throw new DataValidationException("Client secret should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getAuthorizationUri())) { + throw new DataValidationException("Authorization uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getAccessTokenUri())) { + throw new DataValidationException("Token uri should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getScope())) { + throw new DataValidationException("Scope should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { + throw new DataValidationException("User name attribute name should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getClientAuthenticationMethod())) { + throw new DataValidationException("Client authentication method should be specified!"); + } + if (StringUtils.isEmpty(clientRegistration.getLoginButtonLabel())) { + throw new DataValidationException("Login button label should be specified!"); + } + OAuth2MapperConfig mapperConfig = clientRegistration.getMapperConfig(); + if (mapperConfig == null) { + throw new DataValidationException("Mapper config should be specified!"); + } + if (mapperConfig.getType() == null) { + throw new DataValidationException("Mapper config type should be specified!"); + } + if (mapperConfig.getType() == MapperType.BASIC) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.GITHUB) { + OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic(); + if (basicConfig == null) { + throw new DataValidationException("Basic config should be specified!"); + } + if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) { + throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!"); + } + if (basicConfig.getTenantNameStrategy() == null) { + throw new DataValidationException("Tenant name strategy should be specified!"); + } + if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM + && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) { + throw new DataValidationException("Tenant name pattern should be specified!"); + } + } + if (mapperConfig.getType() == MapperType.CUSTOM) { + OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom(); + if (customConfig == null) { + throw new DataValidationException("Custom config should be specified!"); + } + if (StringUtils.isEmpty(customConfig.getUrl())) { + throw new DataValidationException("Custom mapper URL should be specified!"); + } + } + } + } + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index 2a59ccb829..c34875d014 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -15,19 +15,30 @@ */ package org.thingsboard.server.dao.oauth2; -import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationInfoId; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.OAuth2ParamsId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.deprecated.OAuth2ClientRegistrationInfoId; import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; +import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistration; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsDomainParams; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientsParams; import java.util.*; +import java.util.stream.Collectors; public class OAuth2Utils { public static final String OAUTH2_AUTHORIZATION_PATH_TEMPLATE = "/oauth2/authorization/%s"; - public static OAuth2ClientInfo toClientInfo(OAuth2ClientRegistrationInfo clientRegistrationInfo) { + public static OAuth2ClientInfo toClientInfo(OAuth2Registration registration) { OAuth2ClientInfo client = new OAuth2ClientInfo(); - client.setName(clientRegistrationInfo.getLoginButtonLabel()); - client.setUrl(String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, clientRegistrationInfo.getUuidId().toString())); - client.setIcon(clientRegistrationInfo.getLoginButtonIcon()); + client.setName(registration.getLoginButtonLabel()); + client.setUrl(String.format(OAUTH2_AUTHORIZATION_PATH_TEMPLATE, registration.getUuidId().toString())); + client.setIcon(registration.getLoginButtonIcon()); return client; } @@ -99,4 +110,130 @@ public class OAuth2Utils { clientRegistration.setDomainScheme(domainScheme); return clientRegistration; } + + public static OAuth2ParamsInfo toOAuth2ParamsInfo(List registrations, List domains, List mobiles) { + OAuth2ParamsInfo oauth2ParamsInfo = new OAuth2ParamsInfo(); + oauth2ParamsInfo.setClientRegistrations(registrations.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2RegistrationInfo).collect(Collectors.toList())); + oauth2ParamsInfo.setDomainInfos(domains.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2DomainInfo).collect(Collectors.toList())); + oauth2ParamsInfo.setMobileInfos(mobiles.stream().sorted(Comparator.comparing(BaseData::getUuidId)).map(OAuth2Utils::toOAuth2MobileInfo).collect(Collectors.toList())); + return oauth2ParamsInfo; + } + + public static OAuth2RegistrationInfo toOAuth2RegistrationInfo(OAuth2Registration registration) { + return OAuth2RegistrationInfo.builder() + .mapperConfig(registration.getMapperConfig()) + .clientId(registration.getClientId()) + .clientSecret(registration.getClientSecret()) + .authorizationUri(registration.getAuthorizationUri()) + .accessTokenUri(registration.getAccessTokenUri()) + .scope(registration.getScope()) + .platforms(registration.getPlatforms()) + .userInfoUri(registration.getUserInfoUri()) + .userNameAttributeName(registration.getUserNameAttributeName()) + .jwkSetUri(registration.getJwkSetUri()) + .clientAuthenticationMethod(registration.getClientAuthenticationMethod()) + .loginButtonLabel(registration.getLoginButtonLabel()) + .loginButtonIcon(registration.getLoginButtonIcon()) + .additionalInfo(registration.getAdditionalInfo()) + .build(); + } + + public static OAuth2DomainInfo toOAuth2DomainInfo(OAuth2Domain domain) { + return OAuth2DomainInfo.builder() + .name(domain.getDomainName()) + .scheme(domain.getDomainScheme()) + .build(); + } + + public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { + return OAuth2MobileInfo.builder() + .pkgName(mobile.getPkgName()) + .appSecret(mobile.getAppSecret()) + .build(); + } + + public static OAuth2Params infoToOAuth2Params(OAuth2Info oauth2Info) { + OAuth2Params oauth2Params = new OAuth2Params(); + oauth2Params.setEnabled(oauth2Info.isEnabled()); + oauth2Params.setTenantId(TenantId.SYS_TENANT_ID); + return oauth2Params; + } + + public static OAuth2Registration toOAuth2Registration(OAuth2ParamsId oauth2ParamsId, OAuth2RegistrationInfo registrationInfo) { + OAuth2Registration registration = new OAuth2Registration(); + registration.setOauth2ParamsId(oauth2ParamsId); + registration.setMapperConfig(registrationInfo.getMapperConfig()); + registration.setClientId(registrationInfo.getClientId()); + registration.setClientSecret(registrationInfo.getClientSecret()); + registration.setAuthorizationUri(registrationInfo.getAuthorizationUri()); + registration.setAccessTokenUri(registrationInfo.getAccessTokenUri()); + registration.setScope(registrationInfo.getScope()); + registration.setPlatforms(registrationInfo.getPlatforms()); + registration.setUserInfoUri(registrationInfo.getUserInfoUri()); + registration.setUserNameAttributeName(registrationInfo.getUserNameAttributeName()); + registration.setJwkSetUri(registrationInfo.getJwkSetUri()); + registration.setClientAuthenticationMethod(registrationInfo.getClientAuthenticationMethod()); + registration.setLoginButtonLabel(registrationInfo.getLoginButtonLabel()); + registration.setLoginButtonIcon(registrationInfo.getLoginButtonIcon()); + registration.setAdditionalInfo(registrationInfo.getAdditionalInfo()); + return registration; + } + + public static OAuth2Domain toOAuth2Domain(OAuth2ParamsId oauth2ParamsId, OAuth2DomainInfo domainInfo) { + OAuth2Domain domain = new OAuth2Domain(); + domain.setOauth2ParamsId(oauth2ParamsId); + domain.setDomainName(domainInfo.getName()); + domain.setDomainScheme(domainInfo.getScheme()); + return domain; + } + + public static OAuth2Mobile toOAuth2Mobile(OAuth2ParamsId oauth2ParamsId, OAuth2MobileInfo mobileInfo) { + OAuth2Mobile mobile = new OAuth2Mobile(); + mobile.setOauth2ParamsId(oauth2ParamsId); + mobile.setPkgName(mobileInfo.getPkgName()); + mobile.setAppSecret(mobileInfo.getAppSecret()); + return mobile; + } + + @Deprecated + public static OAuth2Info clientParamsToOAuth2Info(OAuth2ClientsParams clientsParams) { + OAuth2Info oauth2Info = new OAuth2Info(); + oauth2Info.setEnabled(clientsParams.isEnabled()); + oauth2Info.setOauth2ParamsInfos(clientsParams.getDomainsParams().stream().map(OAuth2Utils::clientsDomainParamsToOAuth2ParamsInfo).collect(Collectors.toList())); + return oauth2Info; + } + + private static OAuth2ParamsInfo clientsDomainParamsToOAuth2ParamsInfo(OAuth2ClientsDomainParams clientsDomainParams) { + OAuth2ParamsInfo oauth2ParamsInfo = new OAuth2ParamsInfo(); + oauth2ParamsInfo.setMobileInfos(Collections.emptyList()); + oauth2ParamsInfo.setClientRegistrations(clientsDomainParams.getClientRegistrations().stream().map(OAuth2Utils::clientRegistrationDtoToOAuth2RegistrationInfo).collect(Collectors.toList())); + oauth2ParamsInfo.setDomainInfos(clientsDomainParams.getDomainInfos().stream().map(OAuth2Utils::domainInfoToOAuth2DomainInfo).collect(Collectors.toList())); + return oauth2ParamsInfo; + } + + private static OAuth2RegistrationInfo clientRegistrationDtoToOAuth2RegistrationInfo(ClientRegistrationDto clientRegistrationDto) { + return OAuth2RegistrationInfo.builder() + .mapperConfig(clientRegistrationDto.getMapperConfig()) + .clientId(clientRegistrationDto.getClientId()) + .clientSecret(clientRegistrationDto.getClientSecret()) + .authorizationUri(clientRegistrationDto.getAuthorizationUri()) + .accessTokenUri(clientRegistrationDto.getAccessTokenUri()) + .scope(clientRegistrationDto.getScope()) + .userInfoUri(clientRegistrationDto.getUserInfoUri()) + .userNameAttributeName(clientRegistrationDto.getUserNameAttributeName()) + .jwkSetUri(clientRegistrationDto.getJwkSetUri()) + .clientAuthenticationMethod(clientRegistrationDto.getClientAuthenticationMethod()) + .loginButtonLabel(clientRegistrationDto.getLoginButtonLabel()) + .loginButtonIcon(clientRegistrationDto.getLoginButtonIcon()) + .additionalInfo(clientRegistrationDto.getAdditionalInfo()) + .platforms(Collections.emptyList()) + .build(); + } + + private static OAuth2DomainInfo domainInfoToOAuth2DomainInfo(DomainInfo domainInfo) { + return OAuth2DomainInfo.builder() + .name(domainInfo.getName()) + .scheme(domainInfo.getScheme()) + .build(); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationDao.java similarity index 83% rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java rename to dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationDao.java index 2f4f2c1be2..63b2c46034 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationDao.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.oauth2; +package org.thingsboard.server.dao.oauth2.deprecated; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistration; import org.thingsboard.server.dao.Dao; +@Deprecated public interface OAuth2ClientRegistrationDao extends Dao { void deleteAll(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationInfoDao.java similarity index 81% rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java rename to dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationInfoDao.java index 6fe93bacf4..9f44335602 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientRegistrationInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/deprecated/OAuth2ClientRegistrationInfoDao.java @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.oauth2; +package org.thingsboard.server.dao.oauth2.deprecated; -import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.dao.Dao; import java.util.List; -import java.util.Set; +@Deprecated public interface OAuth2ClientRegistrationInfoDao extends Dao { List findAll(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java new file mode 100644 index 0000000000..0d74821678 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2DomainDao.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2DomainEntity; +import org.thingsboard.server.dao.oauth2.OAuth2DomainDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2DomainDao extends JpaAbstractDao implements OAuth2DomainDao { + + private final OAuth2DomainRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2DomainEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findByOAuth2ParamsId(UUID oauth2ParamsId) { + return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); + } + +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java new file mode 100644 index 0000000000..6f216b23f2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2MobileDao.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2MobileEntity; +import org.thingsboard.server.dao.oauth2.OAuth2MobileDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2MobileDao extends JpaAbstractDao implements OAuth2MobileDao { + + private final OAuth2MobileRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2MobileEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findByOAuth2ParamsId(UUID oauth2ParamsId) { + return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); + } + +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java new file mode 100644 index 0000000000..2f28cb58f6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ParamsDao.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.dao.model.sql.OAuth2ParamsEntity; +import org.thingsboard.server.dao.oauth2.OAuth2ParamsDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2ParamsDao extends JpaAbstractDao implements OAuth2ParamsDao { + private final OAuth2ParamsRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2ParamsEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public void deleteAll() { + repository.deleteAll(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java new file mode 100644 index 0000000000..ef35371b6e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.OAuth2RegistrationEntity; +import org.thingsboard.server.dao.oauth2.OAuth2RegistrationDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class JpaOAuth2RegistrationDao extends JpaAbstractDao implements OAuth2RegistrationDao { + + private final OAuth2RegistrationRepository repository; + + @Override + protected Class getEntityClass() { + return OAuth2RegistrationEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return repository; + } + + @Override + public List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(List domainSchemes, String domainName, String pkgName, PlatformType platformType) { + return DaoUtil.convertDataList(repository.findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(domainSchemes, domainName, pkgName, + platformType != null ? "%" + platformType.name() + "%" : null)); + } + + @Override + public List findByOAuth2ParamsId(UUID oauth2ParamsId) { + return DaoUtil.convertDataList(repository.findByOauth2ParamsId(oauth2ParamsId)); + } + + @Override + public String findAppSecret(UUID id, String pkgName) { + return repository.findAppSecret(id, pkgName); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java new file mode 100644 index 0000000000..26d084f613 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2DomainRepository.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2DomainEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2DomainRepository extends CrudRepository { + + List findByOauth2ParamsId(UUID oauth2ParamsId); + +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java new file mode 100644 index 0000000000..d4660ef2ee --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2MobileRepository.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2MobileEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2MobileRepository extends CrudRepository { + + List findByOauth2ParamsId(UUID oauth2ParamsId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java new file mode 100644 index 0000000000..8e53b34e31 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ParamsRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.OAuth2ParamsEntity; + +import java.util.UUID; + +public interface OAuth2ParamsRepository extends CrudRepository { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java new file mode 100644 index 0000000000..5c6e34661f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2RegistrationRepository.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.oauth2; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.dao.model.sql.OAuth2RegistrationEntity; + +import java.util.List; +import java.util.UUID; + +public interface OAuth2RegistrationRepository extends CrudRepository { + + @Query("SELECT reg " + + "FROM OAuth2RegistrationEntity reg " + + "LEFT JOIN OAuth2ParamsEntity params on reg.oauth2ParamsId = params.id " + + "LEFT JOIN OAuth2DomainEntity domain on reg.oauth2ParamsId = domain.oauth2ParamsId " + + "WHERE params.enabled = true " + + "AND domain.domainName = :domainName " + + "AND domain.domainScheme IN (:domainSchemes) " + + "AND (:pkgName IS NULL OR EXISTS (SELECT mobile FROM OAuth2MobileEntity mobile WHERE mobile.oauth2ParamsId = reg.oauth2ParamsId AND mobile.pkgName = :pkgName)) " + + "AND (:platformFilter IS NULL OR reg.platforms IS NULL OR reg.platforms = '' OR reg.platforms LIKE :platformFilter)") + List findEnabledByDomainSchemesDomainNameAndPkgNameAndPlatformType(@Param("domainSchemes") List domainSchemes, + @Param("domainName") String domainName, + @Param("pkgName") String pkgName, + @Param("platformFilter") String platformFilter); + + List findByOauth2ParamsId(UUID oauth2ParamsId); + + @Query("SELECT mobile.appSecret " + + "FROM OAuth2MobileEntity mobile " + + "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + + "WHERE reg.id = :registrationId " + + "AND mobile.pkgName = :pkgName") + String findAppSecret(@Param("registrationId") UUID id, + @Param("pkgName") String pkgName); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationDao.java similarity index 82% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationDao.java index 8aea9c9c3f..8dfe92b18d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationDao.java @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.oauth2.deprecated; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistration; -import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; -import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationDao; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistration; +import org.thingsboard.server.dao.model.sql.deprecated.OAuth2ClientRegistrationEntity; +import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import java.util.UUID; +@Deprecated @Component @RequiredArgsConstructor public class JpaOAuth2ClientRegistrationDao extends JpaAbstractDao implements OAuth2ClientRegistrationDao { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationInfoDao.java similarity index 85% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationInfoDao.java index 0a25678e16..d37ca981f2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2ClientRegistrationInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/JpaOAuth2ClientRegistrationInfoDao.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.oauth2.deprecated; import lombok.RequiredArgsConstructor; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.oauth2.ExtendedOAuth2ClientRegistrationInfo; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; +import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; -import org.thingsboard.server.dao.oauth2.OAuth2ClientRegistrationInfoDao; +import org.thingsboard.server.dao.model.sql.deprecated.OAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import java.util.ArrayList; @@ -31,6 +31,7 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +@Deprecated @Component @RequiredArgsConstructor public class JpaOAuth2ClientRegistrationInfoDao extends JpaAbstractDao implements OAuth2ClientRegistrationInfoDao { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationInfoRepository.java similarity index 81% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationInfoRepository.java index 18ac3be05a..73a2ca9bba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationInfoRepository.java @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.oauth2.deprecated; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.oauth2.SchemeType; -import org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity; -import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.model.sql.deprecated.ExtendedOAuth2ClientRegistrationInfoEntity; +import org.thingsboard.server.dao.model.sql.deprecated.OAuth2ClientRegistrationInfoEntity; import java.util.List; import java.util.UUID; +@Deprecated public interface OAuth2ClientRegistrationInfoRepository extends CrudRepository { @Query("SELECT new OAuth2ClientRegistrationInfoEntity(cr_info) " + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + @@ -34,7 +35,7 @@ public interface OAuth2ClientRegistrationInfoRepository extends CrudRepository findAllByDomainSchemesAndName(@Param("domainSchemes") List domainSchemes, @Param("domainName") String domainName); - @Query("SELECT new org.thingsboard.server.dao.model.sql.ExtendedOAuth2ClientRegistrationInfoEntity(cr_info, cr.domainName, cr.domainScheme) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.deprecated.ExtendedOAuth2ClientRegistrationInfoEntity(cr_info, cr.domainName, cr.domainScheme) " + "FROM OAuth2ClientRegistrationInfoEntity cr_info " + "LEFT JOIN OAuth2ClientRegistrationEntity cr on cr_info.id = cr.clientRegistrationInfoId ") List findAllExtended(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationRepository.java similarity index 83% rename from dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationRepository.java index 1849f46514..1f4e4783ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/OAuth2ClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/deprecated/OAuth2ClientRegistrationRepository.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.sql.oauth2; +package org.thingsboard.server.dao.sql.oauth2.deprecated; import org.springframework.data.repository.CrudRepository; -import org.thingsboard.server.dao.model.sql.OAuth2ClientRegistrationEntity; +import org.thingsboard.server.dao.model.sql.deprecated.OAuth2ClientRegistrationEntity; import java.util.UUID; +@Deprecated public interface OAuth2ClientRegistrationRepository extends CrudRepository { } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 2c5113867a..ca7cd73604 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -374,16 +374,24 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary ( CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); -CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, +CREATE TABLE IF NOT EXISTS oauth2_params ( + id uuid NOT NULL CONSTRAINT oauth2_params_pkey PRIMARY KEY, enabled boolean, + tenant_id uuid, + created_time bigint NOT NULL +); + +CREATE TABLE IF NOT EXISTS oauth2_registration ( + id uuid NOT NULL CONSTRAINT oauth2_registration_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), + platforms varchar(255), user_info_uri varchar(255), user_name_attribute_name varchar(255), jwk_set_uri varchar(255), @@ -404,15 +412,28 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( custom_url varchar(255), custom_username varchar(255), custom_password varchar(255), - custom_send_token boolean + custom_send_token boolean, + CONSTRAINT fk_registration_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS oauth2_client_registration ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, +CREATE TABLE IF NOT EXISTS oauth2_domain ( + id uuid NOT NULL CONSTRAINT oauth2_domain_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, domain_name varchar(255), domain_scheme varchar(31), - client_registration_info_id uuid + CONSTRAINT fk_domain_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_domain_unq_key UNIQUE (oauth2_params_id, domain_name, domain_scheme) +); + +CREATE TABLE IF NOT EXISTS oauth2_mobile ( + id uuid NOT NULL CONSTRAINT oauth2_mobile_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, + created_time bigint NOT NULL, + pkg_name varchar(255), + app_secret varchar(2048), + CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) ); CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( @@ -443,6 +464,49 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) ); +-- Deprecated +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +-- Deprecated +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + CREATE TABLE IF NOT EXISTS api_usage_state ( id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3140ff83d6..303bf6f0f9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -411,16 +411,24 @@ CREATE TABLE IF NOT EXISTS ts_kv_dictionary CONSTRAINT ts_key_id_pkey PRIMARY KEY (key) ); -CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, +CREATE TABLE IF NOT EXISTS oauth2_params ( + id uuid NOT NULL CONSTRAINT oauth2_params_pkey PRIMARY KEY, enabled boolean, + tenant_id uuid, + created_time bigint NOT NULL +); + +CREATE TABLE IF NOT EXISTS oauth2_registration ( + id uuid NOT NULL CONSTRAINT oauth2_registration_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), + platforms varchar(255), user_info_uri varchar(255), user_name_attribute_name varchar(255), jwk_set_uri varchar(255), @@ -441,15 +449,28 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( custom_url varchar(255), custom_username varchar(255), custom_password varchar(255), - custom_send_token boolean + custom_send_token boolean, + CONSTRAINT fk_registration_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS oauth2_client_registration ( - id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, +CREATE TABLE IF NOT EXISTS oauth2_domain ( + id uuid NOT NULL CONSTRAINT oauth2_domain_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, domain_name varchar(255), domain_scheme varchar(31), - client_registration_info_id uuid + CONSTRAINT fk_domain_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_domain_unq_key UNIQUE (oauth2_params_id, domain_name, domain_scheme) +); + +CREATE TABLE IF NOT EXISTS oauth2_mobile ( + id uuid NOT NULL CONSTRAINT oauth2_mobile_pkey PRIMARY KEY, + oauth2_params_id uuid NOT NULL, + created_time bigint NOT NULL, + pkg_name varchar(255), + app_secret varchar(2048), + CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, + CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) ); CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( @@ -480,6 +501,49 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template ( CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id) ); +-- Deprecated +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY, + enabled boolean, + created_time bigint NOT NULL, + additional_info varchar, + client_id varchar(255), + client_secret varchar(255), + authorization_uri varchar(255), + token_uri varchar(255), + scope varchar(255), + user_info_uri varchar(255), + user_name_attribute_name varchar(255), + jwk_set_uri varchar(255), + client_authentication_method varchar(255), + login_button_label varchar(255), + login_button_icon varchar(255), + allow_user_creation boolean, + activate_user boolean, + type varchar(31), + basic_email_attribute_key varchar(31), + basic_first_name_attribute_key varchar(31), + basic_last_name_attribute_key varchar(31), + basic_tenant_name_strategy varchar(31), + basic_tenant_name_pattern varchar(255), + basic_customer_name_pattern varchar(255), + basic_default_dashboard_name varchar(255), + basic_always_full_screen boolean, + custom_url varchar(255), + custom_username varchar(255), + custom_password varchar(255), + custom_send_token boolean +); + +-- Deprecated +CREATE TABLE IF NOT EXISTS oauth2_client_registration ( + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY, + created_time bigint NOT NULL, + domain_name varchar(255), + domain_scheme varchar(31), + client_registration_info_id uuid +); + CREATE TABLE IF NOT EXISTS api_usage_state ( id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index ebfbfccd2a..4077bb2176 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -16,208 +16,227 @@ package org.thingsboard.server.dao.service; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.SchemeType; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2Service; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; public class BaseOAuth2ServiceTest extends AbstractServiceTest { - private static final OAuth2ClientsParams EMPTY_PARAMS = new OAuth2ClientsParams(false, new ArrayList<>()); + private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, Collections.emptyList()); @Autowired protected OAuth2Service oAuth2Service; @Before public void beforeRun() { - Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); + Assert.assertTrue(oAuth2Service.findAllRegistrations().isEmpty()); } @After public void after() { - oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); - Assert.assertTrue(oAuth2Service.findAllClientRegistrationInfos().isEmpty()); - Assert.assertTrue(oAuth2Service.findOAuth2Params().getDomainsParams().isEmpty()); + oAuth2Service.saveOAuth2Info(EMPTY_PARAMS); + Assert.assertTrue(oAuth2Service.findAllRegistrations().isEmpty()); + Assert.assertTrue(oAuth2Service.findOAuth2Info().getOauth2ParamsInfos().isEmpty()); } @Test(expected = DataValidationException.class) public void testSaveHttpAndMixedDomainsTogether() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); } @Test(expected = DataValidationException.class) public void testSaveHttpsAndMixedDomainsTogether() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), - DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build(), + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); } @Test public void testCreateAndFindParams() { - OAuth2ClientsParams clientsParams = createDefaultClientsParams(); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); // TODO ask if it's safe to check equality on AdditionalProperties - Assert.assertEquals(clientsParams, foundClientsParams); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); } @Test public void testDisableParams() { - OAuth2ClientsParams clientsParams = createDefaultClientsParams(); - clientsParams.setEnabled(true); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); - Assert.assertEquals(clientsParams, foundClientsParams); - - clientsParams.setEnabled(false); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundDisabledClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertEquals(clientsParams, foundDisabledClientsParams); + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + oAuth2Info.setEnabled(true); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); + + oAuth2Info.setEnabled(false); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundDisabledOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertEquals(oAuth2Info, foundDisabledOAuth2Info); } @Test public void testClearDomainParams() { - OAuth2ClientsParams clientsParams = createDefaultClientsParams(); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); - Assert.assertEquals(clientsParams, foundClientsParams); - - oAuth2Service.saveOAuth2Params(EMPTY_PARAMS); - OAuth2ClientsParams foundAfterClearClientsParams = oAuth2Service.findOAuth2Params(); + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); + + oAuth2Service.saveOAuth2Info(EMPTY_PARAMS); + OAuth2Info foundAfterClearClientsParams = oAuth2Service.findOAuth2Info(); Assert.assertNotNull(foundAfterClearClientsParams); Assert.assertEquals(EMPTY_PARAMS, foundAfterClearClientsParams); } @Test public void testUpdateClientsParams() { - OAuth2ClientsParams clientsParams = createDefaultClientsParams(); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); - Assert.assertEquals(clientsParams, foundClientsParams); - - OAuth2ClientsParams newClientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); + + OAuth2Info newOAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("another-domain").scheme(SchemeType.HTTPS).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto() + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("test-domain").scheme(SchemeType.MIXED).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto() + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(newClientsParams); - OAuth2ClientsParams foundAfterUpdateClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundAfterUpdateClientsParams); - Assert.assertEquals(newClientsParams, foundAfterUpdateClientsParams); + oAuth2Service.saveOAuth2Info(newOAuth2Info); + OAuth2Info foundAfterUpdateOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundAfterUpdateOAuth2Info); + Assert.assertEquals(newOAuth2Info, foundAfterUpdateOAuth2Info); } @Test public void testGetOAuth2Clients() { - List firstGroup = Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + List firstGroup = Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() ); - List secondGroup = Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto() + List secondGroup = Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo() ); - List thirdGroup = Lists.newArrayList( - validClientRegistrationDto() + List thirdGroup = Lists.newArrayList( + validRegistrationInfo() ); - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(firstGroup) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(secondGroup) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(thirdGroup) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); - Assert.assertEquals(clientsParams, foundClientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); List firstGroupClientInfos = firstGroup.stream() - .map(clientRegistrationDto -> new OAuth2ClientInfo( - clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .map(registrationInfo -> new OAuth2ClientInfo( + registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) .collect(Collectors.toList()); List secondGroupClientInfos = secondGroup.stream() - .map(clientRegistrationDto -> new OAuth2ClientInfo( - clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .map(registrationInfo -> new OAuth2ClientInfo( + registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) .collect(Collectors.toList()); List thirdGroupClientInfos = thirdGroup.stream() - .map(clientRegistrationDto -> new OAuth2ClientInfo( - clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .map(registrationInfo -> new OAuth2ClientInfo( + registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) .collect(Collectors.toList()); - List nonExistentDomainClients = oAuth2Service.getOAuth2Clients("http", "non-existent-domain"); + List nonExistentDomainClients = oAuth2Service.getOAuth2Clients("http", "non-existent-domain", null, null); Assert.assertTrue(nonExistentDomainClients.isEmpty()); - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); firstGroupClientInfos.forEach(firstGroupClientInfo -> { Assert.assertTrue( @@ -227,10 +246,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ); }); - List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain"); + List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain", null, null); Assert.assertTrue(firstDomainHttpsClients.isEmpty()); - List fourthDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "fourth-domain"); + List fourthDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "fourth-domain", null, null); Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpClients.size()); secondGroupClientInfos.forEach(secondGroupClientInfo -> { Assert.assertTrue( @@ -239,7 +258,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { && clientInfo.getName().equals(secondGroupClientInfo.getName())) ); }); - List fourthDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "fourth-domain"); + List fourthDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "fourth-domain", null, null); Assert.assertEquals(secondGroupClientInfos.size(), fourthDomainHttpsClients.size()); secondGroupClientInfos.forEach(secondGroupClientInfo -> { Assert.assertTrue( @@ -249,7 +268,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ); }); - List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); Assert.assertEquals(firstGroupClientInfos.size() + secondGroupClientInfos.size(), secondDomainHttpClients.size()); firstGroupClientInfos.forEach(firstGroupClientInfo -> { Assert.assertTrue( @@ -266,7 +285,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ); }); - List secondDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "second-domain"); + List secondDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "second-domain", null, null); Assert.assertEquals(firstGroupClientInfos.size() + thirdGroupClientInfos.size(), secondDomainHttpsClients.size()); firstGroupClientInfos.forEach(firstGroupClientInfo -> { Assert.assertTrue( @@ -286,34 +305,35 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetOAuth2ClientsForHttpAndHttps() { - List firstGroup = Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + List firstGroup = Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() ); - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTPS).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(firstGroup) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); - OAuth2ClientsParams foundClientsParams = oAuth2Service.findOAuth2Params(); - Assert.assertNotNull(foundClientsParams); - Assert.assertEquals(clientsParams, foundClientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertNotNull(foundOAuth2Info); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); List firstGroupClientInfos = firstGroup.stream() - .map(clientRegistrationDto -> new OAuth2ClientInfo( - clientRegistrationDto.getLoginButtonLabel(), clientRegistrationDto.getLoginButtonIcon(), null)) + .map(registrationInfo -> new OAuth2ClientInfo( + registrationInfo.getLoginButtonLabel(), registrationInfo.getLoginButtonIcon(), null)) .collect(Collectors.toList()); - List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain"); + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpClients.size()); firstGroupClientInfos.forEach(firstGroupClientInfo -> { Assert.assertTrue( @@ -323,7 +343,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ); }); - List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain"); + List firstDomainHttpsClients = oAuth2Service.getOAuth2Clients("https", "first-domain", null, null); Assert.assertEquals(firstGroupClientInfos.size(), firstDomainHttpsClients.size()); firstGroupClientInfos.forEach(firstGroupClientInfo -> { Assert.assertTrue( @@ -336,176 +356,285 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { @Test public void testGetDisabledOAuth2Clients() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); + oAuth2Service.saveOAuth2Info(oAuth2Info); - List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + List secondDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); Assert.assertEquals(5, secondDomainHttpClients.size()); - clientsParams.setEnabled(false); - oAuth2Service.saveOAuth2Params(clientsParams); + oAuth2Info.setEnabled(false); + oAuth2Service.saveOAuth2Info(oAuth2Info); - List secondDomainHttpDisabledClients = oAuth2Service.getOAuth2Clients("http", "second-domain"); + List secondDomainHttpDisabledClients = oAuth2Service.getOAuth2Clients("http", "second-domain", null, null); Assert.assertEquals(0, secondDomainHttpDisabledClients.size()); } @Test - public void testFindAllClientRegistrationInfos() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + public void testFindAllRegistrations() { + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto() + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); - List foundClientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); - Assert.assertEquals(6, foundClientRegistrationInfos.size()); - clientsParams.getDomainsParams().stream() - .flatMap(domainParams -> domainParams.getClientRegistrations().stream()) - .forEach(clientRegistrationDto -> + oAuth2Service.saveOAuth2Info(oAuth2Info); + List foundRegistrations = oAuth2Service.findAllRegistrations(); + Assert.assertEquals(6, foundRegistrations.size()); + oAuth2Info.getOauth2ParamsInfos().stream() + .flatMap(paramsInfo -> paramsInfo.getClientRegistrations().stream()) + .forEach(registrationInfo -> Assert.assertTrue( - foundClientRegistrationInfos.stream() - .anyMatch(clientRegistrationInfo -> clientRegistrationInfo.getClientId().equals(clientRegistrationDto.getClientId())) + foundRegistrations.stream() + .anyMatch(registration -> registration.getClientId().equals(registrationInfo.getClientId())) ) ); } @Test - public void testFindClientRegistrationById() { - OAuth2ClientsParams clientsParams = new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + public void testFindRegistrationById() { + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo() )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), - DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTPS).build(), + OAuth2DomainInfo.builder().name("fifth-domain").scheme(SchemeType.HTTP).build() )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto() + validRegistrationInfo() )) .build() )); - oAuth2Service.saveOAuth2Params(clientsParams); - List clientRegistrationInfos = oAuth2Service.findAllClientRegistrationInfos(); - clientRegistrationInfos.forEach(clientRegistrationInfo -> { - OAuth2ClientRegistrationInfo foundClientRegistrationInfo = oAuth2Service.findClientRegistrationInfo(clientRegistrationInfo.getUuidId()); - Assert.assertEquals(clientRegistrationInfo, foundClientRegistrationInfo); + oAuth2Service.saveOAuth2Info(oAuth2Info); + List foundRegistrations = oAuth2Service.findAllRegistrations(); + foundRegistrations.forEach(registration -> { + OAuth2Registration foundRegistration = oAuth2Service.findRegistration(registration.getUuidId()); + Assert.assertEquals(registration, foundRegistration); }); } - private OAuth2ClientsParams createDefaultClientsParams() { - return new OAuth2ClientsParams(true, Lists.newArrayList( - OAuth2ClientsDomainParams.builder() + @Test + public void testFindAppSecret() { + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .mobileInfos(Lists.newArrayList( + validMobileInfo("com.test.pkg1", "testPkg1AppSecret"), + validMobileInfo("com.test.pkg2", "testPkg2AppSecret") + )) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() + )) + .build(), + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .mobileInfos(Collections.emptyList()) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo() + )) + .build() + )); + oAuth2Service.saveOAuth2Info(oAuth2Info); + + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", null); + Assert.assertEquals(3, firstDomainHttpClients.size()); + for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { + String[] segments = clientInfo.getUrl().split("/"); + String registrationId = segments[segments.length-1]; + String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1"); + Assert.assertNotNull(appSecret); + Assert.assertEquals("testPkg1AppSecret", appSecret); + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2"); + Assert.assertNotNull(appSecret); + Assert.assertEquals("testPkg2AppSecret", appSecret); + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3"); + Assert.assertNull(appSecret); + } + } + + @Test + public void testFindClientsByPackageAndPlatform() { + OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .mobileInfos(Lists.newArrayList( + validMobileInfo("com.test.pkg1", "testPkg1Callback"), + validMobileInfo("com.test.pkg2", "testPkg2Callback") )) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), + validRegistrationInfo("Facebook", Arrays.asList(PlatformType.IOS)), + validRegistrationInfo("GitHub", Collections.emptyList()) )) .build(), - OAuth2ClientsDomainParams.builder() + OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( - DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), - DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() )) + .mobileInfos(Collections.emptyList()) .clientRegistrations(Lists.newArrayList( - validClientRegistrationDto(), - validClientRegistrationDto() + validRegistrationInfo(), + validRegistrationInfo() )) .build() )); + oAuth2Service.saveOAuth2Info(oAuth2Info); + + OAuth2Info foundOAuth2Info = oAuth2Service.findOAuth2Info(); + Assert.assertEquals(oAuth2Info, foundOAuth2Info); + + List firstDomainHttpClients = oAuth2Service.getOAuth2Clients("http", "first-domain", null, null); + Assert.assertEquals(3, firstDomainHttpClients.size()); + List pkg1Clients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", null); + Assert.assertEquals(3, pkg1Clients.size()); + List pkg1AndroidClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", PlatformType.ANDROID); + Assert.assertEquals(2, pkg1AndroidClients.size()); + Assert.assertTrue(pkg1AndroidClients.stream().anyMatch(client -> client.getName().equals("Google"))); + Assert.assertTrue(pkg1AndroidClients.stream().anyMatch(client -> client.getName().equals("GitHub"))); + List pkg1IOSClients = oAuth2Service.getOAuth2Clients("http", "first-domain", "com.test.pkg1", PlatformType.IOS); + Assert.assertEquals(2, pkg1IOSClients.size()); + Assert.assertTrue(pkg1IOSClients.stream().anyMatch(client -> client.getName().equals("Facebook"))); + Assert.assertTrue(pkg1IOSClients.stream().anyMatch(client -> client.getName().equals("GitHub"))); } - private ClientRegistrationDto validClientRegistrationDto() { - return ClientRegistrationDto.builder() + private OAuth2Info createDefaultOAuth2Info() { + return new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("first-domain").scheme(SchemeType.HTTP).build(), + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() + )) + .mobileInfos(Collections.emptyList()) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo(), + validRegistrationInfo() + )) + .build(), + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("second-domain").scheme(SchemeType.MIXED).build(), + OAuth2DomainInfo.builder().name("fourth-domain").scheme(SchemeType.MIXED).build() + )) + .mobileInfos(Collections.emptyList()) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo(), + validRegistrationInfo() + )) + .build() + )); + } + + private OAuth2RegistrationInfo validRegistrationInfo() { + return validRegistrationInfo(null, Collections.emptyList()); + } + + private OAuth2RegistrationInfo validRegistrationInfo(String label, List platforms) { + return OAuth2RegistrationInfo.builder() .clientId(UUID.randomUUID().toString()) .clientSecret(UUID.randomUUID().toString()) .authorizationUri(UUID.randomUUID().toString()) .accessTokenUri(UUID.randomUUID().toString()) .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .platforms(platforms == null ? Collections.emptyList() : platforms) .userInfoUri(UUID.randomUUID().toString()) .userNameAttributeName(UUID.randomUUID().toString()) .jwkSetUri(UUID.randomUUID().toString()) .clientAuthenticationMethod(UUID.randomUUID().toString()) - .loginButtonLabel(UUID.randomUUID().toString()) + .loginButtonLabel(label != null ? label : UUID.randomUUID().toString()) .loginButtonIcon(UUID.randomUUID().toString()) .additionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString())) .mapperConfig( @@ -522,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ) .build(); } + + private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) { + return OAuth2MobileInfo.builder().pkgName(pkgName) + .appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24)) + .build(); + } } diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 726b4ba412..f7b1b4118d 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -24,9 +24,13 @@ DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; +DROP TABLE IF EXISTS oauth2_mobile; +DROP TABLE IF EXISTS oauth2_domain; +DROP TABLE IF EXISTS oauth2_registration; +DROP TABLE IF EXISTS oauth2_params; +DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; -DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS ota_package; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 855a53df2d..a29dea43c2 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -25,9 +25,13 @@ DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS tb_schema_settings; +DROP TABLE IF EXISTS oauth2_mobile; +DROP TABLE IF EXISTS oauth2_domain; +DROP TABLE IF EXISTS oauth2_registration; +DROP TABLE IF EXISTS oauth2_params; +DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; -DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; DROP TABLE IF EXISTS firmware; diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java index 7ae7d5989d..534883f337 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -200,7 +200,6 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message); this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().packetId(), incomingQos2Publish); - message.payload().retain(); channel.writeAndFlush(pubrecMessage); } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 8ad95805a5..b716935b2a 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -103,7 +103,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; -import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -142,7 +142,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -1773,21 +1772,28 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }).getBody(); } - public List getOAuth2Clients() { + public List getOAuth2Clients(String pkgName) { + Map params = new HashMap<>(); + StringBuilder urlBuilder = new StringBuilder(baseURL); + urlBuilder.append("/api/noauth/oauth2Clients"); + if (pkgName != null) { + urlBuilder.append("?pkgName={pkgName}"); + params.put("pkgName", pkgName); + } return restTemplate.exchange( - baseURL + "/api/noauth/oauth2Clients", + urlBuilder.toString(), HttpMethod.POST, HttpEntity.EMPTY, new ParameterizedTypeReference>() { - }).getBody(); + }, params).getBody(); } - public OAuth2ClientsParams getCurrentOAuth2Params() { - return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2ClientsParams.class).getBody(); + public OAuth2Info getCurrentOAuth2Info() { + return restTemplate.getForEntity(baseURL + "/api/oauth2/config", OAuth2Info.class).getBody(); } - public OAuth2ClientsParams saveOAuth2Params(OAuth2ClientsParams oauth2Params) { - return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Params, OAuth2ClientsParams.class).getBody(); + public OAuth2Info saveOAuth2Info(OAuth2Info oauth2Info) { + return restTemplate.postForEntity(baseURL + "/api/oauth2/config", oauth2Info, OAuth2Info.class).getBody(); } public String getLoginProcessingUrl() { @@ -2965,7 +2971,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/otaPackage", otaPackageInfo, OtaPackageInfo.class).getBody(); } - public OtaPackage saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception { + public OtaPackageInfo saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception { HttpHeaders header = new HttpHeaders(); header.setContentType(MediaType.MULTIPART_FORM_DATA); @@ -2987,7 +2993,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } return restTemplate.postForEntity( - baseURL + url, requestEntity, OtaPackage.class, params + baseURL + url, requestEntity, OtaPackageInfo.class, params ).getBody(); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 445b9653a2..614cfd4b9c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -269,16 +270,22 @@ class AlarmState { private JsonNode createDetails(AlarmRuleState ruleState) { JsonNode alarmDetails; String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); + DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId(); - if (StringUtils.isNotEmpty(alarmDetailsStr)) { - for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { - EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); - if (entityKeyValue != null) { - alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); + if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) { + ObjectNode newDetails = JacksonUtil.newObjectNode(); + if (StringUtils.isNotEmpty(alarmDetailsStr)) { + for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { + EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); + if (entityKeyValue != null) { + alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); + } } + newDetails.put("data", alarmDetailsStr); + } + if (dashboardId != null) { + newDetails.put("dashboardId", dashboardId.getId().toString()); } - ObjectNode newDetails = JacksonUtil.newObjectNode(); - newDetails.put("data", alarmDetailsStr); alarmDetails = newDetails; } else if (currentAlarm != null) { alarmDetails = currentAlarm.getDetails(); diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 39acda4590..d81b28a5ff 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -114,13 +114,14 @@ transport: private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" + skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" bootstrap: enable: "${LWM2M_ENABLED_BS:true}" id: "${LWM2M_SERVER_ID_BS:111}" bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" bind_port: "${LWM2M_BIND_PORT_BS:5687}" security: - bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_address: "${LWM2M_BIND_ADDRESS_SECURITY_BS:0.0.0.0}" bind_port: "${LWM2M_BIND_PORT_SECURITY_BS:5688}" # Only for RPK: Public & Private Key. If the keystore file is missing or not working public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 3ad4038066..39d51ced92 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -89,6 +89,13 @@ export class AppComponent implements OnInit { ) ); + this.matIconRegistry.addSvgIconLiteral( + 'apple-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 02c1c8649a..971f037282 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -16,16 +16,17 @@ import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; import { - AggregationType, calculateIntervalComparisonEndTime, - calculateIntervalEndTime, calculateIntervalStartEndTime, + AggregationType, + calculateIntervalComparisonEndTime, + calculateIntervalEndTime, + calculateIntervalStartEndTime, getCurrentTime, - getCurrentTimeForComparison, getTime, + getTime, SubscriptionTimewindow } from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; -import { deepClone } from '@core/utils'; +import { deepClone, isNumeric } from '@core/utils'; import Timeout = NodeJS.Timeout; -import * as moment_ from 'moment'; export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; @@ -407,24 +408,11 @@ export class DataAggregator { } } - private isNumeric(val: any): boolean { - return (val - parseFloat( val ) + 1) >= 0; - } - private convertValue(val: string): any { - if (!this.noAggregation || val && this.isNumeric(val)) { + if (!this.noAggregation || val && isNumeric(val) && Number(val).toString() === val) { return Number(val); - } else { - return val; - } - } - - private getCurrentTime() { - if (this.subsTw.timeForComparison) { - return getCurrentTimeForComparison(this.subsTw.timeForComparison as moment_.unitOfTime.DurationConstructor, this.subsTw.timezone); - } else { - return getCurrentTime(this.subsTw.timezone); } + return val; } } diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 9c769f3777..ff828cc2ac 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -38,7 +38,7 @@ import { } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; import { EntityDataListener, EntityDataLoadResult } from '@core/api/entity-data.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isObject, objectHashCode } from '@core/utils'; +import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isObject, objectHashCode } from '@core/utils'; import { PageData } from '@shared/models/page/page-data'; import { DataAggregator } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @@ -667,8 +667,7 @@ export class EntityDataSubscription { if (prevDataCb) { dataAggregator.updateOnDataCb(prevDataCb); } - } - if (!this.history && !isUpdate) { + } else if (!this.history && !isUpdate) { this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, dataUpdatedCb); } } @@ -743,16 +742,11 @@ export class EntityDataSubscription { } } - private isNumeric(val: any): boolean { - return (val - parseFloat( val ) + 1) >= 0; - } - private convertValue(val: string): any { - if (val && this.isNumeric(val) && Number(val).toString() === val) { + if (val && isNumeric(val) && Number(val).toString() === val) { return Number(val); - } else { - return val; } + return val; } private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index c17d993bf7..42b213f558 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -44,8 +44,8 @@ import { AdminService } from '@core/http/admin.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; -import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; -import { isMobileApp } from '@core/utils'; +import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; +import { isDefinedAndNotNull, isMobileApp } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -205,7 +205,8 @@ export class AuthService { } public loadOAuth2Clients(): Observable> { - return this.http.post>(`/api/noauth/oauth2Clients`, + const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; + return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), tap((OAuth2Clients) => { diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 7e3183255a..6107a0853e 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -14,24 +14,29 @@ /// limitations under the License. /// -import {Injectable} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import {PageLink} from '@shared/models/page/page-link'; -import {defaultHttpOptionsFromConfig, RequestConfig} from './http-utils'; -import {Observable} from 'rxjs'; -import {PageData} from '@shared/models/page/page-data'; -import {DeviceProfile, DeviceProfileInfo, DeviceTransportType} from '@shared/models/device.models'; -import {isDefinedAndNotNull, isEmptyStr} from '@core/utils'; -import {ObjectLwM2M, ServerSecurityConfig} from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; -import {SortOrder} from '@shared/models/page/sort-order'; +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, of, throwError } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; +import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; +import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models'; +import { SortOrder } from '@shared/models/page/sort-order'; +import { OtaPackageService } from '@core/http/ota-package.service'; +import { mergeMap, tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class DeviceProfileService { + private lwm2mBootstrapSecurityInfoInMemoryCache = new Map(); + constructor( - private http: HttpClient + private http: HttpClient, + private otaPackageService: OtaPackageService ) { } @@ -55,12 +60,18 @@ export class DeviceProfileService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } - public getLwm2mBootstrapSecurityInfo(securityMode: string, bootstrapServerIs: boolean, - config?: RequestConfig): Observable { - return this.http.get( - `/api/lwm2m/deviceProfile/bootstrap/${securityMode}/${bootstrapServerIs}`, - defaultHttpOptionsFromConfig(config) - ); + public getLwm2mBootstrapSecurityInfo(isBootstrapServer: boolean, config?: RequestConfig): Observable { + const securityConfig = this.lwm2mBootstrapSecurityInfoInMemoryCache.get(isBootstrapServer); + if (securityConfig) { + return of(securityConfig); + } else { + return this.http.get( + `/api/lwm2m/deviceProfile/bootstrap/${isBootstrapServer}`, + defaultHttpOptionsFromConfig(config) + ).pipe( + tap(serverConfig => this.lwm2mBootstrapSecurityInfoInMemoryCache.set(isBootstrapServer, serverConfig)) + ); + } } public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { @@ -70,6 +81,13 @@ export class DeviceProfileService { ); } + public saveDeviceProfileAndConfirmOtaChange(originDeviceProfile: DeviceProfile, deviceProfile: DeviceProfile, + config?: RequestConfig): Observable { + return this.otaPackageService.confirmDialogUpdatePackage(deviceProfile, originDeviceProfile).pipe( + mergeMap((update) => update ? this.saveDeviceProfile(deviceProfile, config) : throwError('Canceled saving device profiles')) + ); + } + public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable { return this.http.post('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts index e81d472130..72a6bc77af 100644 --- a/ui-ngx/src/app/core/http/oauth2.service.ts +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -18,7 +18,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { OAuth2ClientRegistrationTemplate, OAuth2ClientsParams } from '@shared/models/oauth2.models'; +import { OAuth2ClientRegistrationTemplate, OAuth2Info } from '@shared/models/oauth2.models'; @Injectable({ providedIn: 'root' @@ -29,16 +29,16 @@ export class OAuth2Service { private http: HttpClient ) { } - public getOAuth2Settings(config?: RequestConfig): Observable { - return this.http.get(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); + public getOAuth2Settings(config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); } public getOAuth2Template(config?: RequestConfig): Observable> { return this.http.get>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config)); } - public saveOAuth2Settings(OAuth2Setting: OAuth2ClientsParams, config?: RequestConfig): Observable { - return this.http.post('/api/oauth2/config', OAuth2Setting, + public saveOAuth2Settings(OAuth2Setting: OAuth2Info, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/config', OAuth2Setting, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/ota-package.service.ts b/ui-ngx/src/app/core/http/ota-package.service.ts index 6afeb6e78d..1f9a36882a 100644 --- a/ui-ngx/src/app/core/http/ota-package.service.ts +++ b/ui-ngx/src/app/core/http/ota-package.service.ts @@ -18,18 +18,30 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; -import { ChecksumAlgorithm, OtaPackage, OtaPackageInfo, OtaUpdateType } from '@shared/models/ota-package.models'; +import { + ChecksumAlgorithm, + OtaPackage, + OtaPackageInfo, + OtaPagesIds, + OtaUpdateType +} from '@shared/models/ota-package.models'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { deepClone } from '@core/utils'; +import { BaseData } from '@shared/models/base-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '@core/services/dialog.service'; @Injectable({ providedIn: 'root' }) export class OtaPackageService { constructor( - private http: HttpClient + private http: HttpClient, + private translate: TranslateService, + private dialogService: DialogService ) { } @@ -39,7 +51,7 @@ export class OtaPackageService { } public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType, - hasData = true, config?: RequestConfig): Observable> { + config?: RequestConfig): Observable> { const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`; return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } @@ -120,4 +132,36 @@ export class OtaPackageService { return this.http.delete(`/api/otaPackage/${otaPackageId}`, defaultHttpOptionsFromConfig(config)); } + public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, entityId: EntityId, config?: RequestConfig): Observable { + return this.http.get(`/api/devices/count/${type}?deviceProfileId=${entityId.id}`, defaultHttpOptionsFromConfig(config)); + } + + public confirmDialogUpdatePackage(entity: BaseData&OtaPagesIds, + originEntity: BaseData&OtaPagesIds): Observable { + const tasks: Observable[] = []; + if (originEntity?.id?.id && originEntity.firmwareId?.id !== entity.firmwareId?.id) { + tasks.push(this.countUpdateDeviceAfterChangePackage(OtaUpdateType.FIRMWARE, entity.id)); + } else { + tasks.push(of(0)); + } + if (originEntity?.id?.id && originEntity.softwareId?.id !== entity.softwareId?.id) { + tasks.push(this.countUpdateDeviceAfterChangePackage(OtaUpdateType.SOFTWARE, entity.id)); + } else { + tasks.push(of(0)); + } + return forkJoin(tasks).pipe( + mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => { + let text = ''; + if (deviceFirmwareUpdate > 0) { + text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}); + } + if (deviceSoftwareUpdate > 0) { + text += text.length ? ' ' : ''; + text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate}); + } + return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true); + }) + ); + } + } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index f667fb5788..0dc143e6cb 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -231,7 +231,6 @@ export class DashboardUtilsService { private createDefaultGridSettings(): GridSettings { return { backgroundColor: '#eeeeee', - color: 'rgba(0,0,0,0.870588)', columns: 24, margin: 10, backgroundSizeMode: '100%' diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 800651356a..cc940d6f85 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -27,6 +27,7 @@ import { AuthService } from '@core/auth/auth.service'; const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler'; const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler'; +const dashboardLayoutHandler = 'tbMobileDashboardLayoutHandler'; const navigationHandler = 'tbMobileNavigationHandler'; const mobileHandler = 'tbMobileHandler'; @@ -43,6 +44,7 @@ export class MobileService { private reloadUserObservable: Observable; private lastDashboardId: string; + private toggleLayoutFunction: () => void; constructor(@Inject(WINDOW) private window: Window, private router: Router, @@ -65,12 +67,26 @@ export class MobileService { } } - public onDashboardLoaded() { + public onDashboardLoaded(hasRightLayout: boolean, rightLayoutOpened: boolean) { if (this.mobileApp) { - this.mobileChannel.callHandler(dashboardLoadedHandler); + this.mobileChannel.callHandler(dashboardLoadedHandler, hasRightLayout, rightLayoutOpened); } } + public onDashboardRightLayoutChanged(opened: boolean) { + if (this.mobileApp) { + this.mobileChannel.callHandler(dashboardLayoutHandler, opened); + } + } + + public registerToggleLayoutFunction(toggleLayoutFunction: () => void) { + this.toggleLayoutFunction = toggleLayoutFunction; + } + + public unregisterToggleLayoutFunction() { + this.toggleLayoutFunction = null; + } + public handleWidgetMobileAction(type: WidgetMobileActionType, ...args: any[]): Observable> { if (this.mobileApp) { @@ -110,6 +126,11 @@ export class MobileService { const reloadUserMessage: ReloadUserMessage = message.data; this.reloadUser(reloadUserMessage); break; + case 'toggleDashboardLayout': + if (this.toggleLayoutFunction) { + this.toggleLayoutFunction(); + } + break; } } } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index a287802ec2..aa52c55cbe 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -445,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean { export function isMobileApp(): boolean { return isDefined((window as any).flutter_inappwebview); } + +const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const alphanumericCharactersLength = alphanumericCharacters.length; + +export function randomAlphanumeric(length: number): string { + let result = ''; + for ( let i = 0; i < length; i++ ) { + result += alphanumericCharacters.charAt(Math.floor(Math.random() * alphanumericCharactersLength)); + } + return result; +} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts index 08119e6ce8..aeb0f9c286 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts @@ -98,24 +98,17 @@ export class EntityAliasesDialogComponent extends DialogComponent { if (widget.type === widgetType.rpc) { if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { - const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; - widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; - if (!widgetsTitleList) { - widgetsTitleList = []; - this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; - } - widgetsTitleList.push(widget.config.title); + this.addWidgetTitleToWidgetsMap(widget.config.targetDeviceAliasIds[0], widget.config.title); + } + } else if (widget.type === widgetType.alarm) { + if (widget.config.alarmSource) { + this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title); } } else { const datasources = this.utils.validateDatasources(widget.config.datasources); datasources.forEach((datasource) => { if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { - widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; - if (!widgetsTitleList) { - widgetsTitleList = []; - this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList; - } - widgetsTitleList.push(widget.config.title); + this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); } }); } @@ -141,6 +134,15 @@ export class EntityAliasesDialogComponent extends DialogComponent = this.aliasToWidgetsMap[aliasId]; + if (!widgetsTitleList) { + widgetsTitleList = []; + this.aliasToWidgetsMap[aliasId] = widgetsTitleList; + } + widgetsTitleList.push(widgetTitle); + } + private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { const aliasFormControl = this.fb.group({ id: [aliasId], diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 89629bfe49..cc19e98a99 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -149,8 +149,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @Input() currentState: string; + private hideToolbarValue = false; + @Input() - hideToolbar: boolean; + set hideToolbar(hideToolbar: boolean) { + this.hideToolbarValue = hideToolbar; + } + + get hideToolbar(): boolean { + return (this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit; + } @Input() syncStateWithQueryParam = true; @@ -269,6 +277,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC return !this.widgetEditMode && !this.hideToolbar && (this.toolbarAlwaysOpen() || this.isToolbarOpened || this.isEdit || this.showRightLayoutSwitch()); } + set toolbarOpened(toolbarOpened: boolean) { } @@ -329,7 +338,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboardCtx.aliasController.updateAliases(); setTimeout(() => { this.mobileService.handleDashboardStateName(this.dashboardCtx.stateController.getCurrentStateName()); - this.mobileService.onDashboardLoaded(); + this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened); }); } } @@ -340,6 +349,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.isMobile = !state.matches; } )); + if (this.isMobileApp) { + this.mobileService.registerToggleLayoutFunction(() => { + setTimeout(() => { + this.toggleLayouts(); + this.cd.detectChanges(); + }); + }); + } } private init(data: any) { @@ -430,6 +447,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } ngOnDestroy(): void { + if (this.isMobileApp) { + this.mobileService.unregisterToggleLayoutFunction(); + } this.rxSubscriptions.forEach((subscription) => { subscription.unsubscribe(); }); @@ -469,6 +489,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } + private hideToolbarSetting(): boolean { + if (this.dashboard.configuration.settings && + isDefined(this.dashboard.configuration.settings.hideToolbar)) { + return this.dashboard.configuration.settings.hideToolbar; + } else { + return false; + } + } + public displayTitle(): boolean { if (this.dashboard.configuration.settings && isDefined(this.dashboard.configuration.settings.showTitle)) { @@ -546,15 +575,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } public showRightLayoutSwitch(): boolean { - return this.isMobile && this.layouts.right.show; + return this.isMobile && !this.isMobileApp && this.layouts.right.show; } public toggleLayouts() { this.isRightLayoutOpened = !this.isRightLayoutOpened; + this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened); } public openRightLayout() { this.isRightLayoutOpened = true; + this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened); } public mainLayoutWidth(): string { @@ -782,7 +813,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.updateLayouts(layoutsData); } setTimeout(() => { - this.mobileService.onDashboardLoaded(); + this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened); }); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html index f84405faed..14c2df301b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -

+

{{settings ? 'dashboard.settings' : 'layout.settings'}}

@@ -28,8 +28,8 @@
-
-
+
+
dashboard.state-controller @@ -38,112 +38,116 @@ -
- - {{ 'dashboard.toolbar-always-open' | translate }} - - +
+ dashboard.title-settings + {{ 'dashboard.display-title' | translate }} - + -
-
- +
+
+ dashboard.dashboard-logo-settings + + {{ 'dashboard.display-dashboard-logo' | translate }} + + + +
+
+ dashboard.toolbar-settings + + {{ 'dashboard.hide-toolbar' | translate }} + + + {{ 'dashboard.toolbar-always-open' | translate }} + + {{ 'dashboard.display-dashboards-selection' | translate }} - - + + {{ 'dashboard.display-entities-selection' | translate }} - - + + {{ 'dashboard.display-filters' | translate }} - - + + {{ 'dashboard.display-dashboard-timewindow' | translate }} - - + + {{ 'dashboard.display-dashboard-export' | translate }} - - + + {{ 'dashboard.display-update-dashboard-image' | translate }} - -
- - {{ 'dashboard.display-dashboard-logo' | translate }} - - - + +
-
- - - - dashboard.columns-count - - - {{ 'dashboard.columns-count-required' | translate }} - - - {{ 'dashboard.min-columns-count-message' | translate }} - - - {{ 'dashboard.max-columns-count-message' | translate }} - - - - dashboard.widgets-margins - - - {{ 'dashboard.margin-required' | translate }} - - - {{ 'dashboard.min-margin-message' | translate }} - - - {{ 'dashboard.max-margin-message' | translate }} - - - - {{ 'dashboard.autofill-height' | translate }} - - - - - - - dashboard.background-size-mode - - Fit width - Fit height - Cover - Contain - Original size - - - dashboard.mobile-layout -
- +
+
+ dashboard.layout-settings + + dashboard.columns-count + + + {{ 'dashboard.columns-count-required' | translate }} + + + {{ 'dashboard.min-columns-count-message' | translate }} + + + {{ 'dashboard.max-columns-count-message' | translate }} + + + + dashboard.widgets-margins + + + {{ 'dashboard.margin-required' | translate }} + + + {{ 'dashboard.min-margin-message' | translate }} + + + {{ 'dashboard.max-margin-message' | translate }} + + + + {{ 'dashboard.autofill-height' | translate }} + + + + + + + dashboard.background-size-mode + + Fit width + Fit height + Cover + Contain + Original size + + +
+
+ dashboard.mobile-layout + {{ 'dashboard.autofill-height' | translate }} - + dashboard.mobile-row-height -
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss index f588cb8894..5587d9c35a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.scss @@ -14,13 +14,20 @@ * limitations under the License. */ :host { - small { - white-space: normal; + .fields-group { + padding: 0 8px 8px; + margin: 10px 0; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } } } :host ::ng-deep { - .mat-checkbox-label { + .mat-slide-toggle-content { white-space: normal; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts index 26ab9a3e3b..5d17d33eaa 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts @@ -71,19 +71,33 @@ export class DashboardSettingsDialogComponent extends DialogComponent { @@ -92,13 +106,52 @@ export class DashboardSettingsDialogComponent extends DialogComponent { + if (showTitleValue) { + this.settingsFormGroup.get('titleColor').enable(); + } else { + this.settingsFormGroup.get('titleColor').disable(); + } + } + ); + this.settingsFormGroup.get('showDashboardLogo').valueChanges.subscribe( + (showDashboardLogoValue: boolean) => { + if (showDashboardLogoValue) { + this.settingsFormGroup.get('dashboardLogoUrl').enable(); + } else { + this.settingsFormGroup.get('dashboardLogoUrl').disable(); + } + } + ); + this.settingsFormGroup.get('hideToolbar').valueChanges.subscribe( + (hideToolbarValue: boolean) => { + if (hideToolbarValue) { + this.settingsFormGroup.get('toolbarAlwaysOpen').disable(); + this.settingsFormGroup.get('showDashboardsSelect').disable(); + this.settingsFormGroup.get('showEntitiesSelect').disable(); + this.settingsFormGroup.get('showFilters').disable(); + this.settingsFormGroup.get('showDashboardTimewindow').disable(); + this.settingsFormGroup.get('showDashboardExport').disable(); + this.settingsFormGroup.get('showUpdateDashboardImage').disable(); + } else { + this.settingsFormGroup.get('toolbarAlwaysOpen').enable(); + this.settingsFormGroup.get('showDashboardsSelect').enable(); + this.settingsFormGroup.get('showEntitiesSelect').enable(); + this.settingsFormGroup.get('showFilters').enable(); + this.settingsFormGroup.get('showDashboardTimewindow').enable(); + this.settingsFormGroup.get('showDashboardExport').enable(); + this.settingsFormGroup.get('showUpdateDashboardImage').enable(); + } + } + ); } else { this.settingsFormGroup = this.fb.group({}); } if (this.gridSettings) { + const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight; this.gridSettingsFormGroup = this.fb.group({ - color: [this.gridSettings.color || 'rgba(0,0,0,0.870588)', []], columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1000)]], margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10, [Validators.required, Validators.min(0), Validators.max(50)]], @@ -106,10 +159,19 @@ export class DashboardSettingsDialogComponent extends DialogComponent { + if (mobileAutoFillHeightValue) { + this.gridSettingsFormGroup.get('mobileRowHeight').disable(); + } else { + this.gridSettingsFormGroup.get('mobileRowHeight').enable(); + } + } + ); } else { this.gridSettingsFormGroup = this.fb.group({}); } @@ -133,10 +195,10 @@ export class DashboardSettingsDialogComponent extends DialogComponent
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts index a21ce9ec78..e399e74529 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts @@ -275,6 +275,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp } private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { + update = update && this.dashboardCtrl.dashboardCtx.state !== stateId; this.dashboardCtrl.openDashboardState(stateId, openRightLayout); this.mobileService.handleDashboardStateName(this.getStateName(this.stateObject.length - 1)); if (update) { diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index 58dea3b6f6..c48030050d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -63,7 +63,7 @@
- + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 40a6b0cece..84c421c5c1 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -43,7 +43,7 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; import { - CellActionDescriptor, + CellActionDescriptor, CellActionDescriptorType, EntityActionTableColumn, EntityColumn, EntityTableColumn, @@ -104,6 +104,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn timewindow: Timewindow; dataSource: EntitiesDataSource>; + cellActionType = CellActionDescriptorType; + isDetailsOpen = false; detailsPanelOpened = new EventEmitter(); @@ -526,7 +528,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellContentCache[index]; - if (!res) { + if (isUndefined(res)) { res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); this.cellContentCache[index] = res; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index cba32ddcaf..d6378620f9 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV editingEntity.additionalInfo = mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo); } - this.entitiesTableConfig.saveEntity(editingEntity).subscribe( + this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( (entity) => { this.entity = entity; this.entityComponent.entity = entity; diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 444aa8fb49..c546cc8912 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -46,8 +46,9 @@ formControlName="defaultRuleChainId"> +
{{'device-profile.mobile-dashboard-hint' | translate}}
{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}
+
+ + {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} + + +
{{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}}
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss index a0b8a83cd4..d973f261b7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -18,16 +18,24 @@ .row { margin-top: 1em; } - .tb-alarm-rule-details { + .tb-alarm-rule-details, .tb-alarm-rule-dashboard { padding: 4px; - cursor: pointer; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; &.title { opacity: 0.7; overflow: visible; } } + .tb-alarm-rule-details { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + } + .tb-alarm-rule-dashboard { + &.dashboard { + width: 100%; + max-width: 350px; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index d664c5c2a4..76bfa7698e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -34,6 +34,7 @@ import { EditAlarmDetailsDialogData } from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; import { EntityId } from '@shared/models/id/entity-id'; +import { DashboardId } from '@shared/models/id/dashboard-id'; @Component({ selector: 'tb-alarm-rule', @@ -92,7 +93,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat this.alarmRuleFormGroup = this.fb.group({ condition: [null, [Validators.required]], schedule: [null], - alarmDetails: [null] + alarmDetails: [null], + dashboardId: [null] }); this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.updateModel(); @@ -110,7 +112,11 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat writeValue(value: AlarmRule): void { this.modelValue = value; - this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.reset(model || undefined, {emitEvent: false}); } public openEditDetailsDialog($event: Event) { @@ -143,7 +149,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat private updateModel() { const value = this.alarmRuleFormGroup.value; if (this.modelValue) { - this.modelValue = {...this.modelValue, ...value}; + this.modelValue = {...this.modelValue, ...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null}; this.propagateChange(this.modelValue); } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index bd6f92b175..20f271d660 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -66,5 +66,5 @@ {{ 'device-profile.device-profile-required' | translate }} - {{ hint | translate }} + {{ hint | translate }} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts index 0efcad9608..58433f01c4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts @@ -90,7 +90,7 @@ export class DeviceProfileDialogComponent extends this.submitted = true; if (this.deviceProfileComponent.entityForm.valid) { this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()}; - this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe( + this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(this.deviceProfile, this.deviceProfile).subscribe( (deviceProfile) => { this.dialogRef.close(deviceProfile); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 5fa6c1acdb..83bba2a266 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -60,8 +60,9 @@ formControlName="defaultRuleChainId"> +
{{'device-profile.mobile-dashboard-hint' | translate}}
{ + this.coapDeviceProfileTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.coapDeviceType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(coapDeviceType => { this.updateCoapDeviceTypeBasedControls(coapDeviceType, true); }); - this.coapDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.coapDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + get coapDeviceTypeDefault(): boolean { const coapDeviceType = this.coapDeviceProfileTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.coapDeviceType').value; return coapDeviceType === CoapTransportDeviceType.DEFAULT; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts index b95433d096..27dda6dc91 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts @@ -14,13 +14,15 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; import { deepClone } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-device-profile-configuration', @@ -32,12 +34,14 @@ import { deepClone } from '@core/utils'; multi: true }] }) -export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { +export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { deviceProfileType = DeviceProfileType; deviceProfileConfigurationFormGroup: FormGroup; + private destroy$ = new Subject(); + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -69,11 +73,18 @@ export class DeviceProfileConfigurationComponent implements ControlValueAccessor this.deviceProfileConfigurationFormGroup = this.fb.group({ configuration: [null, Validators.required] }); - this.deviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.deviceProfileConfigurationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html index fdc02f910a..78f08ce31a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html @@ -15,112 +15,82 @@ limitations under the License. --> -
-
-
-
- - {{ 'device-profile.lwm2m.mode' | translate }} - - - {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} - - - - - {{ 'device-profile.lwm2m.server-host' | translate }} - - - {{ 'device-profile.lwm2m.server-host' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.server-port' | translate }} - - - {{ 'device-profile.lwm2m.server-port' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.short-id' | translate }} - - - {{ 'device-profile.lwm2m.short-id' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - -
-
-
-
- - {{ 'device-profile.lwm2m.client-hold-off-time' | translate }} - - - {{ 'device-profile.lwm2m.client-hold-off-time' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.bootstrap-server-account-timeout' | translate }} - - - {{ 'device-profile.lwm2m.bootstrap-server-account-timeout' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.bootstrap-server' | translate }} - -
-
- - {{ 'device-profile.lwm2m.server-public-key' | translate }} - - {{serverPublicKey.value?.length || 0}}/{{lenMaxServerPublicKey}} - - {{ 'device-profile.lwm2m.server-public-key' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - {{ 'device-profile.lwm2m.server-public-key' | translate }} - {{ 'device-profile.lwm2m.pattern_hex_dec' | translate: { - count: 0} }} - - - {{ 'device-profile.lwm2m.server-public-key' | translate }} - {{ 'device-profile.lwm2m.pattern_hex_dec' | translate: { - count: lenMaxServerPublicKey } }} - - -
-
+
+
+ + {{ 'device-profile.lwm2m.mode' | translate }} + + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} + + + + + {{ 'device-profile.lwm2m.server-host' | translate }} + + + {{ 'device-profile.lwm2m.server-host-required' | translate }} + + + + {{ 'device-profile.lwm2m.server-port' | translate }} + + + {{ 'device-profile.lwm2m.server-port-required' | translate }} + + +
+
+ + {{ 'device-profile.lwm2m.short-id' | translate }} + + + {{ 'device-profile.lwm2m.short-id-required' | translate }} + + + + {{ 'device-profile.lwm2m.client-hold-off-time' | translate }} + + + {{ 'device-profile.lwm2m.client-hold-off-time-required' | translate }} + + + + {{ 'device-profile.lwm2m.account-after-timeout' | translate }} + + + {{ 'device-profile.lwm2m.account-after-timeout-required' | translate }} + + +
+
+ + {{ 'device-profile.lwm2m.server-public-key' | translate }} + + {{serverPublicKey.value?.length || 0}}/{{maxLengthPublicKey}} + + {{ 'device-profile.lwm2m.server-public-key-required' | translate }} + + + {{ 'device-profile.lwm2m.server-public-key-pattern' | translate }} + + + {{ 'device-profile.lwm2m.server-public-key-length' | translate: {count: maxLengthPublicKey } }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index 55f8db8f84..cccaed895f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -14,26 +14,24 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { - DEFAULT_CLIENT_HOLD_OFF_TIME, - DEFAULT_ID_SERVER, DEFAULT_PORT_BOOTSTRAP_NO_SEC, DEFAULT_PORT_SERVER_NO_SEC, KEY_REGEXP_HEX_DEC, LEN_MAX_PUBLIC_KEY_RPK, LEN_MAX_PUBLIC_KEY_X509, - SECURITY_CONFIG_MODE, - SECURITY_CONFIG_MODE_NAMES, + securityConfigMode, + securityConfigModeNames, ServerSecurityConfig } from './lwm2m-profile-config.models'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { WINDOW } from '@core/services/window.service'; -import { pairwise, startWith } from 'rxjs/operators'; import { DeviceProfileService } from '@core/http/device-profile.service'; +import { of, Subject } from 'rxjs'; +import { map, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { Observable } from 'rxjs/internal/Observable'; +import { deepClone } from '@core/utils'; -// @dynamic @Component({ selector: 'tb-profile-lwm2m-device-config-server', templateUrl: './lwm2m-device-config-server.component.html', @@ -46,127 +44,78 @@ import { DeviceProfileService } from '@core/http/device-profile.service'; ] }) -export class Lwm2mDeviceConfigServerComponent implements ControlValueAccessor { +export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAccessor, OnDestroy { - private requiredValue: boolean; private disabled = false; + private destroy$ = new Subject(); + + private securityDefaultConfig: ServerSecurityConfig; - valuePrev = null; serverFormGroup: FormGroup; - securityConfigLwM2MType = SECURITY_CONFIG_MODE; - securityConfigLwM2MTypes = Object.keys(SECURITY_CONFIG_MODE); - credentialTypeLwM2MNamesMap = SECURITY_CONFIG_MODE_NAMES; - lenMinServerPublicKey = 0; - lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK; + securityConfigLwM2MType = securityConfigMode; + securityConfigLwM2MTypes = Object.keys(securityConfigMode); + credentialTypeLwM2MNamesMap = securityConfigModeNames; + maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_RPK; currentSecurityMode = null; @Input() - bootstrapServerIs: boolean; + isBootstrapServer = false; - get required(): boolean { - return this.requiredValue; - } + private propagateChange = (v: any) => { }; - @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); + constructor(public fb: FormBuilder, + private deviceProfileService: DeviceProfileService) { } - constructor(public fb: FormBuilder, - private deviceProfileService: DeviceProfileService, - @Inject(WINDOW) private window: Window) { - this.serverFormGroup = this.initServerGroup(); + ngOnInit(): void { + this.serverFormGroup = this.fb.group({ + host: ['', Validators.required], + port: [this.isBootstrapServer ? DEFAULT_PORT_BOOTSTRAP_NO_SEC : DEFAULT_PORT_SERVER_NO_SEC, [Validators.required, Validators.min(0)]], + securityMode: [securityConfigMode.NO_SEC], + serverPublicKey: ['', Validators.required], + clientHoldOffTime: ['', [Validators.required, Validators.min(0)]], + serverId: ['', [Validators.required, Validators.min(0)]], + bootstrapServerAccountTimeout: ['', [Validators.required, Validators.min(0)]], + }); this.serverFormGroup.get('securityMode').valueChanges.pipe( - startWith(null), - pairwise() - ).subscribe(([previousValue, currentValue]) => { - if (previousValue === null || previousValue !== currentValue) { - this.getLwm2mBootstrapSecurityInfo(currentValue); - this.updateValidate(currentValue); - this.serverFormGroup.updateValueAndValidity(); - } - + tap(securityMode => this.updateValidate(securityMode)), + mergeMap(securityMode => this.getLwm2mBootstrapSecurityInfo(securityMode)), + takeUntil(this.destroy$) + ).subscribe(serverSecurityConfig => { + this.serverFormGroup.patchValue(serverSecurityConfig, {emitEvent: false}); }); - this.serverFormGroup.valueChanges.subscribe(value => { - if (this.disabled !== undefined && !this.disabled) { - this.propagateChangeState(value); - } + this.serverFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + this.propagateChangeState(value); }); } - private updateValueFields = (serverData: ServerSecurityConfig): void => { - serverData.bootstrapServerIs = this.bootstrapServerIs; - this.serverFormGroup.patchValue(serverData, {emitEvent: false}); - this.serverFormGroup.get('bootstrapServerIs').disable(); - const securityMode = this.serverFormGroup.get('securityMode').value as SECURITY_CONFIG_MODE; - this.updateValidate(securityMode); + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } - private updateValidate = (securityMode: SECURITY_CONFIG_MODE): void => { - switch (securityMode) { - case SECURITY_CONFIG_MODE.NO_SEC: - this.setValidatorsNoSecPsk(); - break; - case SECURITY_CONFIG_MODE.PSK: - this.setValidatorsNoSecPsk(); - break; - case SECURITY_CONFIG_MODE.RPK: - this.lenMinServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK; - this.lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_RPK; - this.setValidatorsRpkX509(); - break; - case SECURITY_CONFIG_MODE.X509: - this.lenMinServerPublicKey = 0; - this.lenMaxServerPublicKey = LEN_MAX_PUBLIC_KEY_X509; - this.setValidatorsRpkX509(); - break; + writeValue(serverData: ServerSecurityConfig): void { + if (serverData) { + this.serverFormGroup.patchValue(serverData, {emitEvent: false}); + this.updateValidate(serverData.securityMode); } - this.serverFormGroup.updateValueAndValidity(); - } - - private setValidatorsNoSecPsk = (): void => { - this.serverFormGroup.get('serverPublicKey').setValidators([]); - } - - private setValidatorsRpkX509 = (): void => { - this.serverFormGroup.get('serverPublicKey').setValidators([Validators.required, - Validators.pattern(KEY_REGEXP_HEX_DEC), - Validators.minLength(this.lenMinServerPublicKey), - Validators.maxLength(this.lenMaxServerPublicKey)]); - } - - writeValue(value: ServerSecurityConfig): void { - if (value) { - this.updateValueFields(value); + if (!this.securityDefaultConfig){ + this.getLwm2mBootstrapSecurityInfo().subscribe(value => { + if (!serverData) { + this.serverFormGroup.patchValue(value); + } + }); } } - private propagateChange = (v: any) => {}; - registerOnChange(fn: any): void { this.propagateChange = fn; } - private propagateChangeState = (value: any): void => { - if (value !== undefined) { - if (this.valuePrev === null) { - this.valuePrev = 'init'; - } else if (this.valuePrev === 'init') { - this.valuePrev = value; - } else if (JSON.stringify(value) !== JSON.stringify(this.valuePrev)) { - this.valuePrev = value; - if (this.serverFormGroup.valid) { - this.propagateChange(value); - } else { - this.propagateChange(null); - } - } - } - } - setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; - this.valuePrev = null; if (isDisabled) { this.serverFormGroup.disable({emitEvent: false}); } else { @@ -177,33 +126,76 @@ export class Lwm2mDeviceConfigServerComponent implements ControlValueAccessor { registerOnTouched(fn: any): void { } - private initServerGroup = (): FormGroup => { - const port = this.bootstrapServerIs ? DEFAULT_PORT_BOOTSTRAP_NO_SEC : DEFAULT_PORT_SERVER_NO_SEC; - return this.fb.group({ - host: [this.window.location.hostname, this.required ? Validators.required : ''], - port: [port, this.required ? Validators.required : ''], - bootstrapServerIs: [this.bootstrapServerIs, ''], - securityMode: [this.fb.control(SECURITY_CONFIG_MODE.NO_SEC)], - serverPublicKey: ['', this.required ? Validators.required : ''], - clientHoldOffTime: [DEFAULT_CLIENT_HOLD_OFF_TIME, this.required ? Validators.required : ''], - serverId: [DEFAULT_ID_SERVER, this.required ? Validators.required : ''], - bootstrapServerAccountTimeout: ['', this.required ? Validators.required : ''], - }); + private updateValidate(securityMode: securityConfigMode): void { + switch (securityMode) { + case securityConfigMode.NO_SEC: + case securityConfigMode.PSK: + this.clearValidators(); + break; + case securityConfigMode.RPK: + this.maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_RPK; + this.setValidators(LEN_MAX_PUBLIC_KEY_RPK); + break; + case securityConfigMode.X509: + this.maxLengthPublicKey = LEN_MAX_PUBLIC_KEY_X509; + this.setValidators(0); + break; + } + this.serverFormGroup.get('serverPublicKey').updateValueAndValidity({emitEvent: false}); + } + + private clearValidators(): void { + this.serverFormGroup.get('serverPublicKey').clearValidators(); + } + + private setValidators(minLengthKey: number): void { + this.serverFormGroup.get('serverPublicKey').setValidators([ + Validators.required, + Validators.pattern(KEY_REGEXP_HEX_DEC), + Validators.minLength(minLengthKey), + Validators.maxLength(this.maxLengthPublicKey) + ]); } - private getLwm2mBootstrapSecurityInfo = (mode: string): void => { - this.deviceProfileService.getLwm2mBootstrapSecurityInfo(mode, this.serverFormGroup.get('bootstrapServerIs').value).subscribe( - (serverSecurityConfig) => { - this.serverFormGroup.patchValue({ - host: serverSecurityConfig.host, - port: serverSecurityConfig.port, - serverPublicKey: serverSecurityConfig.serverPublicKey, - clientHoldOffTime: serverSecurityConfig.clientHoldOffTime, - serverId: serverSecurityConfig.serverId, - bootstrapServerAccountTimeout: serverSecurityConfig.bootstrapServerAccountTimeout - }, - {emitEvent: true}); + private propagateChangeState = (value: ServerSecurityConfig): void => { + if (value !== undefined) { + if (this.serverFormGroup.valid) { + this.propagateChange(value); + } else { + this.propagateChange(null); } + } + } + + private getLwm2mBootstrapSecurityInfo(securityMode = securityConfigMode.NO_SEC): Observable { + if (this.securityDefaultConfig) { + return of(this.processingBootstrapSecurityInfo(this.securityDefaultConfig, securityMode)); + } + return this.deviceProfileService.getLwm2mBootstrapSecurityInfo(this.isBootstrapServer).pipe( + map(securityInfo => { + this.securityDefaultConfig = securityInfo; + return this.processingBootstrapSecurityInfo(securityInfo, securityMode); + }) ); } + + private processingBootstrapSecurityInfo(securityConfig: ServerSecurityConfig, securityMode: securityConfigMode): ServerSecurityConfig { + const config = deepClone(securityConfig); + switch (securityMode) { + case securityConfigMode.PSK: + config.port = config.securityPort; + config.host = config.securityHost; + config.serverPublicKey = ''; + break; + case securityConfigMode.RPK: + case securityConfigMode.X509: + config.port = config.securityPort; + config.host = config.securityHost; + break; + case securityConfigMode.NO_SEC: + config.serverPublicKey = ''; + break; + } + return config; + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index a29605a527..30b78393d3 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -15,147 +15,172 @@ limitations under the License. --> -
+
-
- - {{ 'device-profile.lwm2m.client-only-observe-after-connect-label' | translate }} - - {{ 'device-profile.lwm2m.client-only-observe-after-connect' | translate: - {count: 1} }} - {{ 'device-profile.lwm2m.client-only-observe-after-connect' | translate: - {count: 2} }} - - -
-
- - -
-
- - -
+ + + +
+ +
+ + + + {{ 'device-profile.lwm2m.servers' | translate }} + + +
+ + {{ 'device-profile.lwm2m.short-id' | translate }} + + + {{ 'device-profile.lwm2m.short-id' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + + + {{ 'device-profile.lwm2m.lifetime' | translate }} + + + {{ 'device-profile.lwm2m.lifetime' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + + + {{ 'device-profile.lwm2m.default-min-period' | translate }} + + + {{ 'device-profile.lwm2m.default-min-period' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + +
+ + {{ 'device-profile.lwm2m.binding' | translate }} + + + {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }} + + + + + {{ 'device-profile.lwm2m.notif-if-disabled' | translate }} + +
+
+ + + {{ 'device-profile.lwm2m.bootstrap-server' | translate }} + + + + + + + + + {{ 'device-profile.lwm2m.lwm2m-server' | translate }} + + + + + + +
+
+
+
+
-
- - - - -
{{ 'device-profile.lwm2m.servers' | translate | uppercase }}
-
-
- -
-
- - {{ 'device-profile.lwm2m.short-id' | translate }} - - - {{ 'device-profile.lwm2m.short-id' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.lifetime' | translate }} - - - {{ 'device-profile.lwm2m.lifetime' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - - - {{ 'device-profile.lwm2m.default-min-period' | translate }} - - - {{ 'device-profile.lwm2m.default-min-period' | translate }} - {{ 'device-profile.lwm2m.required' | translate }} - - -
-
- - {{ 'device-profile.lwm2m.binding' | translate }} - - - {{ bindingModeTypeNamesMap.get(bindingModeType[bindingMode]) }} - - - -
-
- - {{ 'device-profile.lwm2m.notif-if-disabled' | translate }} - -
-
-
-
-
- - - - -
{{ 'device-profile.lwm2m.bootstrap-server' | translate | uppercase }}
-
-
- -
- - -
-
-
-
- - - - -
{{ 'device-profile.lwm2m.lwm2m-server' | translate | uppercase }}
-
-
- -
- - -
-
-
-
-
+
+ device-profile.lwm2m.fw-update + + {{ 'device-profile.lwm2m.fw-update-strategy' | translate }} + + {{ 'device-profile.lwm2m.fw-update-strategy-package' | translate }} + {{ 'device-profile.lwm2m.fw-update-strategy-package-uri' | translate }} + {{ 'device-profile.lwm2m.fw-update-strategy-data' | translate }} + + + + {{ 'device-profile.lwm2m.fw-update-recourse' | translate }} + + + {{ 'device-profile.lwm2m.fw-update-recourse-required' | translate }} + + +
+
+ device-profile.lwm2m.sw-update + + {{ 'device-profile.lwm2m.sw-update-strategy' | translate }} + + {{ 'device-profile.lwm2m.sw-update-strategy-package' | translate }} + {{ 'device-profile.lwm2m.sw-update-strategy-package-uri' | translate }} + + + + {{ 'device-profile.lwm2m.sw-update-recourse' | translate }} + + + {{ 'device-profile.lwm2m.sw-update-recourse-required' | translate }} + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
-
+
Lwm2mDeviceProfileTransportConfigurationComponent), multi: true }] }) -export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, Validators { +export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, Validators, OnDestroy { private configurationValue: Lwm2mProfileConfigModels; private requiredValue: boolean; private disabled = false; + private destroy$ = new Subject(); bindingModeType = BINDING_MODE; bindingModeTypes = Object.keys(BINDING_MODE); @@ -64,6 +70,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro bootstrapServer: string; lwm2mServer: string; sortFunction: (key: string, value: object) => object; + isFwUpdateStrategy: boolean; + isSwUpdateStrategy: boolean; get required(): boolean { return this.requiredValue; @@ -80,24 +88,58 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro constructor(private fb: FormBuilder, private deviceProfileService: DeviceProfileService) { this.lwm2mDeviceProfileFormGroup = this.fb.group({ - clientOnlyObserveAfterConnect: [1, []], objectIds: [null, Validators.required], observeAttrTelemetry: [null, Validators.required], shortId: [null, Validators.required], lifetime: [null, Validators.required], defaultMinPeriod: [null, Validators.required], notifIfDisabled: [true, []], - binding:[], + binding: [], bootstrapServer: [null, Validators.required], lwm2mServer: [null, Validators.required], + clientStrategy: [1, []], + fwUpdateStrategy: [1, []], + swUpdateStrategy: [1, []], + fwUpdateRecourse: [{value: '', disabled: true}, []], + swUpdateRecourse: [{value: '', disabled: true}, []] }); this.lwm2mDeviceConfigFormGroup = this.fb.group({ configurationJson: [null, Validators.required] }); - this.lwm2mDeviceProfileFormGroup.valueChanges.subscribe((value) => { + this.lwm2mDeviceProfileFormGroup.get('fwUpdateStrategy').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((fwStrategy) => { + if (fwStrategy === 2) { + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').enable({emitEvent: false}); + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').patchValue(DEFAULT_FW_UPDATE_RESOURCE, {emitEvent: false}); + this.isFwUpdateStrategy = true; + } else { + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').disable({emitEvent: false}); + this.isFwUpdateStrategy = false; + } + this.otaUpdateFwStrategyValidate(true); + }); + this.lwm2mDeviceProfileFormGroup.get('swUpdateStrategy').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((swStrategy) => { + if (swStrategy === 2) { + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').enable({emitEvent: false}); + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').patchValue(DEFAULT_SW_UPDATE_RESOURCE, {emitEvent: false}); + this.isSwUpdateStrategy = true; + } else { + this.isSwUpdateStrategy = false; + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').disable({emitEvent: false}); + } + this.otaUpdateSwStrategyValidate(true); + }); + this.lwm2mDeviceProfileFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { this.updateDeviceProfileValue(value); }); - this.lwm2mDeviceConfigFormGroup.valueChanges.subscribe(() => { + this.lwm2mDeviceConfigFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); this.sortFunction = this.sortObjectKeyPathJson; @@ -110,6 +152,11 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro registerOnTouched(fn: any): void { } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (isDisabled) { @@ -122,11 +169,17 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } writeValue(value: Lwm2mProfileConfigModels | null): void { - this.configurationValue = (Object.keys(value).length === 0) ? getDefaultProfileConfig() : value; - this.lwm2mDeviceConfigFormGroup.patchValue({ - configurationJson: this.configurationValue - }, {emitEvent: false}); - this.initWriteValue(); + if (isDefinedAndNotNull(value)) { + if (Object.keys(value).length !== 0 && (value?.clientLwM2mSettings || value?.observeAttr || value?.bootstrap)) { + this.configurationValue = value; + } else { + this.configurationValue = getDefaultProfileConfig(); + } + this.lwm2mDeviceConfigFormGroup.patchValue({ + configurationJson: this.configurationValue + }, {emitEvent: false}); + this.initWriteValue(); + } } private initWriteValue = (): void => { @@ -149,8 +202,11 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } private updateWriteValue = (value: ModelValue): void => { + const fwResource = isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ? + this.configurationValue.clientLwM2mSettings.fwUpdateRecourse : ''; + const swResource = isDefinedAndNotNull(this.configurationValue.clientLwM2mSettings.fwUpdateRecourse) ? + this.configurationValue.clientLwM2mSettings.swUpdateRecourse : ''; this.lwm2mDeviceProfileFormGroup.patchValue({ - clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect, objectIds: value, observeAttrTelemetry: this.getObserveAttrTelemetryObjects(value.objectsList), shortId: this.configurationValue.bootstrap.servers.shortId, @@ -159,9 +215,20 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro notifIfDisabled: this.configurationValue.bootstrap.servers.notifIfDisabled, binding: this.configurationValue.bootstrap.servers.binding, bootstrapServer: this.configurationValue.bootstrap.bootstrapServer, - lwm2mServer: this.configurationValue.bootstrap.lwm2mServer + lwm2mServer: this.configurationValue.bootstrap.lwm2mServer, + clientStrategy: this.configurationValue.clientLwM2mSettings.clientStrategy, + fwUpdateStrategy: this.configurationValue.clientLwM2mSettings.fwUpdateStrategy || 1, + swUpdateStrategy: this.configurationValue.clientLwM2mSettings.swUpdateStrategy || 1, + fwUpdateRecourse: fwResource, + swUpdateRecourse: swResource }, {emitEvent: false}); + this.configurationValue.clientLwM2mSettings.fwUpdateRecourse = fwResource; + this.configurationValue.clientLwM2mSettings.swUpdateRecourse = swResource; + this.isFwUpdateStrategy = this.configurationValue.clientLwM2mSettings.fwUpdateStrategy === 2; + this.isSwUpdateStrategy = this.configurationValue.clientLwM2mSettings.swUpdateStrategy === 2; + this.otaUpdateSwStrategyValidate(); + this.otaUpdateFwStrategyValidate(); } private updateModel = (): void => { @@ -181,8 +248,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro private updateDeviceProfileValue(config): void { if (this.lwm2mDeviceProfileFormGroup.valid) { - this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect = - config.clientOnlyObserveAfterConnect; this.updateObserveAttrTelemetryFromGroupToJson(config.observeAttrTelemetry.clientLwM2M); this.configurationValue.bootstrap.bootstrapServer = config.bootstrapServer; this.configurationValue.bootstrap.lwm2mServer = config.lwm2mServer; @@ -192,9 +257,14 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro bootstrapServers.defaultMinPeriod = config.defaultMinPeriod; bootstrapServers.notifIfDisabled = config.notifIfDisabled; bootstrapServers.binding = config.binding; + this.configurationValue.clientLwM2mSettings.clientStrategy = config.clientStrategy; + this.configurationValue.clientLwM2mSettings.fwUpdateStrategy = config.fwUpdateStrategy; + this.configurationValue.clientLwM2mSettings.swUpdateStrategy = config.swUpdateStrategy; + this.configurationValue.clientLwM2mSettings.fwUpdateRecourse = config.fwUpdateRecourse; + this.configurationValue.clientLwM2mSettings.swUpdateRecourse = config.swUpdateRecourse; this.upDateJsonAllConfig(); - this.updateModel(); } + this.updateModel(); } private getObserveAttrTelemetryObjects = (objectList: ObjectLwM2M[]): object => { @@ -241,7 +311,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const pathParameter = Array.from(path.split('/'), String); const objectLwM2M = clientObserveAttrTelemetry.find(x => x.keyId === pathParameter[0]); if (objectLwM2M) { - const instance = this.updateInInstanceKeyName (objectLwM2M.instances[0], +pathParameter[1]); + const instance = this.updateInInstanceKeyName(objectLwM2M.instances[0], +pathParameter[1]); objectLwM2M.instances.push(instance); } }); @@ -252,7 +322,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro instanceUpdate.id = instanceId; instanceUpdate.resources.forEach(resource => { resource.keyName = _.camelCase(resource.name + instanceUpdate.id); - }) + }); return instanceUpdate; } @@ -466,4 +536,23 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } }); } + + private otaUpdateFwStrategyValidate(updated = false): void { + if (this.isFwUpdateStrategy) { + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').setValidators([Validators.required]); + } else { + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').clearValidators(); + } + this.lwm2mDeviceProfileFormGroup.get('fwUpdateRecourse').updateValueAndValidity({emitEvent: updated}); + } + + private otaUpdateSwStrategyValidate(updated = false): void { + if (this.isSwUpdateStrategy) { + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').setValidators([Validators.required]); + } else { + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').clearValidators(); + } + this.lwm2mDeviceProfileFormGroup.get('swUpdateRecourse').updateValueAndValidity({emitEvent: updated}); + } + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts index 8f1f676769..ae7c3ccdc6 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts @@ -15,10 +15,19 @@ /// import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { Observable } from 'rxjs'; -import { filter, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, mergeMap, share, tap } from 'rxjs/operators'; import { ModelValue, ObjectLwM2M, PAGE_SIZE_LIMIT } from './lwm2m-profile-config.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { Direction } from '@shared/models/page/sort-order'; @@ -33,13 +42,18 @@ import { PageLink } from '@shared/models/page/page-link'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Lwm2mObjectListComponent), multi: true - }] + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => Lwm2mObjectListComponent), + multi: true + } + ] }) -export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, Validators { +export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, Validator { private requiredValue: boolean; private dirty = false; - private lw2mModels: Observable>; private modelValue: Array = []; lwm2mListFormGroup: FormGroup; @@ -78,8 +92,8 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V } private updateValidators = (): void => { - this.lwm2mListFormGroup.get('objectLwm2m').setValidators(this.required ? [Validators.required] : []); - this.lwm2mListFormGroup.get('objectLwm2m').updateValueAndValidity(); + this.lwm2mListFormGroup.get('objectsList').setValidators(this.required ? [Validators.required] : []); + this.lwm2mListFormGroup.get('objectsList').updateValueAndValidity(); } registerOnChange(fn: any): void { @@ -92,6 +106,7 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V ngOnInit() { this.filteredObjectsList = this.lwm2mListFormGroup.get('objectLwm2m').valueChanges .pipe( + distinctUntilChanged(), tap((value) => { if (value && typeof value !== 'string') { this.add(value); @@ -100,7 +115,8 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V } }), filter(searchText => isString(searchText)), - mergeMap(searchText => this.fetchListObjects(searchText)) + mergeMap(searchText => this.fetchListObjects(searchText)), + share() ); } @@ -131,6 +147,12 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V } } + validate(): ValidationErrors | null { + return this.lwm2mListFormGroup.valid ? null : { + lwm2mListObj: false + }; + } + private add(object: ObjectLwM2M): void { if (isDefinedAndNotNull(this.modelValue) && this.modelValue.indexOf(object.keyId) === -1) { this.modelValue.push(object.keyId); @@ -157,23 +179,13 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V return object ? object.name : undefined; } - private fetchListObjects = (searchText?: string): Observable> => { + private fetchListObjects = (searchText: string): Observable> => { this.searchText = searchText; - return this.getLwM2mModelsPage().pipe( - map(objectLwM2Ms => objectLwM2Ms) - ); - } - - private getLwM2mModelsPage(): Observable> { const pageLink = new PageLink(PAGE_SIZE_LIMIT, 0, this.searchText, { property: 'id', direction: Direction.ASC }); - this.lw2mModels = this.deviceProfileService.getLwm2mObjectsPage(pageLink).pipe( - publishReplay(1), - refCount() - ); - return this.lw2mModels; + return this.deviceProfileService.getLwm2mObjectsPage(pageLink); } onFocus = (): void => { @@ -183,10 +195,9 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V } } - private clear = (value: string = ''): void => { - this.objectInput.nativeElement.value = value; + private clear = (): void => { this.searchText = ''; - this.lwm2mListFormGroup.get('objectLwm2m').patchValue(value); + this.lwm2mListFormGroup.get('objectLwm2m').patchValue(null); setTimeout(() => { this.objectInput.nativeElement.blur(); this.objectInput.nativeElement.focus(); diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html index e58861c1eb..3ee9bffa27 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -46,14 +46,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html index 759e880ffa..02d594905d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -16,7 +16,7 @@ -->
- + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss index 2de6fe17d6..afed0db1ff 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.scss @@ -24,6 +24,9 @@ } :host{ + section { + padding: 2px; + } .instance-list { mat-expansion-panel-header { color: inherit; diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index acea41394a..1f2d125d9f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -20,7 +20,6 @@ export const INSTANCE = 'instance'; export const RESOURCES = 'resources'; export const ATTRIBUTE_LWM2M = 'attributeLwm2m'; export const CLIENT_LWM2M = 'clientLwM2M'; -export const CLIENT_LWM2M_SETTINGS = 'clientLwM2mSettings'; export const OBSERVE_ATTR_TELEMETRY = 'observeAttrTelemetry'; export const OBSERVE = 'observe'; export const ATTRIBUTE = 'attribute'; @@ -28,7 +27,7 @@ export const TELEMETRY = 'telemetry'; export const KEY_NAME = 'keyName'; export const DEFAULT_ID_SERVER = 123; export const DEFAULT_ID_BOOTSTRAP = 111; -export const DEFAULT_HOST_NAME = 'localhost'; +export const DEFAULT_LOCAL_HOST_NAME = 'localhost'; export const DEFAULT_PORT_SERVER_NO_SEC = 5685; export const DEFAULT_PORT_BOOTSTRAP_NO_SEC = 5687; export const DEFAULT_CLIENT_HOLD_OFF_TIME = 1; @@ -43,6 +42,9 @@ export const KEY_REGEXP_HEX_DEC = /^[-+]?[0-9A-Fa-f]+\.?[0-9A-Fa-f]*?$/; export const KEY_REGEXP_NUMBER = /^(\-?|\+?)\d*$/; export const INSTANCES_ID_VALUE_MIN = 0; export const INSTANCES_ID_VALUE_MAX = 65535; +export const DEFAULT_OTA_UPDATE_PROTOCOL = 'coap://'; +export const DEFAULT_FW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC; +export const DEFAULT_SW_UPDATE_RESOURCE = DEFAULT_OTA_UPDATE_PROTOCOL + DEFAULT_LOCAL_HOST_NAME + ':' + DEFAULT_PORT_SERVER_NO_SEC; export enum BINDING_MODE { @@ -64,7 +66,7 @@ export const BINDING_MODE_NAMES = new Map( [BINDING_MODE.UQ, 'UQ: UDP connection in queue mode'], [BINDING_MODE.US, 'US: both UDP and SMS connections active, both in standard mode'], [BINDING_MODE.UQS, 'UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode'], - [BINDING_MODE.T,'T: TCP connection in standard mode'], + [BINDING_MODE.T, 'T: TCP connection in standard mode'], [BINDING_MODE.TQ, 'TQ: TCP connection in queue mode'], [BINDING_MODE.TS, 'TS: both TCP and SMS connections active, both in standard mode'], [BINDING_MODE.TQS, 'TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode'], @@ -110,19 +112,19 @@ export const ATTRIBUTE_LWM2M_MAP = new Map( export const ATTRIBUTE_KEYS = Object.keys(ATTRIBUTE_LWM2M_ENUM) as string[]; -export enum SECURITY_CONFIG_MODE { +export enum securityConfigMode { PSK = 'PSK', RPK = 'RPK', X509 = 'X509', NO_SEC = 'NO_SEC' } -export const SECURITY_CONFIG_MODE_NAMES = new Map( +export const securityConfigModeNames = new Map( [ - [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'], - [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'], - [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'], - [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'] + [securityConfigMode.PSK, 'Pre-Shared Key'], + [securityConfigMode.RPK, 'Raw Public Key'], + [securityConfigMode.X509, 'X.509 Certificate'], + [securityConfigMode.NO_SEC, 'No Security'] ] ); @@ -141,9 +143,10 @@ export interface BootstrapServersSecurityConfig { export interface ServerSecurityConfig { host?: string; + securityHost?: string; port?: number; - bootstrapServerIs?: boolean; - securityMode: string; + securityPort?: number; + securityMode: securityConfigMode; clientPublicKeyOrId?: string; clientSecretKey?: string; serverPublicKey?: string; @@ -162,11 +165,14 @@ export interface Lwm2mProfileConfigModels { clientLwM2mSettings: ClientLwM2mSettings; observeAttr: ObservableAttributes; bootstrap: BootstrapSecurityConfig; - } export interface ClientLwM2mSettings { - clientOnlyObserveAfterConnect: number; + clientStrategy: string; + fwUpdateStrategy: number; + swUpdateStrategy: number; + fwUpdateRecourse: string; + swUpdateRecourse: string; } export interface ObservableAttributes { @@ -187,12 +193,11 @@ export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecu }; } -export function getDefaultBootstrapServerSecurityConfig(hostname: any): ServerSecurityConfig { +export function getDefaultBootstrapServerSecurityConfig(hostname: string): ServerSecurityConfig { return { host: hostname, port: DEFAULT_PORT_BOOTSTRAP_NO_SEC, - bootstrapServerIs: true, - securityMode: SECURITY_CONFIG_MODE.NO_SEC.toString(), + securityMode: securityConfigMode.NO_SEC, serverPublicKey: '', clientHoldOffTime: DEFAULT_CLIENT_HOLD_OFF_TIME, serverId: DEFAULT_ID_BOOTSTRAP, @@ -202,7 +207,6 @@ export function getDefaultBootstrapServerSecurityConfig(hostname: any): ServerSe export function getDefaultLwM2MServerSecurityConfig(hostname): ServerSecurityConfig { const DefaultLwM2MServerSecurityConfig = getDefaultBootstrapServerSecurityConfig(hostname); - DefaultLwM2MServerSecurityConfig.bootstrapServerIs = false; DefaultLwM2MServerSecurityConfig.port = DEFAULT_PORT_SERVER_NO_SEC; DefaultLwM2MServerSecurityConfig.serverId = DEFAULT_ID_SERVER; return DefaultLwM2MServerSecurityConfig; @@ -230,13 +234,17 @@ export function getDefaultProfileConfig(hostname?: any): Lwm2mProfileConfigModel return { clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), observeAttr: getDefaultProfileObserveAttrConfig(), - bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) + bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_LOCAL_HOST_NAME) }; } function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { return { - clientOnlyObserveAfterConnect: 1 + clientStrategy: '1', + fwUpdateStrategy: 1, + swUpdateStrategy: 1, + fwUpdateRecourse: DEFAULT_FW_UPDATE_RESOURCE, + swUpdateRecourse: DEFAULT_SW_UPDATE_RESOURCE }; } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index 381e2f9e8d..0cbb50ed8e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -39,6 +39,8 @@ import { transportPayloadTypeTranslationMap } from '@shared/models/device.models'; import { isDefinedAndNotNull } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-mqtt-device-profile-transport-configuration', @@ -50,7 +52,7 @@ import { isDefinedAndNotNull } from '@core/utils'; multi: true }] }) -export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { +export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { transportPayloadTypes = Object.keys(TransportPayloadType); @@ -58,6 +60,7 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control mqttDeviceProfileTransportConfigurationFormGroup: FormGroup; + private destroy$ = new Subject(); private requiredValue: boolean; get required(): boolean { @@ -98,15 +101,23 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control }) }, {validator: this.uniqueDeviceTopicValidator} ); - this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.transportPayloadType') - .valueChanges.subscribe(payloadType => { + this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.transportPayloadType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(payloadType => { this.updateTransportPayloadBasedControls(payloadType, true); }); - this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { @@ -192,8 +203,8 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control } private uniqueDeviceTopicValidator(control: FormGroup): { [key: string]: boolean } | null { - if (control.value) { - const formValue = control.value as MqttDeviceProfileTransportConfiguration; + if (control.getRawValue()) { + const formValue = control.getRawValue() as MqttDeviceProfileTransportConfiguration; if (formValue.deviceAttributesTopic === formValue.deviceTelemetryTopic) { return {unique: true}; } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts index 96f7454cba..e6749a9219 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts @@ -14,17 +14,19 @@ /// limitations under the License. /// -import {Component, forwardRef, Input, OnInit} from '@angular/core'; -import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; -import {Store} from '@ngrx/store'; -import {AppState} from '@app/core/core.state'; -import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DeviceProfileTransportConfiguration, DeviceTransportType, SnmpDeviceProfileTransportConfiguration } from '@shared/models/device.models'; -import {isDefinedAndNotNull} from "@core/utils"; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; export interface OidMappingConfiguration { isAttribute: boolean; @@ -44,8 +46,11 @@ export interface OidMappingConfiguration { multi: true }] }) -export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { +export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { + snmpDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private destroy$ = new Subject(); private requiredValue: boolean; private configuration = []; @@ -71,11 +76,18 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({ configuration: [null, Validators.required] }); - this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { this.updateModel(); }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + registerOnChange(fn: any): void { this.propagateChange = fn; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 9685dec25b..0db5c6f094 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -81,9 +81,9 @@ {{ column.title }} - + @@ -125,8 +125,8 @@ 'mat-selected': alarmsDatasource.isSelected(alarm), 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm), 'invisible': alarmsDatasource.dataLoading}" - *matRowDef="let alarm; columns: displayedColumns;" - [ngStyle]="rowStyle(alarm)" + *matRowDef="let alarm; columns: displayedColumns; let row = index" + [ngStyle]="rowStyle(alarm, row)" (click)="onRowClick($event, alarm)"> = []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: AlarmsTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; @@ -261,6 +273,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public onDataUpdated() { this.updateTitle(true); this.alarmsDatasource.updateAlarms(); + this.clearCache(); } public pageLinkSortDirection(): SortDirection { @@ -426,7 +439,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.displayedColumns.push('actions'); } - this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys); + this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys, this.ngZone); if (this.enableSelection) { this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { const hideTitlePanel = selectionMode || this.textSearchMode; @@ -484,6 +497,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } + this.clearCache(); } } as DisplayColumnsPanelData }, @@ -609,91 +623,100 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return widthStyle(columnWidth); } - public rowStyle(alarm: AlarmDataInfo): any { - let style: any = {}; - if (alarm) { - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { + public rowStyle(alarm: AlarmDataInfo, row: number): any { + let res = this.rowStyleCache[row]; + if (!res) { + res = {}; + if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { - style = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; + res = {}; console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); } - } else { - style = {}; } + this.rowStyleCache[row] = res; } - return style; + return res; } - public cellStyle(alarm: AlarmDataInfo, key: EntityColumn): any { - let style: any = {}; - if (alarm && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getAlarmValue(alarm, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value, alarm, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); + public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): any { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + res = {}; + if (alarm && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getAlarmValue(alarm, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + res = styleInfo.cellStyleFunction(value, alarm, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } - } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + - `returns '${e}'. Please check your cell style function.`); + } else { + res = this.defaultStyle(key, value); } - } else { - style = this.defaultStyle(key, value); } + this.cellStyleCache[index] = res; } - if (!style.width) { + if (!res.width) { const columnWidth = this.columnWidth[key.def]; - style = {...style, ...widthStyle(columnWidth)}; + res = Object.assign(res, widthStyle(columnWidth)); } - return style; + return res; } - public cellContent(alarm: AlarmDataInfo, key: EntityColumn): SafeHtml { - if (alarm && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getAlarmValue(alarm, key); - let content = ''; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, alarm, this.ctx); - } catch (e) { - content = '' + value; + public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): SafeHtml { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellContentCache[index]; + if (isUndefined(res)) { + res = ''; + if (alarm && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getAlarmValue(alarm, key); + let content = ''; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, alarm, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); } - } else { - content = this.defaultContent(key, contentInfo, value); - } - if (!isDefined(content)) { - return ''; - - } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } - - } else { - return ''; + this.cellContentCache[index] = res; } + return res; } public onRowClick($event: Event, alarm: AlarmDataInfo) { @@ -936,6 +959,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, isSorting(column: EntityColumn): boolean { return column.type === DataKeyType.alarm && column.name.startsWith('details.'); } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; + } } class AlarmsDatasource implements DataSource { @@ -957,7 +986,8 @@ class AlarmsDatasource implements DataSource { private appliedSortOrderLabel: string; constructor(private subscription: IWidgetSubscription, - private dataKeys: Array) { + private dataKeys: Array, + private ngZone: NgZone) { } connect(collectionViewer: CollectionViewer): Observable> { @@ -989,6 +1019,7 @@ class AlarmsDatasource implements DataSource { updateAlarms() { const subscriptionAlarms = this.subscription.alarms; let alarms = new Array(); + let isEmptySelection = false; subscriptionAlarms.data.forEach((alarmData) => { alarms.push(this.alarmDataToInfo(alarmData)); }); @@ -1001,7 +1032,7 @@ class AlarmsDatasource implements DataSource { const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1); this.selection.deselect(...toRemove); if (this.selection.isEmpty()) { - this.onSelectionModeChanged(false); + isEmptySelection = true; } } const alarmsPageData: PageData = { @@ -1010,9 +1041,14 @@ class AlarmsDatasource implements DataSource { totalElements: subscriptionAlarms.totalElements, hasNext: subscriptionAlarms.hasNext }; - this.alarmsSubject.next(alarms); - this.pageDataSubject.next(alarmsPageData); - this.dataLoading = false; + this.ngZone.run(() => { + if (isEmptySelection) { + this.onSelectionModeChanged(false); + } + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.dataLoading = false; + }); } private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index 00687e9b7c..898ceb3b81 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -772,7 +772,7 @@ function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options context.textAlign = 'center'; context.font = Drawings.font(options, 'Label', fontSizeFactor); context.lineWidth = 0; - drawText(context, options, 'Label', options.label.toUpperCase(), textX, textY); + drawText(context, options, 'Label', options.label, textX, textY); } function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 680ed3ad97..ca93253ddc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -42,9 +42,9 @@ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear> {{ column.title }} - + @@ -84,8 +84,8 @@ = []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: EntitiesTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; @@ -222,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public onDataUpdated() { this.updateTitle(true); this.entityDatasource.dataUpdated(); + this.clearCache(); } public pageLinkSortDirection(): SortDirection { @@ -415,8 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } - this.entityDatasource = new EntityDatasource( - this.translate, dataKeys, this.subscription); + this.entityDatasource = new EntityDatasource(this.translate, dataKeys, this.subscription, this.ngZone); } private editColumnsToDisplay($event: Event) { @@ -460,6 +473,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } + this.clearCache(); } } as DisplayColumnsPanelData }, @@ -535,91 +549,98 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni return widthStyle(columnWidth); } - public rowStyle(entity: EntityData): any { - let style: any = {}; - if (entity) { - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { + public rowStyle(entity: EntityData, row: number): any { + let res = this.rowStyleCache[row]; + if (!res) { + res = {}; + if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { - style = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; + res = {}; console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); } - } else { - style = {}; } - } - return style; - } - - public cellStyle(entity: EntityData, key: EntityColumn): any { - let style: any = {}; - if (entity && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getEntityValue(entity, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - style = styleInfo.cellStyleFunction(value, entity, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); + this.rowStyleCache[row] = res; + } + return res; + } + + public cellStyle(entity: EntityData, key: EntityColumn, row: number): any { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellStyleCache[index]; + if (!res) { + res = {}; + if (entity && key) { + const styleInfo = this.stylesInfo[key.def]; + const value = getEntityValue(entity, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + res = styleInfo.cellStyleFunction(value, entity, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } - } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + - `returns '${e}'. Please check your cell style function.`); } - } else { - style = {}; + this.cellStyleCache[index] = res; } } - if (!style.width) { + if (!res.width) { const columnWidth = this.columnWidth[key.def]; - style = {...style, ...widthStyle(columnWidth)}; - } - return style; - } - - public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { - if (entity && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getEntityValue(entity, key); - let content: string; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, entity, this.ctx); - } catch (e) { - content = '' + value; + res = Object.assign(res, widthStyle(columnWidth)); + } + return res; + } + + public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml { + const col = this.columns.indexOf(key); + const index = row * this.columns.length + col; + let res = this.cellContentCache[index]; + if (isUndefined(res)) { + res = ''; + if (entity && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getEntityValue(entity, key); + let content: string; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + content = contentInfo.cellContentFunction(value, entity, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); } - } else { - content = this.defaultContent(key, contentInfo, value); - } - if (!isDefined(content)) { - return ''; - - } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } - - } else { - return ''; + this.cellContentCache[index] = res; } + return res; } private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { @@ -672,6 +693,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {entity}, entityLabel); } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; + } } class EntityDatasource implements DataSource { @@ -689,7 +716,8 @@ class EntityDatasource implements DataSource { constructor( private translate: TranslateService, private dataKeys: Array, - private subscription: IWidgetSubscription + private subscription: IWidgetSubscription, + private ngZone: NgZone ) { } @@ -732,9 +760,11 @@ class EntityDatasource implements DataSource { totalElements: datasourcesPageData.totalElements, hasNext: datasourcesPageData.hasNext }; - this.entitiesSubject.next(entities); - this.pageDataSubject.next(entitiesPageData); - this.dataLoading = false; + this.ngZone.run(() => { + this.entitiesSubject.next(entities); + this.pageDataSubject.next(entitiesPageData); + this.dataLoading = false; + }); } private datasourceToEntityData(datasource: Datasource, data: DatasourceData[]): EntityData { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index a92b4e16bb..4524082e5b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -39,23 +39,23 @@ - +
Timestamp - + {{ h.dataKey.label }} - + @@ -93,8 +93,8 @@ -
= []; + private cellStyleCache: Array = []; + private rowStyleCache: Array = []; + private settings: TimeseriesTableWidgetSettings; private widgetConfig: WidgetConfig; private data: Array; @@ -194,6 +198,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public onDataUpdated() { this.updateCurrentSourceData(); + this.clearCache(); } private initialize() { @@ -235,7 +240,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } public getTabLabel(source: TimeseriesTableSource){ - if(this.useEntityLabel){ + if (this.useEntityLabel) { return source.datasource.entityLabel || source.datasource.entityName; } else { return source.datasource.entityName; @@ -286,7 +291,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if (this.actionCellDescriptors.length) { source.displayedColumns.push('actions'); } - const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe); + const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe, this.ngZone); tsDatasource.dataUpdated(this.data); this.sources.push(source); } @@ -337,6 +342,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI onSourceIndexChanged() { this.updateCurrentSourceData(); this.updateActiveEntityInfo(); + this.clearCache(); } private enterFilterMode() { @@ -378,6 +384,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI source.pageLink.sortOrder.property = sort.active; source.pageLink.sortOrder.direction = Direction[sort.direction.toUpperCase()]; source.timeseriesDatasource.loadRows(); + this.clearCache(); this.ctx.detectChanges(); } @@ -397,94 +404,109 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return source.datasource.entityId; } - public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow): any { - let style: any = {}; - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.name] = row[headerInfo.index]; - }); - style = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); - } - if (Array.isArray(style)) { - throw new TypeError(`Array instead of style object`); - } - } catch (e) { - style = {}; - console.warn(`Row style function in widget ` + - `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); - } - } - return style; - } - - public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): any { - let style: any = {}; - if (index > 0) { - const styleInfo = source.stylesInfo[index - 1]; - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any { + let res = this.rowStyleCache[index]; + if (!res) { + res = {}; + if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { try { const rowData = source.rowDataTemplate; rowData.Timestamp = row[0]; source.header.forEach((headerInfo) => { rowData[headerInfo.dataKey.name] = row[headerInfo.index]; }); - style = styleInfo.cellStyleFunction(value, rowData, this.ctx); - if (!isObject(style)) { - throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); } - if (Array.isArray(style)) { + if (Array.isArray(res)) { throw new TypeError(`Array instead of style object`); } } catch (e) { - style = {}; - console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + - `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); + res = {}; + console.warn(`Row style function in widget ` + + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); } } + this.rowStyleCache[index] = res; } - return style; - } - - public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any): SafeHtml { - if (index === 0) { - return row.formattedTs; - } else { - let content; - const contentInfo = source.contentsInfo[index - 1]; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.name] = row[headerInfo.index]; - }); - content = contentInfo.cellContentFunction(value, rowData, this.ctx); - } catch (e) { - content = '' + value; + return res; + } + + public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): any { + const cacheIndex = rowIndex * (source.header.length + 1) + index; + let res = this.cellStyleCache[cacheIndex]; + if (!res) { + res = {}; + if (index > 0) { + const styleInfo = source.stylesInfo[index - 1]; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + }); + res = styleInfo.cellStyleFunction(value, rowData, this.ctx); + if (!isObject(res)) { + throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); + } + if (Array.isArray(res)) { + throw new TypeError(`Array instead of style object`); + } + } catch (e) { + res = {}; + console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); + } } - } else { - const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; - const units = contentInfo.units || this.ctx.widgetConfig.units; - content = this.ctx.utils.formatValue(value, decimals, units, true); } + this.cellStyleCache[cacheIndex] = res; + } + return res; + } - if (!isDefined(content)) { - return ''; + public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml { + const cacheIndex = rowIndex * (source.header.length + 1) + index ; + let res = this.cellContentCache[cacheIndex]; + if (isUndefined(res)) { + res = ''; + if (index === 0) { + res = row.formattedTs; } else { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - return this.domSanitizer.bypassSecurityTrustHtml(content); - default: - return content; + let content; + const contentInfo = source.contentsInfo[index - 1]; + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.name] = row[headerInfo.index]; + }); + content = contentInfo.cellContentFunction(value, rowData, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = contentInfo.units || this.ctx.widgetConfig.units; + content = this.ctx.utils.formatValue(value, decimals, units, true); + } + + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + res = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + default: + res = content; + } } } + this.cellContentCache[cacheIndex] = res; } + return res; } public onRowClick($event: Event, row: TimeseriesRow) { @@ -530,6 +552,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private loadCurrentSourceRow() { this.sources[this.sourceIndex].timeseriesDatasource.loadRows(); + this.clearCache(); + } + + private clearCache() { + this.cellContentCache.length = 0; + this.cellStyleCache.length = 0; + this.rowStyleCache.length = 0; } } @@ -545,7 +574,8 @@ class TimeseriesDatasource implements DataSource { private source: TimeseriesTableSource, private hideEmptyLines: boolean, private dateFormatFilter: string, - private datePipe: DatePipe + private datePipe: DatePipe, + private ngZone: NgZone ) { this.source.timeseriesDatasource = this; } @@ -568,8 +598,10 @@ class TimeseriesDatasource implements DataSource { catchError(() => of(emptyPageData())), ).subscribe( (pageData) => { - this.rowsSubject.next(pageData.data); - this.pageDataSubject.next(pageData); + this.ngZone.run(() => { + this.rowsSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + }); } ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss index a269d569ae..0372773016 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.scss @@ -59,5 +59,13 @@ #widget-container { min-height: 0; min-width: 0; + + canvas { + user-select: none; + + &::selection, &::-moz-selection { + background-color: transparent; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index fea6e14291..dee17a000c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -759,7 +759,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); this.widgetContext.$container.css('display', 'block'); - this.widgetContext.$container.css('user-select', 'none'); this.widgetContext.$container.attr('id', 'container'); if (this.widgetSizeDetected) { this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 39be34b823..54cf110d5b 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -48,20 +48,6 @@ device.label - - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(deviceWizardFormGroup.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - -
@@ -74,13 +60,10 @@
- +
- {{ 'device-profile.transport-configuration' | translate }} + {{ 'device-profile.transport-configuration' | translate }} + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(transportConfigFormGroup.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 4e2b5b2ff8..b9c38d24dd 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -29,7 +29,6 @@ import { DeviceProvisionConfiguration, DeviceProvisionType, DeviceTransportType, - deviceTransportTypeConfigurationInfoMap, deviceTransportTypeHintMap, deviceTransportTypeTranslationMap } from '@shared/models/device.models'; @@ -66,7 +65,6 @@ export class DeviceWizardDialogComponent extends showNext = true; createProfile = false; - createTransportConfiguration = false; entityType = EntityType; @@ -88,7 +86,7 @@ export class DeviceWizardDialogComponent extends customerFormGroup: FormGroup; - labelPosition = 'end'; + labelPosition: MatHorizontalStepper['labelPosition'] = 'end'; serviceType = ServiceType.TB_RULE_ENGINE; @@ -109,7 +107,6 @@ export class DeviceWizardDialogComponent extends label: [''], gateway: [false], overwriteActivityTime: [false], - transportType: [DeviceTransportType.DEFAULT, Validators.required], addProfileType: [0], deviceProfileId: [null, Validators.required], newDeviceProfileTitle: [{value: null, disabled: true}], @@ -130,7 +127,6 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.get('defaultQueueName').disable(); this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = false; - this.createTransportConfiguration = false; } else { this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null); this.deviceWizardFormGroup.get('deviceProfileId').disable(); @@ -141,18 +137,18 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup.updateValueAndValidity(); this.createProfile = true; - this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value && - deviceTransportTypeConfigurationInfoMap.get(this.deviceWizardFormGroup.get('transportType').value).hasProfileConfiguration; } } )); this.transportConfigFormGroup = this.fb.group( { + transportType: [DeviceTransportType.DEFAULT, Validators.required], transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required] } ); - this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => { + + this.subscriptions.push(this.transportConfigFormGroup.get('transportType').valueChanges.subscribe((transportType) => { this.deviceProfileTransportTypeChanged(transportType); })); @@ -229,8 +225,6 @@ export class DeviceWizardDialogComponent extends if (index > 0) { if (!this.createProfile) { index += 3; - } else if (!this.createTransportConfiguration) { - index += 1; } } switch (index) { @@ -256,8 +250,6 @@ export class DeviceWizardDialogComponent extends private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void { this.transportConfigFormGroup.patchValue( {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); - this.createTransportConfiguration = this.createProfile && deviceTransportType && - deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; } add(): void { @@ -281,7 +273,7 @@ export class DeviceWizardDialogComponent extends const deviceProfile: DeviceProfile = { name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value, type: DeviceProfileType.DEFAULT, - transportType: this.deviceWizardFormGroup.get('transportType').value, + transportType: this.transportConfigFormGroup.get('transportType').value, provisionType: deviceProvisionConfiguration.type, provisionDeviceKey, profileData: { diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index e6de5dbf84..a1393b0bd2 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -37,7 +37,7 @@ export type EntityStringFunction> = (entity: T) => str export type EntityVoidFunction> = (entity: T) => void; export type EntityIdsVoidFunction> = (ids: HasUUID[]) => void; export type EntityCountStringFunction = (count: number) => string; -export type EntityTwoWayOperation> = (entity: T) => Observable; +export type EntityTwoWayOperation> = (entity: T, originalEntity?: T) => Observable; export type EntityByIdOperation> = (id: HasUUID) => Observable; export type EntityIdOneWayOperation = (id: HasUUID) => Observable; export type EntityActionFunction> = (action: EntityAction) => boolean; @@ -48,6 +48,9 @@ export type CellContentFunction> = (entity: T, key: st export type CellTooltipFunction> = (entity: T, key: string) => string | undefined; export type HeaderCellStyleFunction> = (key: string) => object; export type CellStyleFunction> = (entity: T, key: string) => object; +export type CopyCellContent> = (entity: T, key: string, length: number) => object; + +export enum CellActionDescriptorType { 'DEFAULT', 'COPY_BUTTON'} export interface CellActionDescriptor> { name: string; @@ -56,7 +59,8 @@ export interface CellActionDescriptor> { mdiIcon?: string; style?: any; isEnabled: (entity: T) => boolean; - onAction: ($event: MouseEvent, entity: T) => void; + onAction: ($event: MouseEvent, entity: T) => any; + type?: CellActionDescriptorType; } export interface GroupActionDescriptor> { @@ -95,7 +99,8 @@ export class EntityTableColumn> extends BaseEntityTabl public sortable: boolean = true, public headerCellStyleFunction: HeaderCellStyleFunction = () => ({}), public cellTooltipFunction: CellTooltipFunction = () => undefined, - public isNumberColumn: boolean = false) { + public isNumberColumn: boolean = false, + public actionCell: CellActionDescriptor = null) { super('content', key, title, width, sortable); } } @@ -173,7 +178,7 @@ export class EntityTableConfig, P extends PageLink = P deleteEntitiesTitle: EntityCountStringFunction = () => ''; deleteEntitiesContent: EntityCountStringFunction = () => ''; loadEntity: EntityByIdOperation = () => of(); - saveEntity: EntityTwoWayOperation = (entity) => of(entity); + saveEntity: EntityTwoWayOperation = (entity, originalEntity) => of(entity); deleteEntity: EntityIdOneWayOperation = () => of(); entitiesFetchFunction: EntitiesFetchFunction = () => of(emptyPageData()); onEntityAction: EntityActionFunction = () => false; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index 3998b4fc9a..43989d907a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -34,19 +34,19 @@ {{ 'admin.oauth2.enable' | translate }}
- +
- + - {{ domainListTittle(domain) }} + {{ domainListTittle(oauth2ParamsInfo) }} - - - - - -
+
+
+ +
+ + + + +
+
+ admin.oauth2.no-mobile-apps +
+
+
+
+
+ + admin.oauth2.mobile-package + + admin.oauth2.mobile-package-hint + + + {{ 'admin.oauth2.mobile-package-unique' | translate }} + +
+
+ + admin.oauth2.mobile-app-secret + + + + + {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }} + + +
+
+
+ - +
-
- -
-
+
+
-
-
-
- -
- - + + + +
admin.oauth2.providers
- @@ -140,8 +207,8 @@
admin.oauth2.client-id @@ -245,16 +321,13 @@ admin.oauth2.user-info-uri - + - - {{ 'admin.oauth2.user-info-uri-required' | translate }} - {{ 'admin.oauth2.uri-pattern-error' | translate }} @@ -449,7 +522,7 @@
@@ -467,7 +540,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss index e17d7c34e4..aeb401dedf 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss @@ -62,7 +62,7 @@ } } } - .domains-list{ + .domains-list, .apps-list { margin-bottom: 1.5em; .mat-form-field-suffix .mat-icon-button .mat-icon{ font-size: 24px; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index e93ea168fe..bf5dc75023 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -18,8 +18,6 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import { ClientAuthenticationMethod, - ClientRegistration, - DomainInfo, DomainSchema, domainSchemaTranslations, MapperConfig, @@ -27,8 +25,11 @@ import { MapperConfigCustom, MapperConfigType, OAuth2ClientRegistrationTemplate, - OAuth2ClientsDomainParams, - OAuth2ClientsParams, + OAuth2DomainInfo, + OAuth2Info, OAuth2MobileInfo, + OAuth2ParamsInfo, + OAuth2RegistrationInfo, PlatformType, + platformTypeTranslations, TenantNameStrategy } from '@shared/models/oauth2.models'; import { Store } from '@ngrx/store'; @@ -41,7 +42,7 @@ import { WINDOW } from '@core/services/window.service'; import { forkJoin, Subscription } from 'rxjs'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; -import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; import { OAuth2Service } from '@core/http/oauth2.service'; import { ActivatedRoute } from '@angular/router'; @@ -52,6 +53,20 @@ import { ActivatedRoute } from '@angular/router'; }) export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { + constructor(protected store: Store, + private route: ActivatedRoute, + private oauth2Service: OAuth2Service, + private fb: FormBuilder, + private dialogService: DialogService, + private translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store); + } + + get oauth2ParamsInfos(): FormArray { + return this.oauth2SettingsForm.get('oauth2ParamsInfos') as FormArray; + } + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/; private subscriptions: Subscription[] = []; @@ -77,7 +92,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha readonly separatorKeysCodes: number[] = [ENTER, COMMA]; oauth2SettingsForm: FormGroup; - auth2ClientsParams: OAuth2ClientsParams; + oauth2Info: OAuth2Info; clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); mapperConfigType = MapperConfigType; @@ -85,19 +100,21 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha tenantNameStrategies = Object.keys(TenantNameStrategy); protocols = Object.keys(DomainSchema); domainSchemaTranslations = domainSchemaTranslations; + platformTypes = Object.keys(PlatformType); + platformTypeTranslations = platformTypeTranslations; templateProvider = ['Custom']; private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; - constructor(protected store: Store, - private route: ActivatedRoute, - private oauth2Service: OAuth2Service, - private fb: FormBuilder, - private dialogService: DialogService, - private translate: TranslateService, - @Inject(WINDOW) private window: Window) { - super(store); + private static validateScope(control: AbstractControl): ValidationErrors | null { + const scope: string[] = control.value; + if (!scope || !scope.length) { + return { + required: true + }; + } + return null; } ngOnInit(): void { @@ -106,10 +123,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha this.oauth2Service.getOAuth2Template(), this.oauth2Service.getOAuth2Settings() ]).subscribe( - ([templates, auth2ClientsParams]) => { + ([templates, oauth2Info]) => { this.initTemplates(templates); - this.auth2ClientsParams = auth2ClientsParams; - this.initOAuth2Settings(this.auth2ClientsParams); + this.oauth2Info = oauth2Info; + this.initOAuth2Settings(this.oauth2Info); } ); } @@ -130,11 +147,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha this.templateProvider.sort(); } - get domainsParams(): FormArray { - return this.oauth2SettingsForm.get('domainsParams') as FormArray; - } - - private formBasicGroup(type: MapperConfigType, mapperConfigBasic?: MapperConfigBasic): FormGroup { + private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): FormGroup { let tenantNamePattern; if (mapperConfigBasic?.tenantNamePattern) { tenantNamePattern = mapperConfigBasic.tenantNamePattern; @@ -173,16 +186,16 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private buildOAuth2SettingsForm(): void { this.oauth2SettingsForm = this.fb.group({ - domainsParams: this.fb.array([]), + oauth2ParamsInfos: this.fb.array([]), enabled: [false] }); } - private initOAuth2Settings(auth2ClientsParams: OAuth2ClientsParams): void { - if (auth2ClientsParams) { - this.oauth2SettingsForm.patchValue({enabled: auth2ClientsParams.enabled}, {emitEvent: false}); - auth2ClientsParams.domainsParams.forEach((domain) => { - this.domainsParams.push(this.buildDomainsForm(domain)); + private initOAuth2Settings(oauth2Info: OAuth2Info): void { + if (oauth2Info) { + this.oauth2SettingsForm.patchValue({enabled: oauth2Info.enabled}, {emitEvent: false}); + oauth2Info.oauth2ParamsInfos.forEach((oauth2ParamsInfo) => { + this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm(oauth2ParamsInfo)); }); } } @@ -201,8 +214,20 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return null; } + private uniquePkgNameValidator(control: FormGroup): { [key: string]: boolean } | null { + if (control.parent?.value) { + const pkgName = control.value.pkgName; + const mobileInfosList = control.parent.getRawValue() + .filter((mobileInfo) => mobileInfo.pkgName === pkgName); + if (mobileInfosList.length > 1) { + return {unique: true}; + } + } + return null; + } + public domainListTittle(control: AbstractControl): string { - const domainInfos = control.get('domainInfos').value as DomainInfo[]; + const domainInfos = control.get('domainInfos').value as OAuth2DomainInfo[]; if (domainInfos.length) { const domainList = new Set(); domainInfos.forEach((domain) => { @@ -213,41 +238,52 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return this.translate.instant('admin.oauth2.new-domain'); } - private buildDomainsForm(auth2ClientsDomainParams?: OAuth2ClientsDomainParams): FormGroup { - const formDomain = this.fb.group({ + private buildOAuth2ParamsInfoForm(oauth2ParamsInfo?: OAuth2ParamsInfo): FormGroup { + const formOAuth2Params = this.fb.group({ domainInfos: this.fb.array([], Validators.required), + mobileInfos: this.fb.array([]), clientRegistrations: this.fb.array([], Validators.required) }); - if (auth2ClientsDomainParams) { - auth2ClientsDomainParams.domainInfos.forEach((domain) => { - this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain)); + if (oauth2ParamsInfo) { + oauth2ParamsInfo.domainInfos.forEach((domain) => { + this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm(domain)); }); - auth2ClientsDomainParams.clientRegistrations.forEach((registration) => { - this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration)); + oauth2ParamsInfo.mobileInfos.forEach((mobile) => { + this.mobileInfos(formOAuth2Params).push(this.buildMobileInfoForm(mobile)); + }); + oauth2ParamsInfo.clientRegistrations.forEach((registration) => { + this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm(registration)); }); } else { - this.clientDomainProviders(formDomain).push(this.buildProviderForm()); - this.clientDomainInfos(formDomain).push(this.buildDomainForm()); + this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm()); + this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm()); } - return formDomain; + return formOAuth2Params; } - private buildDomainForm(domainInfo?: DomainInfo): FormGroup { - const domain = this.fb.group({ + private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): FormGroup { + return this.fb.group({ name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ Validators.required, Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] }, {validators: this.uniqueDomainValidator}); - return domain; } - private buildProviderForm(clientRegistration?: ClientRegistration): FormGroup { + private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup { + return this.fb.group({ + pkgName: [mobileInfo?.pkgName, [Validators.required]], + appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048), + Validators.pattern(/^[A-Za-z0-9]+$/)]], + }, {validators: this.uniquePkgNameValidator}); + } + + private buildRegistrationForm(registration?: OAuth2RegistrationInfo): FormGroup { let additionalInfo = null; - if (isDefinedAndNotNull(clientRegistration?.additionalInfo)) { - additionalInfo = clientRegistration.additionalInfo; + if (isDefinedAndNotNull(registration?.additionalInfo)) { + additionalInfo = registration.additionalInfo; if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { additionalInfo.providerName = 'Custom'; } @@ -261,43 +297,43 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha additionalInfo: this.fb.group({ providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] }), - loginButtonLabel: [clientRegistration?.loginButtonLabel ? clientRegistration.loginButtonLabel : null, Validators.required], - loginButtonIcon: [clientRegistration?.loginButtonIcon ? clientRegistration.loginButtonIcon : null], - clientId: [clientRegistration?.clientId ? clientRegistration.clientId : '', Validators.required], - clientSecret: [clientRegistration?.clientSecret ? clientRegistration.clientSecret : '', Validators.required], - accessTokenUri: [clientRegistration?.accessTokenUri ? clientRegistration.accessTokenUri : '', - [Validators.required, - Validators.pattern(this.URL_REGEXP)]], - authorizationUri: [clientRegistration?.authorizationUri ? clientRegistration.authorizationUri : '', + platforms: [registration?.platforms ? registration.platforms : []], + loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required], + loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null], + clientId: [registration?.clientId ? registration.clientId : '', Validators.required], + clientSecret: [registration?.clientSecret ? registration.clientSecret : '', Validators.required], + accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], - scope: this.fb.array(clientRegistration?.scope ? clientRegistration.scope : [], this.validateScope), - jwkSetUri: [clientRegistration?.jwkSetUri ? clientRegistration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], - userInfoUri: [clientRegistration?.userInfoUri ? clientRegistration.userInfoUri : '', + authorizationUri: [registration?.authorizationUri ? registration.authorizationUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], + scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope), + jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], + userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', + [Validators.pattern(this.URL_REGEXP)]], clientAuthenticationMethod: [ - clientRegistration?.clientAuthenticationMethod ? clientRegistration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, + registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], userNameAttributeName: [ - clientRegistration?.userNameAttributeName ? clientRegistration.userNameAttributeName : 'email', Validators.required], + registration?.userNameAttributeName ? registration.userNameAttributeName : 'email', Validators.required], mapperConfig: this.fb.group({ allowUserCreation: [ - isDefinedAndNotNull(clientRegistration?.mapperConfig?.allowUserCreation) ? - clientRegistration.mapperConfig.allowUserCreation : true + isDefinedAndNotNull(registration?.mapperConfig?.allowUserCreation) ? + registration.mapperConfig.allowUserCreation : true ], activateUser: [ - isDefinedAndNotNull(clientRegistration?.mapperConfig?.activateUser) ? clientRegistration.mapperConfig.activateUser : false + isDefinedAndNotNull(registration?.mapperConfig?.activateUser) ? registration.mapperConfig.activateUser : false ], type: [ - clientRegistration?.mapperConfig?.type ? clientRegistration.mapperConfig.type : MapperConfigType.BASIC, Validators.required + registration?.mapperConfig?.type ? registration.mapperConfig.type : MapperConfigType.BASIC, Validators.required ] } ) }); - if (clientRegistration) { - this.changeMapperConfigType(clientRegistrationFormGroup, clientRegistration.mapperConfig.type, clientRegistration.mapperConfig); + if (registration) { + this.changeMapperConfigType(clientRegistrationFormGroup, registration.mapperConfig.type, registration.mapperConfig); } else { this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC); this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup); @@ -315,16 +351,6 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return clientRegistrationFormGroup; } - private validateScope(control: AbstractControl): ValidationErrors | null { - const scope: string[] = control.value; - if (!scope || !scope.length) { - return { - required: true - }; - } - return null; - } - private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) { if (provider === 'Custom') { clientRegistration.reset(this.defaultProvider, {emitEvent: false}); @@ -355,7 +381,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha } else { mapperConfig.removeControl('custom'); if (!mapperConfig.get('basic')) { - mapperConfig.addControl('basic', this.formBasicGroup(type, predefinedValue?.basic)); + mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); } if (type === MapperConfigType.GITHUB) { mapperConfig.get('basic.emailAttributeKey').disable(); @@ -370,7 +396,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha const setting = this.oauth2SettingsForm.getRawValue(); this.oauth2Service.saveOAuth2Settings(setting).subscribe( (oauth2Settings) => { - this.auth2ClientsParams = oauth2Settings; + this.oauth2Info = oauth2Settings; this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false}); this.oauth2SettingsForm.markAsUntouched(); this.oauth2SettingsForm.markAsPristine(); @@ -403,58 +429,62 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha controller.markAsDirty(); } - addDomain(): void { - this.domainsParams.push(this.buildDomainsForm()); + addOAuth2ParamsInfo(): void { + this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm()); } - deleteDomain($event: Event, index: number): void { + deleteOAuth2ParamsInfo($event: Event, index: number): void { if ($event) { $event.stopPropagation(); $event.preventDefault(); } - const domainName = this.domainListTittle(this.domainsParams.at(index)); + const domainName = this.domainListTittle(this.oauth2ParamsInfos.at(index)); this.dialogService.confirm( this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}), this.translate.instant('admin.oauth2.delete-domain-text'), null, this.translate.instant('action.delete') ).subscribe((data) => { if (data) { - this.domainsParams.removeAt(index); - this.domainsParams.markAsTouched(); - this.domainsParams.markAsDirty(); + this.oauth2ParamsInfos.removeAt(index); + this.oauth2ParamsInfos.markAsTouched(); + this.oauth2ParamsInfos.markAsDirty(); } }); } - clientDomainProviders(control: AbstractControl): FormArray { + clientRegistrations(control: AbstractControl): FormArray { return control.get('clientRegistrations') as FormArray; } - clientDomainInfos(control: AbstractControl): FormArray { + domainInfos(control: AbstractControl): FormArray { return control.get('domainInfos') as FormArray; } - addProvider(control: AbstractControl): void { - this.clientDomainProviders(control).push(this.buildProviderForm()); + mobileInfos(control: AbstractControl): FormArray { + return control.get('mobileInfos') as FormArray; } - deleteProvider($event: Event, control: AbstractControl, index: number): void { + addRegistration(control: AbstractControl): void { + this.clientRegistrations(control).push(this.buildRegistrationForm()); + } + + deleteRegistration($event: Event, control: AbstractControl, index: number): void { if ($event) { $event.stopPropagation(); $event.preventDefault(); } - const providerName = this.clientDomainProviders(control).at(index).get('additionalInfo.providerName').value; + const providerName = this.clientRegistrations(control).at(index).get('additionalInfo.providerName').value; this.dialogService.confirm( this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}), this.translate.instant('admin.oauth2.delete-registration-text'), null, this.translate.instant('action.delete') ).subscribe((data) => { if (data) { - this.clientDomainProviders(control).removeAt(index); - this.clientDomainProviders(control).markAsTouched(); - this.clientDomainProviders(control).markAsDirty(); + this.clientRegistrations(control).removeAt(index); + this.clientRegistrations(control).markAsTouched(); + this.clientRegistrations(control).markAsDirty(); } }); } @@ -480,24 +510,41 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha } addDomainInfo(control: AbstractControl): void { - this.clientDomainInfos(control).push(this.buildDomainForm({ + this.domainInfos(control).push(this.buildDomainInfoForm({ name: '', scheme: DomainSchema.HTTPS })); } - removeDomain($event: Event, control: AbstractControl, index: number): void { + removeDomainInfo($event: Event, control: AbstractControl, index: number): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.domainInfos(control).removeAt(index); + this.domainInfos(control).markAsTouched(); + this.domainInfos(control).markAsDirty(); + } + + addMobileInfo(control: AbstractControl): void { + this.mobileInfos(control).push(this.buildMobileInfoForm({ + pkgName: '', + appSecret: randomAlphanumeric(24) + })); + } + + removeMobileInfo($event: Event, control: AbstractControl, index: number): void { if ($event) { $event.stopPropagation(); $event.preventDefault(); } - this.clientDomainInfos(control).removeAt(index); - this.clientDomainInfos(control).markAsTouched(); - this.clientDomainInfos(control).markAsDirty(); + this.mobileInfos(control).removeAt(index); + this.mobileInfos(control).markAsTouched(); + this.mobileInfos(control).markAsDirty(); } redirectURI(control: AbstractControl, schema?: DomainSchema): string { - const domainInfo = control.value as DomainInfo; + const domainInfo = control.value as OAuth2DomainInfo; if (domainInfo.name !== '') { let protocol; if (isDefined(schema)) { diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index 804b349cff..2c53f04986 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -32,7 +32,7 @@ import { }) export class DeviceProfileTabsComponent extends EntityTabsComponent { - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts index 49a256dd55..98281c1f88 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -104,7 +104,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve this.deviceProfileService.getDeviceProfiles(pageLink); this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id); - this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile); + this.config.saveEntity = (deviceProfile, originDeviceProfile) => + this.deviceProfileService.saveDeviceProfileAndConfirmOtaChange(originDeviceProfile, deviceProfile); this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id); this.config.onEntityAction = action => this.onDeviceProfileAction(action); this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 1ed9f784f1..b55498ed05 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -35,6 +35,7 @@ import { TranslateService } from '@ngx-translate/core'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { Subject } from 'rxjs'; import { OtaUpdateType } from '@shared/models/ota-package.models'; +import { distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'tb-device', @@ -78,7 +79,7 @@ export class DeviceComponent extends EntityComponent { } buildForm(entity: DeviceInfo): FormGroup { - return this.fb.group( + const form = this.fb.group( { name: [entity ? entity.name : '', [Validators.required]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], @@ -95,6 +96,17 @@ export class DeviceComponent extends EntityComponent { ) } ); + form.get('deviceProfileId').valueChanges.pipe( + distinctUntilChanged((prev, curr) => prev?.id === curr?.id) + ).subscribe(profileId => { + if (profileId && this.isEdit) { + this.entityForm.patchValue({ + firmwareId: null, + softwareId: null + }, {emitEvent: false}); + } + }); + return form; } updateForm(entity: DeviceInfo) { @@ -156,10 +168,6 @@ export class DeviceComponent extends EntityComponent { this.entityForm.markAsDirty(); } } - this.entityForm.patchValue({ - firmwareId: null, - softwareId: null - }); } } } diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index b22d43da46..0f542bec8a 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -17,6 +17,7 @@ import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; import { + CellActionDescriptorType, DateEntityTableColumn, EntityTableColumn, EntityTableConfig @@ -35,6 +36,8 @@ import { PageLink } from '@shared/models/page/page-link'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; @Injectable() export class OtaUpdateTableConfigResolve implements Resolve> { @@ -44,6 +47,7 @@ export class OtaUpdateTableConfigResolve implements Resolve, private otaPackageService: OtaPackageService, private fileSize: FileSizePipe) { this.config.entityType = EntityType.OTA_PACKAGE; @@ -55,25 +59,50 @@ export class OtaUpdateTableConfigResolve implements Resolve('createdTime', 'common.created-time', this.datePipe, '150px'), - new EntityTableColumn('title', 'ota-update.title', '25%'), - new EntityTableColumn('version', 'ota-update.version', '25%'), - new EntityTableColumn('type', 'ota-update.package-type', '25%', entity => { + new EntityTableColumn('title', 'ota-update.title', '20%'), + new EntityTableColumn('version', 'ota-update.version', '20%'), + new EntityTableColumn('type', 'ota-update.package-type', '20%', entity => { return this.translate.instant(OtaUpdateTypeTranslationMap.get(entity.type)); }), - new EntityTableColumn('fileName', 'ota-update.file-name', '25%'), + new EntityTableColumn('url', 'ota-update.direct-url', '20%', entity => { + return entity.url ? (entity.url.length > 20 ? `${entity.url.slice(0, 20)}…` : entity.url) : ''; + }, () => ({}), true, () => ({}), () => undefined, false, + { + name: this.translate.instant('ota-update.copy-direct-url'), + icon: 'content_paste', + style: { + 'font-size': '16px', + color: 'rgba(0,0,0,.87)' + }, + isEnabled: (otaPackage) => !!otaPackage.url, + onAction: ($event, entity) => entity.url, + type: CellActionDescriptorType.COPY_BUTTON + }), + new EntityTableColumn('fileName', 'ota-update.file-name', '20%'), new EntityTableColumn('dataSize', 'ota-update.file-size', '70px', entity => { - return this.fileSize.transform(entity.dataSize || 0); + return entity.dataSize ? this.fileSize.transform(entity.dataSize) : ''; }), - new EntityTableColumn('checksum', 'ota-update.checksum', '540px', entity => { - return `${ChecksumAlgorithmTranslationMap.get(entity.checksumAlgorithm)}: ${entity.checksum}`; - }, () => ({}), false) + new EntityTableColumn('checksum', 'ota-update.checksum', '220px', entity => { + return entity.checksum ? this.checksumText(entity) : ''; + }, () => ({}), true, () => ({}), () => undefined, false, + { + name: this.translate.instant('ota-update.copy-checksum'), + icon: 'content_paste', + style: { + 'font-size': '16px', + color: 'rgba(0,0,0,.87)' + }, + isEnabled: (otaPackage) => !!otaPackage.checksum, + onAction: ($event, entity) => entity.checksum, + type: CellActionDescriptorType.COPY_BUTTON + }) ); this.config.cellActionDescriptors.push( { name: this.translate.instant('ota-update.download'), icon: 'file_download', - isEnabled: (otaPackage) => otaPackage.hasData, + isEnabled: (otaPackage) => otaPackage.hasData && !otaPackage.url, onAction: ($event, entity) => this.exportPackage($event, entity) } ); @@ -101,7 +130,19 @@ export class OtaUpdateTableConfigResolve implements Resolve 20) { + text = `${text.slice(0, 20)}…`; + } + return text; } onPackageAction(action: EntityAction): boolean { diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index 77a68f72e5..6802768303 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -17,7 +17,7 @@ -->
+
-
+
ota-update.title @@ -69,6 +77,7 @@ -
ota-update.warning-after-save-no-edit
-
- - ota-update.checksum-algorithm - - - {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }} - - - - - ota-update.checksum - - -
- - +
ota-update.warning-after-save-no-edit
+ + Upload binary file + Use external URL +
-
-
+
+
+ + + + {{ 'ota-update.auto-generate-checksum' | translate }} + +
+
- ota-update.file-name - - - - ota-update.file-size-bytes - + ota-update.checksum-algorithm + + + {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }} + + - ota-update.content-type - + ota-update.checksum + + ota-update.checksum-hint
+
+
+ + ota-update.file-name + + + + ota-update.file-size-bytes + + + + ota-update.content-type + + +
+
+
+
+ + ota-update.direct-url + + + ota-update.direct-url-required + +
diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts index fd4cd7ae27..0c2d82e171 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.ts @@ -30,6 +30,8 @@ import { OtaUpdateTypeTranslationMap } from '@shared/models/ota-package.models'; import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { filter, takeUntil } from 'rxjs/operators'; +import { isNotEmptyStr } from '@core/utils'; @Component({ selector: 'tb-ota-update', @@ -52,6 +54,26 @@ export class OtaUpdateComponent extends EntityComponent implements O super(store, fb, entityValue, entitiesTableConfigValue); } + ngOnInit() { + super.ngOnInit(); + this.entityForm.get('resource').valueChanges.pipe( + filter(() => this.isAdd), + takeUntil(this.destroy$) + ).subscribe((resource) => { + if (resource === 'file') { + this.entityForm.get('url').clearValidators(); + this.entityForm.get('file').setValidators(Validators.required); + this.entityForm.get('url').updateValueAndValidity({emitEvent: false}); + this.entityForm.get('file').updateValueAndValidity({emitEvent: false}); + } else { + this.entityForm.get('file').clearValidators(); + this.entityForm.get('url').setValidators([Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]); + this.entityForm.get('file').updateValueAndValidity({emitEvent: false}); + this.entityForm.get('url').updateValueAndValidity({emitEvent: false}); + } + }); + } + ngOnDestroy() { super.ngOnDestroy(); this.destroy$.next(); @@ -74,6 +96,8 @@ export class OtaUpdateComponent extends EntityComponent implements O deviceProfileId: [entity ? entity.deviceProfileId : null, Validators.required], checksumAlgorithm: [entity && entity.checksumAlgorithm ? entity.checksumAlgorithm : ChecksumAlgorithm.SHA256], checksum: [entity ? entity.checksum : '', Validators.maxLength(1020)], + url: [entity ? entity.url : ''], + resource: ['file'], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], @@ -82,6 +106,7 @@ export class OtaUpdateComponent extends EntityComponent implements O }); if (this.isAdd) { form.addControl('file', this.fb.control(null, Validators.required)); + form.addControl('generateChecksum', this.fb.control(true)); } else { form.addControl('fileName', this.fb.control(null)); form.addControl('dataSize', this.fb.control(null)); @@ -101,6 +126,8 @@ export class OtaUpdateComponent extends EntityComponent implements O fileName: entity.fileName, dataSize: entity.dataSize, contentType: entity.contentType, + url: entity.url, + resource: isNotEmptyStr(entity.url) ? 'url' : 'file', additionalInfo: { description: entity.additionalInfo ? entity.additionalInfo.description : '' } @@ -108,8 +135,6 @@ export class OtaUpdateComponent extends EntityComponent implements O if (!this.isAdd && this.entityForm.enabled) { this.entityForm.disable({emitEvent: false}); this.entityForm.get('additionalInfo').enable({emitEvent: false}); - // this.entityForm.get('dataSize').disable({emitEvent: false}); - // this.entityForm.get('contentType').disable({emitEvent: false}); } } @@ -134,4 +159,26 @@ export class OtaUpdateComponent extends EntityComponent implements O horizontalPosition: 'right' })); } + + onPackageDirectUrlCopied() { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('ota-update.checksum-copied-message'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + prepareFormValue(formValue: any): any { + if (formValue.resource === 'url') { + delete formValue.file; + } else { + delete formValue.url; + } + delete formValue.resource; + delete formValue.generateChecksum; + return super.prepareFormValue(formValue); + } } diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.html b/ui-ngx/src/app/shared/components/button/copy-button.component.html new file mode 100644 index 0000000000..92808e25d5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.html @@ -0,0 +1,31 @@ + + diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.scss b/ui-ngx/src/app/shared/components/button/copy-button.component.scss new file mode 100644 index 0000000000..0e5f5de8d9 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-icon-button{ + height: 32px; + width: 32px; + line-height: 32px; + .mat-icon.copied{ + color: #00C851 !important; + } + } + &:hover{ + .mat-icon{ + color: #28567E !important; + } + } +} diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.ts b/ui-ngx/src/app/shared/components/button/copy-button.component.ts new file mode 100644 index 0000000000..60f5db12e0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ClipboardService } from 'ngx-clipboard'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-copy-button', + styleUrls: ['copy-button.component.scss'], + templateUrl: './copy-button.component.html' +}) +export class CopyButtonComponent { + + private timer; + + copied = false; + + @Input() + copyText: string; + + @Input() + disabled = false; + + @Input() + mdiIcon: string; + + @Input() + icon: string; + + @Input() + tooltipText: string; + + @Input() + tooltipPosition: TooltipPosition; + + @Input() + style: {[key: string]: any} = {}; + + @Input() + color: string; + + @Output() + successCopied = new EventEmitter(); + + constructor(private clipboardService: ClipboardService, + private translate: TranslateService, + private cd: ChangeDetectorRef) { + } + + copy($event: Event): void { + $event.stopPropagation(); + if (this.timer) { + clearTimeout(this.timer); + } + this.clipboardService.copy(this.copyText); + this.successCopied.emit(this.copyText); + this.copied = true; + this.timer = setTimeout(() => { + this.copied = false; + this.cd.detectChanges(); + }, 1500); + } + + get matTooltipText(): string { + return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; + } + + get matTooltipPosition(): TooltipPosition { + return this.copied ? 'below' : this.tooltipPosition; + } + +} diff --git a/ui-ngx/src/app/shared/components/color-input.component.html b/ui-ngx/src/app/shared/components/color-input.component.html index fbf8aea853..a0d226f589 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.html +++ b/ui-ngx/src/app/shared/components/color-input.component.html @@ -20,7 +20,7 @@ {{icon}} {{label}} -
+
diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html index cf36de3a7d..c1cef05057 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + + [matAutocomplete]="packageAutocomplete" + [matAutocompleteDisabled]="disabled">