687 changed files with 29638 additions and 5350 deletions
@ -1,700 +0,0 @@ |
|||||
{ |
|
||||
"title": "Raspberry PI GPIO Demo Dashboard", |
|
||||
"configuration": { |
|
||||
"description": "Demo dashboard for Raspberry PI GPIO Demo", |
|
||||
"widgets": { |
|
||||
"602177f6-267b-cb87-4e8f-e23d7fb2f61c": { |
|
||||
"isSystemType": true, |
|
||||
"bundleAlias": "gpio_widgets", |
|
||||
"typeAlias": "raspberry_pi_gpio_control", |
|
||||
"type": "rpc", |
|
||||
"title": "New widget", |
|
||||
"sizeX": 6, |
|
||||
"sizeY": 10, |
|
||||
"config": { |
|
||||
"targetDeviceAliases": [], |
|
||||
"showTitle": true, |
|
||||
"backgroundColor": "#fff", |
|
||||
"color": "rgba(0, 0, 0, 0.87)", |
|
||||
"padding": "0px", |
|
||||
"settings": { |
|
||||
"parseGpioStatusFunction": "return body[pin] === true;", |
|
||||
"gpioStatusChangeRequest": { |
|
||||
"method": "setGpioStatus", |
|
||||
"paramsBody": "{\n \"pin\": \"{$pin}\",\n \"enabled\": \"{$enabled}\"\n}" |
|
||||
}, |
|
||||
"requestTimeout": 500, |
|
||||
"switchPanelBackgroundColor": "#008a00", |
|
||||
"gpioStatusRequest": { |
|
||||
"method": "getGpioStatus", |
|
||||
"paramsBody": "{}" |
|
||||
}, |
|
||||
"gpioList": [ |
|
||||
{ |
|
||||
"pin": 7, |
|
||||
"label": "GPIO 4 (GPCLK0)", |
|
||||
"row": 3, |
|
||||
"col": 0, |
|
||||
"_uniqueKey": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"pin": 11, |
|
||||
"label": "GPIO 17", |
|
||||
"row": 5, |
|
||||
"col": 0, |
|
||||
"_uniqueKey": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"pin": 12, |
|
||||
"label": "GPIO 18", |
|
||||
"row": 5, |
|
||||
"col": 1, |
|
||||
"_uniqueKey": 2 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 3, |
|
||||
"pin": 13, |
|
||||
"label": "GPIO 27", |
|
||||
"row": 6, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 4, |
|
||||
"pin": 15, |
|
||||
"label": "GPIO 22", |
|
||||
"row": 7, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 5, |
|
||||
"pin": 16, |
|
||||
"label": "GPIO 23", |
|
||||
"row": 7, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 6, |
|
||||
"pin": 18, |
|
||||
"label": "GPIO 24", |
|
||||
"row": 8, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 7, |
|
||||
"pin": 22, |
|
||||
"label": "GPIO 25", |
|
||||
"row": 10, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 8, |
|
||||
"pin": 29, |
|
||||
"label": "GPIO 5", |
|
||||
"row": 14, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 9, |
|
||||
"pin": 31, |
|
||||
"label": "GPIO 6", |
|
||||
"row": 15, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 10, |
|
||||
"pin": 32, |
|
||||
"label": "GPIO 12", |
|
||||
"row": 15, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 11, |
|
||||
"pin": 33, |
|
||||
"label": "GPIO 13", |
|
||||
"row": 16, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 12, |
|
||||
"pin": 35, |
|
||||
"label": "GPIO 19", |
|
||||
"row": 17, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 13, |
|
||||
"pin": 36, |
|
||||
"label": "GPIO 16", |
|
||||
"row": 17, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 14, |
|
||||
"pin": 37, |
|
||||
"label": "GPIO 26", |
|
||||
"row": 18, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 15, |
|
||||
"pin": 38, |
|
||||
"label": "GPIO 20", |
|
||||
"row": 18, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"_uniqueKey": 16, |
|
||||
"pin": 40, |
|
||||
"label": "GPIO 21", |
|
||||
"row": 19, |
|
||||
"col": 1 |
|
||||
} |
|
||||
] |
|
||||
}, |
|
||||
"title": "Raspberry Pi GPIO Control Panel", |
|
||||
"datasources": [], |
|
||||
"targetDeviceAliasIds": [ |
|
||||
"f26b12b6-6938-e1a0-85ec-d88a1f23e382" |
|
||||
] |
|
||||
}, |
|
||||
"row": 0, |
|
||||
"col": 0, |
|
||||
"id": "602177f6-267b-cb87-4e8f-e23d7fb2f61c" |
|
||||
}, |
|
||||
"3cca52a5-e874-eb43-b444-8efa01e663c8": { |
|
||||
"isSystemType": true, |
|
||||
"bundleAlias": "gpio_widgets", |
|
||||
"typeAlias": "raspberry_pi_gpio_panel", |
|
||||
"type": "latest", |
|
||||
"title": "New widget", |
|
||||
"sizeX": 7, |
|
||||
"sizeY": 10, |
|
||||
"config": { |
|
||||
"showTitle": true, |
|
||||
"backgroundColor": "#fff", |
|
||||
"color": "rgba(0, 0, 0, 0.87)", |
|
||||
"padding": "0px", |
|
||||
"settings": { |
|
||||
"gpioList": [ |
|
||||
{ |
|
||||
"pin": 1, |
|
||||
"label": "3.3V", |
|
||||
"row": 0, |
|
||||
"col": 0, |
|
||||
"color": "#fc9700", |
|
||||
"_uniqueKey": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"pin": 2, |
|
||||
"label": "5V", |
|
||||
"row": 0, |
|
||||
"col": 1, |
|
||||
"color": "#fb0000", |
|
||||
"_uniqueKey": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"pin": 3, |
|
||||
"label": "GPIO 2 (I2C1_SDA)", |
|
||||
"row": 1, |
|
||||
"col": 0, |
|
||||
"color": "#02fefb", |
|
||||
"_uniqueKey": 2 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fb0000", |
|
||||
"pin": 4, |
|
||||
"label": "5V", |
|
||||
"row": 1, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#02fefb", |
|
||||
"pin": 5, |
|
||||
"label": "GPIO 3 (I2C1_SCL)", |
|
||||
"row": 2, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 6, |
|
||||
"label": "GND", |
|
||||
"row": 2, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 7, |
|
||||
"label": "GPIO 4 (GPCLK0)", |
|
||||
"row": 3, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fdfb00", |
|
||||
"pin": 8, |
|
||||
"label": "GPIO 14 (UART_TXD)", |
|
||||
"row": 3, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 9, |
|
||||
"label": "GND", |
|
||||
"row": 4, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fdfb00", |
|
||||
"pin": 10, |
|
||||
"label": "GPIO 15 (UART_RXD)", |
|
||||
"row": 4, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 11, |
|
||||
"label": "GPIO 17", |
|
||||
"row": 5, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 12, |
|
||||
"label": "GPIO 18", |
|
||||
"row": 5, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 13, |
|
||||
"label": "GPIO 27", |
|
||||
"row": 6, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 14, |
|
||||
"label": "GND", |
|
||||
"row": 6, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 15, |
|
||||
"label": "GPIO 22", |
|
||||
"row": 7, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 16, |
|
||||
"label": "GPIO 23", |
|
||||
"row": 7, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fc9700", |
|
||||
"pin": 17, |
|
||||
"label": "3.3V", |
|
||||
"row": 8, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 18, |
|
||||
"label": "GPIO 24", |
|
||||
"row": 8, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fd01fd", |
|
||||
"pin": 19, |
|
||||
"label": "GPIO 10 (SPI_MOSI)", |
|
||||
"row": 9, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 20, |
|
||||
"label": "GND", |
|
||||
"row": 9, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fd01fd", |
|
||||
"pin": 21, |
|
||||
"label": "GPIO 9 (SPI_MISO)", |
|
||||
"row": 10, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 22, |
|
||||
"label": "GPIO 25", |
|
||||
"row": 10, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fd01fd", |
|
||||
"pin": 23, |
|
||||
"label": "GPIO 11 (SPI_SCLK)", |
|
||||
"row": 11, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fd01fd", |
|
||||
"pin": 24, |
|
||||
"label": "GPIO 8 (SPI_CE0)", |
|
||||
"row": 11, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 25, |
|
||||
"label": "GND", |
|
||||
"row": 12, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#fd01fd", |
|
||||
"pin": 26, |
|
||||
"label": "GPIO 7 (SPI_CE1)", |
|
||||
"row": 12, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#ffffff", |
|
||||
"pin": 27, |
|
||||
"label": "ID_SD", |
|
||||
"row": 13, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#ffffff", |
|
||||
"pin": 28, |
|
||||
"label": "ID_SC", |
|
||||
"row": 13, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 29, |
|
||||
"label": "GPIO 5", |
|
||||
"row": 14, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 30, |
|
||||
"label": "GND", |
|
||||
"row": 14, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 31, |
|
||||
"label": "GPIO 6", |
|
||||
"row": 15, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 32, |
|
||||
"label": "GPIO 12", |
|
||||
"row": 15, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 33, |
|
||||
"label": "GPIO 13", |
|
||||
"row": 16, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 34, |
|
||||
"label": "GND", |
|
||||
"row": 16, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 35, |
|
||||
"label": "GPIO 19", |
|
||||
"row": 17, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 36, |
|
||||
"label": "GPIO 16", |
|
||||
"row": 17, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 37, |
|
||||
"label": "GPIO 26", |
|
||||
"row": 18, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 38, |
|
||||
"label": "GPIO 20", |
|
||||
"row": 18, |
|
||||
"col": 1 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#000000", |
|
||||
"pin": 39, |
|
||||
"label": "GND", |
|
||||
"row": 19, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
{ |
|
||||
"color": "#00fd00", |
|
||||
"pin": 40, |
|
||||
"label": "GPIO 21", |
|
||||
"row": 19, |
|
||||
"col": 1 |
|
||||
} |
|
||||
], |
|
||||
"ledPanelBackgroundColor": "#008a00" |
|
||||
}, |
|
||||
"title": "Raspberry Pi GPIO Status Panel", |
|
||||
"datasources": [ |
|
||||
{ |
|
||||
"type": "entity", |
|
||||
"dataKeys": [ |
|
||||
{ |
|
||||
"name": "7", |
|
||||
"type": "attribute", |
|
||||
"label": "7", |
|
||||
"color": "#2196f3", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.20925966435886978 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "11", |
|
||||
"type": "attribute", |
|
||||
"label": "11", |
|
||||
"color": "#4caf50", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.330267349594344 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "12", |
|
||||
"type": "attribute", |
|
||||
"label": "12", |
|
||||
"color": "#f44336", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.5040578704481748 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "13", |
|
||||
"type": "attribute", |
|
||||
"label": "13", |
|
||||
"color": "#ffc107", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.588956328191639 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "15", |
|
||||
"type": "attribute", |
|
||||
"label": "15", |
|
||||
"color": "#607d8b", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.9229040530336119 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "16", |
|
||||
"type": "attribute", |
|
||||
"label": "16", |
|
||||
"color": "#9c27b0", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.8692315253041654 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "18", |
|
||||
"type": "attribute", |
|
||||
"label": "18", |
|
||||
"color": "#8bc34a", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.41465562857521543 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "22", |
|
||||
"type": "attribute", |
|
||||
"label": "22", |
|
||||
"color": "#3f51b5", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.36135260043112827 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "29", |
|
||||
"type": "attribute", |
|
||||
"label": "29", |
|
||||
"color": "#e91e63", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.9904592276182183 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "31", |
|
||||
"type": "attribute", |
|
||||
"label": "31", |
|
||||
"color": "#ffeb3b", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.038330985429919195 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "32", |
|
||||
"type": "attribute", |
|
||||
"label": "32", |
|
||||
"color": "#03a9f4", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.4334683890135089 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "33", |
|
||||
"type": "attribute", |
|
||||
"label": "33", |
|
||||
"color": "#ff9800", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.6487255992492305 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "35", |
|
||||
"type": "attribute", |
|
||||
"label": "35", |
|
||||
"color": "#673ab7", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.971555321150732 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "36", |
|
||||
"type": "attribute", |
|
||||
"label": "36", |
|
||||
"color": "#cddc39", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.7826129728424382 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "37", |
|
||||
"type": "attribute", |
|
||||
"label": "37", |
|
||||
"color": "#009688", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.44925676517537627 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "38", |
|
||||
"type": "attribute", |
|
||||
"label": "38", |
|
||||
"color": "#795548", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.051518155759787465 |
|
||||
}, |
|
||||
{ |
|
||||
"name": "40", |
|
||||
"type": "attribute", |
|
||||
"label": "40", |
|
||||
"color": "#00bcd4", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.8733296686871144 |
|
||||
} |
|
||||
], |
|
||||
"name": "RPi", |
|
||||
"entityAliasId": "f26b12b6-6938-e1a0-85ec-d88a1f23e382" |
|
||||
} |
|
||||
], |
|
||||
"timewindow": { |
|
||||
"realtime": { |
|
||||
"timewindowMs": 60000 |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"row": 0, |
|
||||
"col": 6, |
|
||||
"id": "3cca52a5-e874-eb43-b444-8efa01e663c8" |
|
||||
} |
|
||||
}, |
|
||||
"states": { |
|
||||
"default": { |
|
||||
"name": "Default", |
|
||||
"root": true, |
|
||||
"layouts": { |
|
||||
"main": { |
|
||||
"widgets": { |
|
||||
"602177f6-267b-cb87-4e8f-e23d7fb2f61c": { |
|
||||
"sizeX": 6, |
|
||||
"sizeY": 10, |
|
||||
"row": 0, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
"3cca52a5-e874-eb43-b444-8efa01e663c8": { |
|
||||
"sizeX": 7, |
|
||||
"sizeY": 10, |
|
||||
"row": 0, |
|
||||
"col": 6 |
|
||||
} |
|
||||
}, |
|
||||
"gridSettings": { |
|
||||
"backgroundColor": "#eeeeee", |
|
||||
"color": "rgba(0,0,0,0.870588)", |
|
||||
"columns": 24, |
|
||||
"margins": [ |
|
||||
10, |
|
||||
10 |
|
||||
], |
|
||||
"backgroundSizeMode": "100%" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"entityAliases": { |
|
||||
"f26b12b6-6938-e1a0-85ec-d88a1f23e382": { |
|
||||
"id": "f26b12b6-6938-e1a0-85ec-d88a1f23e382", |
|
||||
"alias": "RPi", |
|
||||
"filter": { |
|
||||
"type": "entityName", |
|
||||
"resolveMultiple": false, |
|
||||
"entityType": "DEVICE", |
|
||||
"entityNameFilter": "Raspberry Pi Demo Device" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"timewindow": { |
|
||||
"displayValue": "", |
|
||||
"selectedTab": 0, |
|
||||
"realtime": { |
|
||||
"interval": 1000, |
|
||||
"timewindowMs": 60000 |
|
||||
}, |
|
||||
"history": { |
|
||||
"historyType": 0, |
|
||||
"interval": 1000, |
|
||||
"timewindowMs": 60000, |
|
||||
"fixedTimewindow": { |
|
||||
"startTimeMs": 1498653734150, |
|
||||
"endTimeMs": 1498740134150 |
|
||||
} |
|
||||
}, |
|
||||
"aggregation": { |
|
||||
"type": "AVG", |
|
||||
"limit": 200 |
|
||||
} |
|
||||
}, |
|
||||
"settings": { |
|
||||
"stateControllerId": "default", |
|
||||
"showTitle": true, |
|
||||
"showDashboardsSelect": true, |
|
||||
"showEntitiesSelect": true, |
|
||||
"showDashboardTimewindow": true, |
|
||||
"showDashboardExport": true, |
|
||||
"toolbarAlwaysOpen": false |
|
||||
} |
|
||||
}, |
|
||||
"name": "Raspberry PI GPIO Demo Dashboard" |
|
||||
} |
|
||||
@ -1,264 +0,0 @@ |
|||||
{ |
|
||||
"title": "Temperature & Humidity Demo Dashboard", |
|
||||
"configuration": { |
|
||||
"description": "Demo dashboard for sample applications that upload temperature and humidity received from DHT11 or DHT22 sensors", |
|
||||
"widgets": { |
|
||||
"03e06986-1c50-e9e4-267c-2bae930ad9a2": { |
|
||||
"isSystemType": true, |
|
||||
"bundleAlias": "digital_gauges", |
|
||||
"typeAlias": "digital_thermometer", |
|
||||
"type": "latest", |
|
||||
"title": "New widget", |
|
||||
"sizeX": 5, |
|
||||
"sizeY": 5, |
|
||||
"config": { |
|
||||
"datasources": [ |
|
||||
{ |
|
||||
"type": "entity", |
|
||||
"dataKeys": [ |
|
||||
{ |
|
||||
"name": "temperature", |
|
||||
"type": "timeseries", |
|
||||
"label": "temperature", |
|
||||
"color": "#2196f3", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.3720839051412099 |
|
||||
} |
|
||||
], |
|
||||
"name": "DHT11", |
|
||||
"entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62" |
|
||||
} |
|
||||
], |
|
||||
"timewindow": { |
|
||||
"realtime": { |
|
||||
"timewindowMs": 60000 |
|
||||
} |
|
||||
}, |
|
||||
"showTitle": false, |
|
||||
"backgroundColor": "#000000", |
|
||||
"color": "rgba(0, 0, 0, 0.87)", |
|
||||
"padding": "0px", |
|
||||
"settings": { |
|
||||
"maxValue": 50, |
|
||||
"donutStartAngle": 90, |
|
||||
"showValue": true, |
|
||||
"showMinMax": true, |
|
||||
"gaugeWidthScale": 1, |
|
||||
"levelColors": [ |
|
||||
"#304ffe", |
|
||||
"#7e57c2", |
|
||||
"#ff4081", |
|
||||
"#d32f2f" |
|
||||
], |
|
||||
"refreshAnimationType": "<>", |
|
||||
"refreshAnimationTime": 700, |
|
||||
"startAnimationType": "<>", |
|
||||
"startAnimationTime": 700, |
|
||||
"titleFont": { |
|
||||
"family": "RobotoDraft", |
|
||||
"size": 12, |
|
||||
"style": "normal", |
|
||||
"weight": "500" |
|
||||
}, |
|
||||
"labelFont": { |
|
||||
"family": "RobotoDraft", |
|
||||
"size": 8, |
|
||||
"style": "normal", |
|
||||
"weight": "500" |
|
||||
}, |
|
||||
"valueFont": { |
|
||||
"family": "Segment7Standard", |
|
||||
"style": "normal", |
|
||||
"weight": "500", |
|
||||
"size": 18 |
|
||||
}, |
|
||||
"minMaxFont": { |
|
||||
"family": "Segment7Standard", |
|
||||
"size": 12, |
|
||||
"style": "normal", |
|
||||
"weight": "500" |
|
||||
}, |
|
||||
"dashThickness": 1.5, |
|
||||
"decimals": 0, |
|
||||
"minValue": 0, |
|
||||
"units": "°C", |
|
||||
"gaugeColor": "#333333", |
|
||||
"neonGlowBrightness": 35, |
|
||||
"gaugeType": "donut", |
|
||||
"showTitle": false |
|
||||
}, |
|
||||
"title": "Temperature" |
|
||||
}, |
|
||||
"row": 0, |
|
||||
"col": 0, |
|
||||
"id": "03e06986-1c50-e9e4-267c-2bae930ad9a2" |
|
||||
}, |
|
||||
"88808eb1-d381-9970-c852-e3499df68bd8": { |
|
||||
"isSystemType": true, |
|
||||
"bundleAlias": "digital_gauges", |
|
||||
"typeAlias": "digital_vertical_bar", |
|
||||
"type": "latest", |
|
||||
"title": "New widget", |
|
||||
"sizeX": 3, |
|
||||
"sizeY": 5, |
|
||||
"config": { |
|
||||
"datasources": [ |
|
||||
{ |
|
||||
"type": "entity", |
|
||||
"dataKeys": [ |
|
||||
{ |
|
||||
"name": "humidity", |
|
||||
"type": "timeseries", |
|
||||
"label": "humidity", |
|
||||
"color": "#2196f3", |
|
||||
"settings": {}, |
|
||||
"_hash": 0.9492802776509441 |
|
||||
} |
|
||||
], |
|
||||
"name": "DHT11", |
|
||||
"entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62" |
|
||||
} |
|
||||
], |
|
||||
"timewindow": { |
|
||||
"realtime": { |
|
||||
"timewindowMs": 60000 |
|
||||
} |
|
||||
}, |
|
||||
"showTitle": false, |
|
||||
"backgroundColor": "#000000", |
|
||||
"color": "rgba(0, 0, 0, 0.87)", |
|
||||
"padding": "0px", |
|
||||
"settings": { |
|
||||
"maxValue": 100, |
|
||||
"donutStartAngle": 90, |
|
||||
"showValue": true, |
|
||||
"showMinMax": true, |
|
||||
"gaugeWidthScale": 0.75, |
|
||||
"levelColors": [ |
|
||||
"#3d5afe", |
|
||||
"#f44336" |
|
||||
], |
|
||||
"refreshAnimationType": "<>", |
|
||||
"refreshAnimationTime": 700, |
|
||||
"startAnimationType": "<>", |
|
||||
"startAnimationTime": 700, |
|
||||
"titleFont": { |
|
||||
"family": "RobotoDraft", |
|
||||
"size": 12, |
|
||||
"style": "normal", |
|
||||
"weight": "500" |
|
||||
}, |
|
||||
"labelFont": { |
|
||||
"family": "RobotoDraft", |
|
||||
"size": 8, |
|
||||
"style": "normal", |
|
||||
"weight": "500" |
|
||||
}, |
|
||||
"valueFont": { |
|
||||
"family": "Segment7Standard", |
|
||||
"style": "normal", |
|
||||
"weight": "500", |
|
||||
"size": 14 |
|
||||
}, |
|
||||
"minMaxFont": { |
|
||||
"family": "Segment7Standard", |
|
||||
"size": 8, |
|
||||
"style": "normal", |
|
||||
"weight": "normal", |
|
||||
"color": "#cccccc" |
|
||||
}, |
|
||||
"neonGlowBrightness": 20, |
|
||||
"decimals": 0, |
|
||||
"showUnitTitle": true, |
|
||||
"gaugeColor": "#171a1c", |
|
||||
"gaugeType": "verticalBar", |
|
||||
"showTitle": false, |
|
||||
"minValue": 0, |
|
||||
"dashThickness": 1.2 |
|
||||
}, |
|
||||
"title": "Humidity" |
|
||||
}, |
|
||||
"row": 0, |
|
||||
"col": 5, |
|
||||
"id": "88808eb1-d381-9970-c852-e3499df68bd8" |
|
||||
} |
|
||||
}, |
|
||||
"states": { |
|
||||
"default": { |
|
||||
"name": "Default", |
|
||||
"root": true, |
|
||||
"layouts": { |
|
||||
"main": { |
|
||||
"widgets": { |
|
||||
"03e06986-1c50-e9e4-267c-2bae930ad9a2": { |
|
||||
"sizeX": 5, |
|
||||
"sizeY": 5, |
|
||||
"row": 0, |
|
||||
"col": 0 |
|
||||
}, |
|
||||
"88808eb1-d381-9970-c852-e3499df68bd8": { |
|
||||
"sizeX": 3, |
|
||||
"sizeY": 5, |
|
||||
"row": 0, |
|
||||
"col": 5 |
|
||||
} |
|
||||
}, |
|
||||
"gridSettings": { |
|
||||
"backgroundColor": "#eeeeee", |
|
||||
"color": "rgba(0,0,0,0.870588)", |
|
||||
"columns": 24, |
|
||||
"margins": [ |
|
||||
10, |
|
||||
10 |
|
||||
], |
|
||||
"backgroundSizeMode": "100%" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"entityAliases": { |
|
||||
"63a93238-c13f-4403-4bcc-9ccc86bd6a62": { |
|
||||
"id": "63a93238-c13f-4403-4bcc-9ccc86bd6a62", |
|
||||
"alias": "DHT11", |
|
||||
"filter": { |
|
||||
"type": "entityName", |
|
||||
"resolveMultiple": false, |
|
||||
"entityType": "DEVICE", |
|
||||
"entityNameFilter": "DHT11 Demo Device" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
"timewindow": { |
|
||||
"displayValue": "", |
|
||||
"selectedTab": 0, |
|
||||
"realtime": { |
|
||||
"interval": 1000, |
|
||||
"timewindowMs": 60000 |
|
||||
}, |
|
||||
"history": { |
|
||||
"historyType": 0, |
|
||||
"interval": 1000, |
|
||||
"timewindowMs": 60000, |
|
||||
"fixedTimewindow": { |
|
||||
"startTimeMs": 1498653790019, |
|
||||
"endTimeMs": 1498740190019 |
|
||||
} |
|
||||
}, |
|
||||
"aggregation": { |
|
||||
"type": "AVG", |
|
||||
"limit": 200 |
|
||||
} |
|
||||
}, |
|
||||
"settings": { |
|
||||
"stateControllerId": "default", |
|
||||
"showTitle": true, |
|
||||
"showDashboardsSelect": true, |
|
||||
"showEntitiesSelect": true, |
|
||||
"showDashboardTimewindow": true, |
|
||||
"showDashboardExport": true, |
|
||||
"toolbarAlwaysOpen": false |
|
||||
} |
|
||||
}, |
|
||||
"name": "Temperature & Humidity Demo Dashboard" |
|
||||
} |
|
||||
@ -1,190 +0,0 @@ |
|||||
{ |
|
||||
"ruleChain": { |
|
||||
"additionalInfo": null, |
|
||||
"name": "Root Rule Chain", |
|
||||
"type": "CORE", |
|
||||
"firstRuleNodeId": null, |
|
||||
"root": true, |
|
||||
"debugMode": false, |
|
||||
"configuration": null |
|
||||
}, |
|
||||
"metadata": { |
|
||||
"firstNodeIndex": 3, |
|
||||
"nodes": [ |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 1069, |
|
||||
"layoutY": 267 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", |
|
||||
"name": "Is Thermostat?", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"jsScript": "return msg.id.entityType === \"DEVICE\" && msg.type === \"thermostat\";" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 824, |
|
||||
"layoutY": 156 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", |
|
||||
"name": "Save Timeseries", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"defaultTTL": 0 |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 825, |
|
||||
"layoutY": 52 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", |
|
||||
"name": "Save Client Attributes", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"scope": "CLIENT_SCOPE", |
|
||||
"notifyDevice": "false" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 347, |
|
||||
"layoutY": 149 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", |
|
||||
"name": "Message Type Switch", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"version": 0 |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 839, |
|
||||
"layoutY": 345 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbLogNode", |
|
||||
"name": "Log RPC from Device", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 832, |
|
||||
"layoutY": 407 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbLogNode", |
|
||||
"name": "Log Other", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 825, |
|
||||
"layoutY": 468 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", |
|
||||
"name": "RPC Call Request", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"timeoutInSeconds": 60 |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 1069, |
|
||||
"layoutY": 90 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", |
|
||||
"name": "Is Thermostat?", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"jsScript": "return metadata[\"deviceType\"] === \"thermostat\";" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 1090, |
|
||||
"layoutY": 360 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbCreateRelationNode", |
|
||||
"name": "Relate to Asset", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"direction": "FROM", |
|
||||
"relationType": "ToAlarmPropagationAsset", |
|
||||
"entityType": "ASSET", |
|
||||
"entityNamePattern": "Thermostat Alarms", |
|
||||
"entityTypePattern": "AlarmPropagationAsset", |
|
||||
"entityCacheExpiration": 300, |
|
||||
"createEntityIfNotExists": true, |
|
||||
"changeOriginatorToRelatedEntity": false, |
|
||||
"removeCurrentRelations": false |
|
||||
} |
|
||||
} |
|
||||
], |
|
||||
"connections": [ |
|
||||
{ |
|
||||
"fromIndex": 0, |
|
||||
"toIndex": 8, |
|
||||
"type": "True" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 1, |
|
||||
"toIndex": 7, |
|
||||
"type": "Success" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 5, |
|
||||
"type": "Other" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 2, |
|
||||
"type": "Post attributes" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 1, |
|
||||
"type": "Post telemetry" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 4, |
|
||||
"type": "RPC Request from Device" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 6, |
|
||||
"type": "RPC Request to Device" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 3, |
|
||||
"toIndex": 0, |
|
||||
"type": "Entity Created" |
|
||||
} |
|
||||
], |
|
||||
"ruleChainConnections": [ |
|
||||
{ |
|
||||
"fromIndex": 7, |
|
||||
"targetRuleChainId": { |
|
||||
"entityType": "RULE_CHAIN", |
|
||||
"id": "25e26570-89ed-11ea-a650-cd6e14e633bd" |
|
||||
}, |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 1109, |
|
||||
"layoutY": 182, |
|
||||
"ruleChainNodeId": "rule-chain-node-10" |
|
||||
}, |
|
||||
"type": "True" |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
} |
|
||||
@ -1,142 +0,0 @@ |
|||||
{ |
|
||||
"ruleChain": { |
|
||||
"additionalInfo": null, |
|
||||
"name": "Thermostat Alarms", |
|
||||
"type": "CORE", |
|
||||
"firstRuleNodeId": null, |
|
||||
"root": false, |
|
||||
"debugMode": false, |
|
||||
"configuration": null |
|
||||
}, |
|
||||
"metadata": { |
|
||||
"firstNodeIndex": 5, |
|
||||
"nodes": [ |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 929, |
|
||||
"layoutY": 67 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode", |
|
||||
"name": "Create Temp Alarm", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"alarmType": "High Temperature", |
|
||||
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.temperature;\nreturn details;", |
|
||||
"severity": "MAJOR", |
|
||||
"propagate": true, |
|
||||
"useMessageAlarmData": false, |
|
||||
"relationTypes": [ |
|
||||
"ToAlarmPropagationAsset" |
|
||||
] |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 930, |
|
||||
"layoutY": 201 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbClearAlarmNode", |
|
||||
"name": "Clear Temp Alarm", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"alarmType": "High Temperature", |
|
||||
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 930, |
|
||||
"layoutY": 131 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode", |
|
||||
"name": "Create Humidity Alarm", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"alarmType": "Low Humidity", |
|
||||
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.humidity;\nreturn details;", |
|
||||
"severity": "MINOR", |
|
||||
"propagate": true, |
|
||||
"useMessageAlarmData": false, |
|
||||
"relationTypes": [ |
|
||||
"ToAlarmPropagationAsset" |
|
||||
] |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 929, |
|
||||
"layoutY": 275 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.action.TbClearAlarmNode", |
|
||||
"name": "Clear Humidity Alarm", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"alarmType": "Low Humidity", |
|
||||
"alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 586, |
|
||||
"layoutY": 148 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.filter.TbJsSwitchNode", |
|
||||
"name": "Check Alarms", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"jsScript": "var relations = [];\nif(metadata[\"ss_alarmTemperature\"] === \"true\"){\n if(msg.temperature > metadata[\"ss_thresholdTemperature\"]){\n relations.push(\"NewTempAlarm\");\n } else {\n relations.push(\"ClearTempAlarm\");\n }\n}\nif(metadata[\"ss_alarmHumidity\"] === \"true\"){\n if(msg.humidity < metadata[\"ss_thresholdHumidity\"]){\n relations.push(\"NewHumidityAlarm\");\n } else {\n relations.push(\"ClearHumidityAlarm\");\n }\n}\n\nreturn relations;" |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"additionalInfo": { |
|
||||
"layoutX": 321, |
|
||||
"layoutY": 149 |
|
||||
}, |
|
||||
"type": "org.thingsboard.rule.engine.metadata.TbGetAttributesNode", |
|
||||
"name": "Fetch Configuration", |
|
||||
"debugMode": false, |
|
||||
"configuration": { |
|
||||
"clientAttributeNames": [], |
|
||||
"sharedAttributeNames": [], |
|
||||
"serverAttributeNames": [ |
|
||||
"alarmTemperature", |
|
||||
"thresholdTemperature", |
|
||||
"alarmHumidity", |
|
||||
"thresholdHumidity" |
|
||||
], |
|
||||
"latestTsKeyNames": [], |
|
||||
"tellFailureIfAbsent": false, |
|
||||
"getLatestValueWithTs": false |
|
||||
} |
|
||||
} |
|
||||
], |
|
||||
"connections": [ |
|
||||
{ |
|
||||
"fromIndex": 4, |
|
||||
"toIndex": 0, |
|
||||
"type": "NewTempAlarm" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 4, |
|
||||
"toIndex": 1, |
|
||||
"type": "ClearTempAlarm" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 4, |
|
||||
"toIndex": 2, |
|
||||
"type": "NewHumidityAlarm" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 4, |
|
||||
"toIndex": 3, |
|
||||
"type": "ClearHumidityAlarm" |
|
||||
}, |
|
||||
{ |
|
||||
"fromIndex": 5, |
|
||||
"toIndex": 4, |
|
||||
"type": "Success" |
|
||||
} |
|
||||
], |
|
||||
"ruleChainConnections": null |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,23 @@ |
|||||
|
{ |
||||
|
"providerId": "Facebook", |
||||
|
"accessTokenUri": "https://graph.facebook.com/v2.8/oauth/access_token", |
||||
|
"authorizationUri": "https://www.facebook.com/v2.8/dialog/oauth", |
||||
|
"scope": ["email","public_profile"], |
||||
|
"jwkSetUri": null, |
||||
|
"userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email", |
||||
|
"clientAuthenticationMethod": "BASIC", |
||||
|
"userNameAttributeName": "email", |
||||
|
"mapperConfig": { |
||||
|
"type": "BASIC", |
||||
|
"basic": { |
||||
|
"emailAttributeKey": "email", |
||||
|
"firstNameAttributeKey": "first_name", |
||||
|
"lastNameAttributeKey": "last_name", |
||||
|
"tenantNameStrategy": "DOMAIN" |
||||
|
} |
||||
|
}, |
||||
|
"comment": null, |
||||
|
"loginButtonIcon": "facebook-logo", |
||||
|
"loginButtonLabel": "Facebook", |
||||
|
"helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog" |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
{ |
||||
|
"providerId": "Github", |
||||
|
"accessTokenUri": "https://github.com/login/oauth/access_token", |
||||
|
"authorizationUri": "https://github.com/login/oauth/authorize", |
||||
|
"scope": ["read:user","user:email"], |
||||
|
"jwkSetUri": null, |
||||
|
"userInfoUri": "https://api.github.com/user", |
||||
|
"clientAuthenticationMethod": "BASIC", |
||||
|
"userNameAttributeName": "login", |
||||
|
"mapperConfig": { |
||||
|
"type": "GITHUB", |
||||
|
"basic": { |
||||
|
"firstNameAttributeKey": "name", |
||||
|
"tenantNameStrategy": "DOMAIN" |
||||
|
} |
||||
|
}, |
||||
|
"comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to <a href=\"https://docs.github.com/en/rest/reference/users#list-email-addresses-for-the-authenticated-user\">Github Documentation</a>", |
||||
|
"loginButtonIcon": "github-logo", |
||||
|
"loginButtonLabel": "Github", |
||||
|
"helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app" |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
{ |
||||
|
"providerId": "Google", |
||||
|
"additionalInfo": null, |
||||
|
"accessTokenUri": "https://oauth2.googleapis.com/token", |
||||
|
"authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth", |
||||
|
"scope": ["email","openid","profile"], |
||||
|
"jwkSetUri": "https://www.googleapis.com/oauth2/v3/certs", |
||||
|
"userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo", |
||||
|
"clientAuthenticationMethod": "BASIC", |
||||
|
"userNameAttributeName": "email", |
||||
|
"mapperConfig": { |
||||
|
"type": "BASIC", |
||||
|
"basic": { |
||||
|
"emailAttributeKey": "email", |
||||
|
"firstNameAttributeKey": "given_name", |
||||
|
"lastNameAttributeKey": "family_name", |
||||
|
"tenantNameStrategy": "DOMAIN" |
||||
|
} |
||||
|
}, |
||||
|
"comment": null, |
||||
|
"loginButtonIcon": "google-logo", |
||||
|
"loginButtonLabel": "Google", |
||||
|
"helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication" |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,267 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.config; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; |
||||
|
import org.springframework.security.crypto.keygen.StringKeyGenerator; |
||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistration; |
||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; |
||||
|
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; |
||||
|
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; |
||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; |
||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
|
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; |
||||
|
import org.springframework.security.oauth2.core.oidc.OidcScopes; |
||||
|
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; |
||||
|
import org.springframework.security.web.util.UrlUtils; |
||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.CollectionUtils; |
||||
|
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.utils.MiscUtils; |
||||
|
|
||||
|
import javax.servlet.http.HttpServletRequest; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.security.MessageDigest; |
||||
|
import java.security.NoSuchAlgorithmException; |
||||
|
import java.util.Base64; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@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 REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"; |
||||
|
private static final char PATH_DELIMITER = '/'; |
||||
|
|
||||
|
private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher( |
||||
|
DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}"); |
||||
|
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder()); |
||||
|
private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); |
||||
|
|
||||
|
@Autowired |
||||
|
private ClientRegistrationRepository clientRegistrationRepository; |
||||
|
|
||||
|
@Autowired(required = false) |
||||
|
private OAuth2Configuration oauth2Configuration; |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { |
||||
|
String registrationId = this.resolveRegistrationId(request); |
||||
|
String redirectUriAction = getAction(request, "login"); |
||||
|
return resolve(request, registrationId, redirectUriAction); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) { |
||||
|
if (registrationId == null) { |
||||
|
return null; |
||||
|
} |
||||
|
String redirectUriAction = getAction(request, "authorize"); |
||||
|
return resolve(request, registrationId, redirectUriAction); |
||||
|
} |
||||
|
|
||||
|
private String getAction(HttpServletRequest request, String defaultAction) { |
||||
|
String action = request.getParameter("action"); |
||||
|
if (action == null) { |
||||
|
return defaultAction; |
||||
|
} |
||||
|
return action; |
||||
|
} |
||||
|
|
||||
|
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { |
||||
|
if (registrationId == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); |
||||
|
if (clientRegistration == null) { |
||||
|
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId); |
||||
|
} |
||||
|
|
||||
|
Map<String, Object> attributes = new HashMap<>(); |
||||
|
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); |
||||
|
|
||||
|
OAuth2AuthorizationRequest.Builder builder; |
||||
|
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) { |
||||
|
builder = OAuth2AuthorizationRequest.authorizationCode(); |
||||
|
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
|
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && |
||||
|
clientRegistration.getScopes().contains(OidcScopes.OPENID)) { |
||||
|
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
|
// scope
|
||||
|
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
|
||||
|
addNonceParameters(attributes, additionalParameters); |
||||
|
} |
||||
|
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { |
||||
|
addPkceParameters(attributes, additionalParameters); |
||||
|
} |
||||
|
builder.additionalParameters(additionalParameters); |
||||
|
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) { |
||||
|
builder = OAuth2AuthorizationRequest.implicit(); |
||||
|
} else { |
||||
|
throw new IllegalArgumentException("Invalid Authorization Grant Type (" + |
||||
|
clientRegistration.getAuthorizationGrantType().getValue() + |
||||
|
") for Client Registration with Id: " + clientRegistration.getRegistrationId()); |
||||
|
} |
||||
|
|
||||
|
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); |
||||
|
|
||||
|
return builder |
||||
|
.clientId(clientRegistration.getClientId()) |
||||
|
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) |
||||
|
.redirectUri(redirectUriStr) |
||||
|
.scopes(clientRegistration.getScopes()) |
||||
|
.state(this.stateGenerator.generateKey()) |
||||
|
.attributes(attributes) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
private String resolveRegistrationId(HttpServletRequest request) { |
||||
|
if (this.authorizationRequestMatcher.matches(request)) { |
||||
|
return this.authorizationRequestMatcher |
||||
|
.matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/> |
||||
|
* - baseUrl (e.g. https://localhost/app) <br/>
|
||||
|
* - baseScheme (e.g. https) <br/> |
||||
|
* - baseHost (e.g. localhost) <br/> |
||||
|
* - basePort (e.g. :8080) <br/> |
||||
|
* - basePath (e.g. /app) <br/> |
||||
|
* - registrationId (e.g. google) <br/> |
||||
|
* - action (e.g. login) <br/> |
||||
|
* <p/> |
||||
|
* Null variables are provided as empty strings. |
||||
|
* <p/> |
||||
|
* Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL |
||||
|
* |
||||
|
* @return expanded URI |
||||
|
*/ |
||||
|
private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) { |
||||
|
Map<String, String> uriVariables = new HashMap<>(); |
||||
|
uriVariables.put("registrationId", clientRegistration.getRegistrationId()); |
||||
|
|
||||
|
UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) |
||||
|
.replacePath(request.getContextPath()) |
||||
|
.replaceQuery(null) |
||||
|
.fragment(null) |
||||
|
.build(); |
||||
|
String scheme = uriComponents.getScheme(); |
||||
|
uriVariables.put("baseScheme", scheme == null ? "" : scheme); |
||||
|
String host = uriComponents.getHost(); |
||||
|
uriVariables.put("baseHost", host == null ? "" : host); |
||||
|
// following logic is based on HierarchicalUriComponents#toUriString()
|
||||
|
int port = uriComponents.getPort(); |
||||
|
uriVariables.put("basePort", port == -1 ? "" : ":" + port); |
||||
|
String path = uriComponents.getPath(); |
||||
|
if (StringUtils.hasLength(path)) { |
||||
|
if (path.charAt(0) != PATH_DELIMITER) { |
||||
|
path = PATH_DELIMITER + path; |
||||
|
} |
||||
|
} |
||||
|
uriVariables.put("basePath", path == null ? "" : path); |
||||
|
uriVariables.put("baseUrl", uriComponents.toUriString()); |
||||
|
|
||||
|
uriVariables.put("action", action == null ? "" : action); |
||||
|
|
||||
|
String redirectUri = getRedirectUri(request); |
||||
|
log.trace("Redirect URI - {}.", redirectUri); |
||||
|
|
||||
|
return UriComponentsBuilder.fromUriString(redirectUri) |
||||
|
.buildAndExpand(uriVariables) |
||||
|
.toUriString(); |
||||
|
} |
||||
|
|
||||
|
private String getRedirectUri(HttpServletRequest request) { |
||||
|
String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI; |
||||
|
|
||||
|
String scheme = MiscUtils.getScheme(request); |
||||
|
String domainName = MiscUtils.getDomainName(request); |
||||
|
int port = MiscUtils.getPort(request); |
||||
|
String baseUrl = scheme + "://" + domainName; |
||||
|
if (needsPort(scheme, port)){ |
||||
|
baseUrl += ":" + port; |
||||
|
} |
||||
|
return baseUrl + loginProcessingUri; |
||||
|
} |
||||
|
|
||||
|
private boolean needsPort(String scheme, int port) { |
||||
|
boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80; |
||||
|
boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443; |
||||
|
return !isHttpDefault && !isHttpsDefault; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests. |
||||
|
* |
||||
|
* @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request |
||||
|
* @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request |
||||
|
* |
||||
|
* @since 5.2 |
||||
|
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">3.1.2.1. Authentication Request</a> |
||||
|
*/ |
||||
|
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) { |
||||
|
try { |
||||
|
String nonce = this.secureKeyGenerator.generateKey(); |
||||
|
String nonceHash = createHash(nonce); |
||||
|
attributes.put(OidcParameterNames.NONCE, nonce); |
||||
|
additionalParameters.put(OidcParameterNames.NONCE, nonceHash); |
||||
|
} catch (NoSuchAlgorithmException e) { } |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests |
||||
|
* |
||||
|
* @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request |
||||
|
* @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually, |
||||
|
* {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request. |
||||
|
* |
||||
|
* @since 5.2 |
||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a> |
||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a> |
||||
|
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a> |
||||
|
*/ |
||||
|
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) { |
||||
|
String codeVerifier = this.secureKeyGenerator.generateKey(); |
||||
|
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier); |
||||
|
try { |
||||
|
String codeChallenge = createHash(codeVerifier); |
||||
|
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge); |
||||
|
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); |
||||
|
} catch (NoSuchAlgorithmException e) { |
||||
|
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static String createHash(String value) throws NoSuchAlgorithmException { |
||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256"); |
||||
|
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII)); |
||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.controller; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.audit.ActionType; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; |
||||
|
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.security.permission.Operation; |
||||
|
import org.thingsboard.server.service.security.permission.Resource; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@RestController |
||||
|
@TbCoreComponent |
||||
|
@RequestMapping("/api/oauth2/config/template") |
||||
|
@Slf4j |
||||
|
public class OAuth2ConfigTemplateController extends BaseController { |
||||
|
private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId"; |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@RequestMapping(method = RequestMethod.POST) |
||||
|
@ResponseStatus(value = HttpStatus.OK) |
||||
|
public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException { |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE); |
||||
|
return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE) |
||||
|
@ResponseStatus(value = HttpStatus.OK) |
||||
|
public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { |
||||
|
checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId); |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); |
||||
|
OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId)); |
||||
|
oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
||||
|
@RequestMapping(method = RequestMethod.GET, produces = "application/json") |
||||
|
@ResponseBody |
||||
|
public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() throws ThingsboardException { |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ); |
||||
|
return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates(); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.controller; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.http.HttpStatus; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
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.SchemeType; |
||||
|
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.security.permission.Operation; |
||||
|
import org.thingsboard.server.service.security.permission.Resource; |
||||
|
import org.thingsboard.server.utils.MiscUtils; |
||||
|
|
||||
|
import javax.servlet.http.HttpServletRequest; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@RestController |
||||
|
@TbCoreComponent |
||||
|
@RequestMapping("/api") |
||||
|
@Slf4j |
||||
|
public class OAuth2Controller extends BaseController { |
||||
|
|
||||
|
@Autowired |
||||
|
private OAuth2Configuration oAuth2Configuration; |
||||
|
|
||||
|
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) |
||||
|
@ResponseBody |
||||
|
public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException { |
||||
|
try { |
||||
|
return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request)); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") |
||||
|
@ResponseBody |
||||
|
public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException { |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); |
||||
|
return oAuth2Service.findOAuth2Params(); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) |
||||
|
@ResponseStatus(value = HttpStatus.OK) |
||||
|
public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException { |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE); |
||||
|
oAuth2Service.saveOAuth2Params(oauth2Params); |
||||
|
return oAuth2Service.findOAuth2Params(); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") |
||||
|
@RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) |
||||
|
@ResponseBody |
||||
|
public String getLoginProcessingUrl() throws ThingsboardException { |
||||
|
try { |
||||
|
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ); |
||||
|
return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\""; |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,450 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.apiusage; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.checkerframework.checker.nullness.qual.Nullable; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Lazy; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.rule.engine.api.MailService; |
||||
|
import org.thingsboard.server.common.data.ApiFeature; |
||||
|
import org.thingsboard.server.common.data.ApiUsageRecordKey; |
||||
|
import org.thingsboard.server.common.data.ApiUsageState; |
||||
|
import org.thingsboard.server.common.data.ApiUsageStateMailMessage; |
||||
|
import org.thingsboard.server.common.data.ApiUsageStateValue; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.TenantProfile; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.ApiUsageStateId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.TenantProfileId; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.data.page.PageDataIterable; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
||||
|
import org.thingsboard.server.common.msg.queue.ServiceType; |
||||
|
import org.thingsboard.server.common.msg.queue.TbCallback; |
||||
|
import org.thingsboard.server.common.msg.tools.SchedulerUtils; |
||||
|
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
||||
|
import org.thingsboard.server.dao.tenant.TenantService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.scheduler.SchedulerComponent; |
||||
|
import org.thingsboard.server.service.queue.TbClusterService; |
||||
|
import org.thingsboard.server.service.telemetry.InternalTelemetryService; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.concurrent.locks.Lock; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class DefaultTbApiUsageStateService implements TbApiUsageStateService { |
||||
|
|
||||
|
public static final String HOURLY = "Hourly"; |
||||
|
public static final FutureCallback<Integer> VOID_CALLBACK = new FutureCallback<Integer>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable Integer result) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
} |
||||
|
}; |
||||
|
private final TbClusterService clusterService; |
||||
|
private final PartitionService partitionService; |
||||
|
private final TenantService tenantService; |
||||
|
private final TimeseriesService tsService; |
||||
|
private final ApiUsageStateService apiUsageStateService; |
||||
|
private final SchedulerComponent scheduler; |
||||
|
private final TbTenantProfileCache tenantProfileCache; |
||||
|
private final MailService mailService; |
||||
|
|
||||
|
@Lazy |
||||
|
@Autowired |
||||
|
private InternalTelemetryService tsWsService; |
||||
|
|
||||
|
// Tenants that should be processed on this server
|
||||
|
private final Map<TenantId, TenantApiUsageState> myTenantStates = new ConcurrentHashMap<>(); |
||||
|
// Tenants that should be processed on other servers
|
||||
|
private final Map<TenantId, ApiUsageState> otherTenantStates = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Value("${usage.stats.report.enabled:true}") |
||||
|
private boolean enabled; |
||||
|
|
||||
|
@Value("${usage.stats.check.cycle:60000}") |
||||
|
private long nextCycleCheckInterval; |
||||
|
|
||||
|
private final Lock updateLock = new ReentrantLock(); |
||||
|
|
||||
|
private final ExecutorService mailExecutor; |
||||
|
|
||||
|
public DefaultTbApiUsageStateService(TbClusterService clusterService, |
||||
|
PartitionService partitionService, |
||||
|
TenantService tenantService, |
||||
|
TimeseriesService tsService, |
||||
|
ApiUsageStateService apiUsageStateService, |
||||
|
SchedulerComponent scheduler, |
||||
|
TbTenantProfileCache tenantProfileCache, MailService mailService) { |
||||
|
this.clusterService = clusterService; |
||||
|
this.partitionService = partitionService; |
||||
|
this.tenantService = tenantService; |
||||
|
this.tsService = tsService; |
||||
|
this.apiUsageStateService = apiUsageStateService; |
||||
|
this.scheduler = scheduler; |
||||
|
this.tenantProfileCache = tenantProfileCache; |
||||
|
this.mailService = mailService; |
||||
|
this.mailExecutor = Executors.newSingleThreadExecutor(); |
||||
|
} |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
if (enabled) { |
||||
|
log.info("Starting api usage service."); |
||||
|
scheduler.scheduleAtFixedRate(this::checkStartOfNextCycle, nextCycleCheckInterval, nextCycleCheckInterval, TimeUnit.MILLISECONDS); |
||||
|
log.info("Started api usage service."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) { |
||||
|
ToUsageStatsServiceMsg statsMsg = msg.getValue(); |
||||
|
TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); |
||||
|
|
||||
|
if (tenantProfileCache.get(tenantId) == null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
TenantApiUsageState tenantState; |
||||
|
List<TsKvEntry> updatedEntries; |
||||
|
Map<ApiFeature, ApiUsageStateValue> result; |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
tenantState = getOrFetchState(tenantId); |
||||
|
long ts = tenantState.getCurrentCycleTs(); |
||||
|
long hourTs = tenantState.getCurrentHourTs(); |
||||
|
long newHourTs = SchedulerUtils.getStartOfCurrentHour(); |
||||
|
if (newHourTs != hourTs) { |
||||
|
tenantState.setHour(newHourTs); |
||||
|
} |
||||
|
updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length); |
||||
|
Set<ApiFeature> apiFeatures = new HashSet<>(); |
||||
|
for (UsageStatsKVProto kvProto : statsMsg.getValuesList()) { |
||||
|
ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey()); |
||||
|
long newValue = tenantState.add(recordKey, kvProto.getValue()); |
||||
|
updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); |
||||
|
long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue()); |
||||
|
updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); |
||||
|
apiFeatures.add(recordKey.getApiFeature()); |
||||
|
} |
||||
|
result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
tsWsService.saveAndNotifyInternal(tenantId, tenantState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK); |
||||
|
if (!result.isEmpty()) { |
||||
|
persistAndNotify(tenantState, result); |
||||
|
} |
||||
|
callback.onSuccess(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { |
||||
|
if (partitionChangeEvent.getServiceType().equals(ServiceType.TB_CORE)) { |
||||
|
myTenantStates.entrySet().removeIf(entry -> !partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); |
||||
|
otherTenantStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); |
||||
|
initStatesFromDataBase(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiUsageState getApiUsageState(TenantId tenantId) { |
||||
|
TenantApiUsageState tenantState = myTenantStates.get(tenantId); |
||||
|
if (tenantState != null) { |
||||
|
return tenantState.getApiUsageState(); |
||||
|
} else { |
||||
|
ApiUsageState state = otherTenantStates.get(tenantId); |
||||
|
if (state != null) { |
||||
|
return state; |
||||
|
} else { |
||||
|
if (partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) { |
||||
|
return getOrFetchState(tenantId).getApiUsageState(); |
||||
|
} else { |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
state = otherTenantStates.get(tenantId); |
||||
|
if (state == null) { |
||||
|
state = apiUsageStateService.findTenantApiUsageState(tenantId); |
||||
|
if (state != null) { |
||||
|
otherTenantStates.put(tenantId, state); |
||||
|
} |
||||
|
} |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
return state; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onApiUsageStateUpdate(TenantId tenantId) { |
||||
|
otherTenantStates.remove(tenantId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onTenantProfileUpdate(TenantProfileId tenantProfileId) { |
||||
|
log.info("[{}] On Tenant Profile Update", tenantProfileId); |
||||
|
TenantProfile tenantProfile = tenantProfileCache.get(tenantProfileId); |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
myTenantStates.values().forEach(state -> { |
||||
|
if (tenantProfile.getId().equals(state.getTenantProfileId())) { |
||||
|
updateTenantState(state, tenantProfile); |
||||
|
} |
||||
|
}); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onTenantUpdate(TenantId tenantId) { |
||||
|
log.info("[{}] On Tenant Update.", tenantId); |
||||
|
TenantProfile tenantProfile = tenantProfileCache.get(tenantId); |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
TenantApiUsageState state = myTenantStates.get(tenantId); |
||||
|
if (state != null && !state.getTenantProfileId().equals(tenantProfile.getId())) { |
||||
|
updateTenantState(state, tenantProfile); |
||||
|
} |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void updateTenantState(TenantApiUsageState state, TenantProfile profile) { |
||||
|
TenantProfileData oldProfileData = state.getTenantProfileData(); |
||||
|
state.setTenantProfileId(profile.getId()); |
||||
|
state.setTenantProfileData(profile.getProfileData()); |
||||
|
Map<ApiFeature, ApiUsageStateValue> result = state.checkStateUpdatedDueToThresholds(); |
||||
|
if (!result.isEmpty()) { |
||||
|
persistAndNotify(state, result); |
||||
|
} |
||||
|
updateProfileThresholds(state.getTenantId(), state.getApiUsageState().getId(), |
||||
|
oldProfileData.getConfiguration(), profile.getProfileData().getConfiguration()); |
||||
|
} |
||||
|
|
||||
|
private void updateProfileThresholds(TenantId tenantId, ApiUsageStateId id, |
||||
|
TenantProfileConfiguration oldData, TenantProfileConfiguration newData) { |
||||
|
long ts = System.currentTimeMillis(); |
||||
|
List<TsKvEntry> profileThresholds = new ArrayList<>(); |
||||
|
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { |
||||
|
long newProfileThreshold = newData.getProfileThreshold(key); |
||||
|
if (oldData == null || oldData.getProfileThreshold(key) != newProfileThreshold) { |
||||
|
log.info("[{}] Updating profile threshold [{}]:[{}]", tenantId, key, newProfileThreshold); |
||||
|
profileThresholds.add(new BasicTsKvEntry(ts, new LongDataEntry(key.getApiLimitKey(), newProfileThreshold))); |
||||
|
} |
||||
|
} |
||||
|
if (!profileThresholds.isEmpty()) { |
||||
|
tsWsService.saveAndNotifyInternal(tenantId, id, profileThresholds, VOID_CALLBACK); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void persistAndNotify(TenantApiUsageState state, Map<ApiFeature, ApiUsageStateValue> result) { |
||||
|
log.info("[{}] Detected update of the API state: {}", state.getTenantId(), result); |
||||
|
apiUsageStateService.update(state.getApiUsageState()); |
||||
|
clusterService.onApiStateChange(state.getApiUsageState(), null); |
||||
|
long ts = System.currentTimeMillis(); |
||||
|
List<TsKvEntry> stateTelemetry = new ArrayList<>(); |
||||
|
result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name()))))); |
||||
|
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK); |
||||
|
|
||||
|
String email = tenantService.findTenantById(state.getTenantId()).getEmail(); |
||||
|
|
||||
|
if (StringUtils.isNotEmpty(email)) { |
||||
|
result.forEach((apiFeature, stateValue) -> { |
||||
|
mailExecutor.submit(() -> { |
||||
|
try { |
||||
|
mailService.sendApiFeatureStateEmail(apiFeature, stateValue, email, createStateMailMessage(state, apiFeature, stateValue)); |
||||
|
} catch (ThingsboardException e) { |
||||
|
log.warn("[{}] Can't send update of the API state to tenant with provided email [{}]", state.getTenantId(), email, e); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} else { |
||||
|
log.warn("[{}] Can't send update of the API state to tenant with empty email!", state.getTenantId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ApiUsageStateMailMessage createStateMailMessage(TenantApiUsageState state, ApiFeature apiFeature, ApiUsageStateValue stateValue) { |
||||
|
StateChecker checker = getStateChecker(stateValue); |
||||
|
for (ApiUsageRecordKey apiUsageRecordKey : ApiUsageRecordKey.getKeys(apiFeature)) { |
||||
|
long threshold = state.getProfileThreshold(apiUsageRecordKey); |
||||
|
long warnThreshold = state.getProfileWarnThreshold(apiUsageRecordKey); |
||||
|
long value = state.get(apiUsageRecordKey); |
||||
|
if (checker.check(threshold, warnThreshold, value)) { |
||||
|
return new ApiUsageStateMailMessage(apiUsageRecordKey, threshold, value); |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private StateChecker getStateChecker(ApiUsageStateValue stateValue) { |
||||
|
if (ApiUsageStateValue.ENABLED.equals(stateValue)) { |
||||
|
return (t, wt, v) -> true; |
||||
|
} else if (ApiUsageStateValue.WARNING.equals(stateValue)) { |
||||
|
return (t, wt, v) -> v < t && v >= wt; |
||||
|
} else { |
||||
|
return (t, wt, v) -> v >= t; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private interface StateChecker { |
||||
|
boolean check(long threshold, long warnThreshold, long value); |
||||
|
} |
||||
|
|
||||
|
private void checkStartOfNextCycle() { |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
long now = System.currentTimeMillis(); |
||||
|
myTenantStates.values().forEach(state -> { |
||||
|
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { |
||||
|
TenantId tenantId = state.getTenantId(); |
||||
|
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); |
||||
|
saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values())); |
||||
|
updateTenantState(state, tenantProfileCache.get(tenantId)); |
||||
|
} |
||||
|
}); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void saveNewCounts(TenantApiUsageState state, List<ApiUsageRecordKey> keys) { |
||||
|
List<TsKvEntry> counts = keys.stream() |
||||
|
.map(key -> new BasicTsKvEntry(state.getCurrentCycleTs(), new LongDataEntry(key.getApiCountKey(), 0L))) |
||||
|
.collect(Collectors.toList()); |
||||
|
|
||||
|
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK); |
||||
|
} |
||||
|
|
||||
|
private TenantApiUsageState getOrFetchState(TenantId tenantId) { |
||||
|
TenantApiUsageState tenantState = myTenantStates.get(tenantId); |
||||
|
if (tenantState == null) { |
||||
|
ApiUsageState dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId); |
||||
|
if (dbStateEntity == null) { |
||||
|
try { |
||||
|
dbStateEntity = apiUsageStateService.createDefaultApiUsageState(tenantId); |
||||
|
} catch (Exception e) { |
||||
|
dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId); |
||||
|
} |
||||
|
} |
||||
|
TenantProfile tenantProfile = tenantProfileCache.get(tenantId); |
||||
|
tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity); |
||||
|
List<ApiUsageRecordKey> newCounts = new ArrayList<>(); |
||||
|
try { |
||||
|
List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get(); |
||||
|
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { |
||||
|
boolean cycleEntryFound = false; |
||||
|
boolean hourlyEntryFound = false; |
||||
|
for (TsKvEntry tsKvEntry : dbValues) { |
||||
|
if (tsKvEntry.getKey().equals(key.getApiCountKey())) { |
||||
|
cycleEntryFound = true; |
||||
|
|
||||
|
boolean oldCount = tsKvEntry.getTs() == tenantState.getCurrentCycleTs(); |
||||
|
tenantState.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L); |
||||
|
|
||||
|
if (!oldCount) { |
||||
|
newCounts.add(key); |
||||
|
} |
||||
|
} else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { |
||||
|
hourlyEntryFound = true; |
||||
|
tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); |
||||
|
} |
||||
|
if (cycleEntryFound && hourlyEntryFound) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity); |
||||
|
myTenantStates.put(tenantId, tenantState); |
||||
|
saveNewCounts(tenantState, newCounts); |
||||
|
} catch (InterruptedException | ExecutionException e) { |
||||
|
log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e); |
||||
|
} |
||||
|
} |
||||
|
return tenantState; |
||||
|
} |
||||
|
|
||||
|
private void initStatesFromDataBase() { |
||||
|
try { |
||||
|
log.info("Initializing tenant states."); |
||||
|
PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, 1024); |
||||
|
for (Tenant tenant : tenantIterator) { |
||||
|
if (!myTenantStates.containsKey(tenant.getId()) && partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), tenant.getId()).isMyPartition()) { |
||||
|
log.debug("[{}] Initializing tenant state.", tenant.getId()); |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
updateTenantState(getOrFetchState(tenant.getId()), tenantProfileCache.get(tenant.getTenantProfileId())); |
||||
|
log.debug("[{}] Initialized tenant state.", tenant.getId()); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("[{}] Failed to initialize tenant API state", tenant.getId(), e); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
log.info("Initialized tenant states."); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("Unknown failure", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
private void destroy() { |
||||
|
if (mailExecutor != null) { |
||||
|
mailExecutor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.apiusage; |
||||
|
|
||||
|
import org.springframework.context.ApplicationListener; |
||||
|
import org.thingsboard.server.common.data.ApiUsageState; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.TenantProfileId; |
||||
|
import org.thingsboard.server.common.msg.queue.TbCallback; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
|
||||
|
public interface TbApiUsageStateService extends ApplicationListener<PartitionChangeEvent> { |
||||
|
|
||||
|
void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback); |
||||
|
|
||||
|
ApiUsageState getApiUsageState(TenantId tenantId); |
||||
|
|
||||
|
void onTenantProfileUpdate(TenantProfileId tenantProfileId); |
||||
|
|
||||
|
void onTenantUpdate(TenantId tenantId); |
||||
|
|
||||
|
void onApiUsageStateUpdate(TenantId tenantId); |
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.apiusage; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import lombok.Setter; |
||||
|
import org.springframework.data.util.Pair; |
||||
|
import org.thingsboard.server.common.data.ApiFeature; |
||||
|
import org.thingsboard.server.common.data.ApiUsageRecordKey; |
||||
|
import org.thingsboard.server.common.data.ApiUsageState; |
||||
|
import org.thingsboard.server.common.data.ApiUsageStateValue; |
||||
|
import org.thingsboard.server.common.data.TenantProfile; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.TenantProfileId; |
||||
|
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; |
||||
|
import org.thingsboard.server.common.msg.tools.SchedulerUtils; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
|
||||
|
public class TenantApiUsageState { |
||||
|
|
||||
|
private final Map<ApiUsageRecordKey, Long> currentCycleValues = new ConcurrentHashMap<>(); |
||||
|
private final Map<ApiUsageRecordKey, Long> currentHourValues = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Getter |
||||
|
@Setter |
||||
|
private TenantProfileId tenantProfileId; |
||||
|
@Getter |
||||
|
@Setter |
||||
|
private TenantProfileData tenantProfileData; |
||||
|
@Getter |
||||
|
private final ApiUsageState apiUsageState; |
||||
|
@Getter |
||||
|
private volatile long currentCycleTs; |
||||
|
@Getter |
||||
|
private volatile long nextCycleTs; |
||||
|
@Getter |
||||
|
private volatile long currentHourTs; |
||||
|
|
||||
|
public TenantApiUsageState(TenantProfile tenantProfile, ApiUsageState apiUsageState) { |
||||
|
this.tenantProfileId = tenantProfile.getId(); |
||||
|
this.tenantProfileData = tenantProfile.getProfileData(); |
||||
|
this.apiUsageState = apiUsageState; |
||||
|
this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth(); |
||||
|
this.nextCycleTs = SchedulerUtils.getStartOfNextMonth(); |
||||
|
this.currentHourTs = SchedulerUtils.getStartOfCurrentHour(); |
||||
|
} |
||||
|
|
||||
|
public void put(ApiUsageRecordKey key, Long value) { |
||||
|
currentCycleValues.put(key, value); |
||||
|
} |
||||
|
|
||||
|
public void putHourly(ApiUsageRecordKey key, Long value) { |
||||
|
currentHourValues.put(key, value); |
||||
|
} |
||||
|
|
||||
|
public long add(ApiUsageRecordKey key, long value) { |
||||
|
long result = currentCycleValues.getOrDefault(key, 0L) + value; |
||||
|
currentCycleValues.put(key, result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public long get(ApiUsageRecordKey key) { |
||||
|
return currentCycleValues.getOrDefault(key, 0L); |
||||
|
} |
||||
|
|
||||
|
public long addToHourly(ApiUsageRecordKey key, long value) { |
||||
|
long result = currentHourValues.getOrDefault(key, 0L) + value; |
||||
|
currentHourValues.put(key, result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public void setHour(long currentHourTs) { |
||||
|
this.currentHourTs = currentHourTs; |
||||
|
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { |
||||
|
currentHourValues.put(key, 0L); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setCycles(long currentCycleTs, long nextCycleTs) { |
||||
|
this.currentCycleTs = currentCycleTs; |
||||
|
this.nextCycleTs = nextCycleTs; |
||||
|
for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { |
||||
|
currentCycleValues.put(key, 0L); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public long getProfileThreshold(ApiUsageRecordKey key) { |
||||
|
return tenantProfileData.getConfiguration().getProfileThreshold(key); |
||||
|
} |
||||
|
|
||||
|
public long getProfileWarnThreshold(ApiUsageRecordKey key) { |
||||
|
return tenantProfileData.getConfiguration().getWarnThreshold(key); |
||||
|
} |
||||
|
|
||||
|
public TenantId getTenantId() { |
||||
|
return apiUsageState.getTenantId(); |
||||
|
} |
||||
|
|
||||
|
public ApiUsageStateValue getFeatureValue(ApiFeature feature) { |
||||
|
switch (feature) { |
||||
|
case TRANSPORT: |
||||
|
return apiUsageState.getTransportState(); |
||||
|
case RE: |
||||
|
return apiUsageState.getReExecState(); |
||||
|
case DB: |
||||
|
return apiUsageState.getDbStorageState(); |
||||
|
case JS: |
||||
|
return apiUsageState.getJsExecState(); |
||||
|
case EMAIL: |
||||
|
return apiUsageState.getEmailExecState(); |
||||
|
case SMS: |
||||
|
return apiUsageState.getSmsExecState(); |
||||
|
default: |
||||
|
return ApiUsageStateValue.ENABLED; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean setFeatureValue(ApiFeature feature, ApiUsageStateValue value) { |
||||
|
ApiUsageStateValue currentValue = getFeatureValue(feature); |
||||
|
switch (feature) { |
||||
|
case TRANSPORT: |
||||
|
apiUsageState.setTransportState(value); |
||||
|
break; |
||||
|
case RE: |
||||
|
apiUsageState.setReExecState(value); |
||||
|
break; |
||||
|
case DB: |
||||
|
apiUsageState.setDbStorageState(value); |
||||
|
break; |
||||
|
case JS: |
||||
|
apiUsageState.setJsExecState(value); |
||||
|
break; |
||||
|
case EMAIL: |
||||
|
apiUsageState.setEmailExecState(value); |
||||
|
break; |
||||
|
case SMS: |
||||
|
apiUsageState.setSmsExecState(value); |
||||
|
break; |
||||
|
} |
||||
|
return !currentValue.equals(value); |
||||
|
} |
||||
|
|
||||
|
public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThresholds() { |
||||
|
return checkStateUpdatedDueToThreshold(new HashSet<>(Arrays.asList(ApiFeature.values()))); |
||||
|
} |
||||
|
|
||||
|
public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(Set<ApiFeature> features) { |
||||
|
Map<ApiFeature, ApiUsageStateValue> result = new HashMap<>(); |
||||
|
for (ApiFeature feature : features) { |
||||
|
Pair<ApiFeature, ApiUsageStateValue> tmp = checkStateUpdatedDueToThreshold(feature); |
||||
|
if (tmp != null) { |
||||
|
result.put(tmp.getFirst(), tmp.getSecond()); |
||||
|
} |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public Pair<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(ApiFeature feature) { |
||||
|
ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED; |
||||
|
for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) { |
||||
|
long value = get(recordKey); |
||||
|
long threshold = getProfileThreshold(recordKey); |
||||
|
long warnThreshold = getProfileWarnThreshold(recordKey); |
||||
|
ApiUsageStateValue tmpValue; |
||||
|
if (threshold == 0 || value < warnThreshold) { |
||||
|
tmpValue = ApiUsageStateValue.ENABLED; |
||||
|
} else if (value < threshold) { |
||||
|
tmpValue = ApiUsageStateValue.WARNING; |
||||
|
} else { |
||||
|
tmpValue = ApiUsageStateValue.DISABLED; |
||||
|
} |
||||
|
featureValue = ApiUsageStateValue.toMoreRestricted(featureValue, tmpValue); |
||||
|
} |
||||
|
return setFeatureValue(feature, featureValue) ? Pair.of(feature, featureValue) : null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,268 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.device; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang.RandomStringUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.audit.ActionType; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.security.DeviceCredentials; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.common.msg.queue.ServiceType; |
||||
|
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.audit.AuditLogService; |
||||
|
import org.thingsboard.server.dao.device.DeviceCredentialsService; |
||||
|
import org.thingsboard.server.dao.device.DeviceDao; |
||||
|
import org.thingsboard.server.dao.device.DeviceProfileDao; |
||||
|
import org.thingsboard.server.dao.device.DeviceProvisionService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionFailedException; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionRequest; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponse; |
||||
|
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; |
||||
|
import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; |
||||
|
import org.thingsboard.server.queue.TbQueueCallback; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.state.DeviceStateService; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.Optional; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
|
||||
|
|
||||
|
@Service |
||||
|
@Slf4j |
||||
|
@TbCoreComponent |
||||
|
public class DeviceProvisionServiceImpl implements DeviceProvisionService { |
||||
|
|
||||
|
protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer; |
||||
|
|
||||
|
private static final String DEVICE_PROVISION_STATE = "provisionState"; |
||||
|
private static final String PROVISIONED_STATE = "provisioned"; |
||||
|
|
||||
|
private final ReentrantLock deviceCreationLock = new ReentrantLock(); |
||||
|
|
||||
|
@Autowired |
||||
|
DeviceDao deviceDao; |
||||
|
|
||||
|
@Autowired |
||||
|
DeviceProfileDao deviceProfileDao; |
||||
|
|
||||
|
@Autowired |
||||
|
DeviceService deviceService; |
||||
|
|
||||
|
@Autowired |
||||
|
DeviceCredentialsService deviceCredentialsService; |
||||
|
|
||||
|
@Autowired |
||||
|
AttributesService attributesService; |
||||
|
|
||||
|
@Autowired |
||||
|
DeviceStateService deviceStateService; |
||||
|
|
||||
|
@Autowired |
||||
|
AuditLogService auditLogService; |
||||
|
|
||||
|
@Autowired |
||||
|
PartitionService partitionService; |
||||
|
|
||||
|
public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) { |
||||
|
ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) { |
||||
|
String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey(); |
||||
|
String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret(); |
||||
|
if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) { |
||||
|
provisionRequest.setDeviceName(provisionRequest.getDeviceName().trim()); |
||||
|
if (StringUtils.isEmpty(provisionRequest.getDeviceName())) { |
||||
|
log.warn("Provision request contains empty device name!"); |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) { |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); |
||||
|
} |
||||
|
|
||||
|
DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey); |
||||
|
|
||||
|
if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null || |
||||
|
targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) { |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); |
||||
|
} |
||||
|
|
||||
|
Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null); |
||||
|
|
||||
|
switch (targetProfile.getProvisionType()) { |
||||
|
case ALLOW_CREATE_NEW_DEVICES: |
||||
|
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { |
||||
|
if (targetDevice != null) { |
||||
|
log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName()); |
||||
|
notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false); |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} else { |
||||
|
return createDevice(provisionRequest, targetProfile); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
case CHECK_PRE_PROVISIONED_DEVICES: |
||||
|
if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { |
||||
|
if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) { |
||||
|
return processProvision(targetDevice, provisionRequest); |
||||
|
} else { |
||||
|
log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName()); |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); |
||||
|
} |
||||
|
|
||||
|
private ProvisionResponse processProvision(Device device, ProvisionRequest provisionRequest) { |
||||
|
try { |
||||
|
Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(), |
||||
|
DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); |
||||
|
if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { |
||||
|
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} else { |
||||
|
saveProvisionStateAttribute(device).get(); |
||||
|
notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true); |
||||
|
} |
||||
|
} catch (InterruptedException | ExecutionException e) { |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} |
||||
|
return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS); |
||||
|
} |
||||
|
|
||||
|
private ProvisionResponse createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { |
||||
|
deviceCreationLock.lock(); |
||||
|
try { |
||||
|
return processCreateDevice(provisionRequest, profile); |
||||
|
} finally { |
||||
|
deviceCreationLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) { |
||||
|
pushProvisionEventToRuleEngine(provisionRequest, device, type); |
||||
|
logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest); |
||||
|
} |
||||
|
|
||||
|
private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { |
||||
|
Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); |
||||
|
try { |
||||
|
if (device == null) { |
||||
|
if (StringUtils.isEmpty(provisionRequest.getDeviceName())) { |
||||
|
String newDeviceName = RandomStringUtils.randomAlphanumeric(20); |
||||
|
log.info("Device name not found in provision request. Generated name is: {}", newDeviceName); |
||||
|
provisionRequest.setDeviceName(newDeviceName); |
||||
|
} |
||||
|
Device savedDevice = deviceService.saveDevice(provisionRequest, profile); |
||||
|
|
||||
|
deviceStateService.onDeviceAdded(savedDevice); |
||||
|
saveProvisionStateAttribute(savedDevice).get(); |
||||
|
pushDeviceCreatedEventToRuleEngine(savedDevice); |
||||
|
notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true); |
||||
|
|
||||
|
return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS); |
||||
|
} else { |
||||
|
log.warn("[{}] The device is already provisioned!", device.getName()); |
||||
|
notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} |
||||
|
} catch (InterruptedException | ExecutionException e) { |
||||
|
throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<List<Void>> saveProvisionStateAttribute(Device device) { |
||||
|
return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, |
||||
|
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE), |
||||
|
System.currentTimeMillis()))); |
||||
|
} |
||||
|
|
||||
|
private DeviceCredentials getDeviceCredentials(Device device) { |
||||
|
return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); |
||||
|
} |
||||
|
|
||||
|
private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) { |
||||
|
try { |
||||
|
JsonNode entityNode = JacksonUtil.valueToTree(request); |
||||
|
TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode)); |
||||
|
sendToRuleEngine(device.getTenantId(), msg, null); |
||||
|
} catch (IllegalArgumentException e) { |
||||
|
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void pushDeviceCreatedEventToRuleEngine(Device device) { |
||||
|
try { |
||||
|
ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); |
||||
|
TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); |
||||
|
sendToRuleEngine(device.getTenantId(), msg, null); |
||||
|
} catch (JsonProcessingException | IllegalArgumentException e) { |
||||
|
log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { |
||||
|
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator()); |
||||
|
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg)) |
||||
|
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) |
||||
|
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); |
||||
|
ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); |
||||
|
} |
||||
|
|
||||
|
private TbMsgMetaData createTbMsgMetaData(Device device) { |
||||
|
TbMsgMetaData metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("tenantId", device.getTenantId().toString()); |
||||
|
return metaData; |
||||
|
} |
||||
|
|
||||
|
private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) { |
||||
|
ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE; |
||||
|
auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.text.StrSubstitutor; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; |
||||
|
import org.thingsboard.server.dao.oauth2.OAuth2User; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class BasicMapperUtils { |
||||
|
private static final String START_PLACEHOLDER_PREFIX = "%{"; |
||||
|
private static final String END_PLACEHOLDER_PREFIX = "}"; |
||||
|
|
||||
|
public static OAuth2User getOAuth2User(String email, Map<String, Object> attributes, OAuth2MapperConfig config) { |
||||
|
OAuth2User oauth2User = new OAuth2User(); |
||||
|
oauth2User.setEmail(email); |
||||
|
oauth2User.setTenantName(getTenantName(email, attributes, config)); |
||||
|
if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) { |
||||
|
String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey()); |
||||
|
oauth2User.setLastName(lastName); |
||||
|
} |
||||
|
if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) { |
||||
|
String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey()); |
||||
|
oauth2User.setFirstName(firstName); |
||||
|
} |
||||
|
if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) { |
||||
|
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); |
||||
|
String customerName = sub.replace(config.getBasic().getCustomerNamePattern()); |
||||
|
oauth2User.setCustomerName(customerName); |
||||
|
} |
||||
|
oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen()); |
||||
|
if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) { |
||||
|
oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName()); |
||||
|
} |
||||
|
return oauth2User; |
||||
|
} |
||||
|
|
||||
|
public static String getTenantName(String email, Map<String, Object> attributes, OAuth2MapperConfig config) { |
||||
|
switch (config.getBasic().getTenantNameStrategy()) { |
||||
|
case EMAIL: |
||||
|
return email; |
||||
|
case DOMAIN: |
||||
|
return email.substring(email .indexOf("@") + 1); |
||||
|
case CUSTOM: |
||||
|
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); |
||||
|
return sub.replace(config.getBasic().getTenantNamePattern()); |
||||
|
default: |
||||
|
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static String getStringAttributeByKey(Map<String, Object> attributes, String key) { |
||||
|
String result = null; |
||||
|
try { |
||||
|
result = (String) attributes.get(key); |
||||
|
} catch (Exception e) { |
||||
|
log.warn("Can't convert attribute to String by key " + key); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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 lombok.Data; |
||||
|
import lombok.ToString; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
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.dao.oauth2.OAuth2Configuration; |
||||
|
import org.thingsboard.server.dao.oauth2.OAuth2User; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
|
||||
|
@Service(value = "githubOAuth2ClientMapper") |
||||
|
@Slf4j |
||||
|
public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { |
||||
|
private static final String EMAIL_URL_KEY = "emailUrl"; |
||||
|
|
||||
|
private static final String AUTHORIZATION = "Authorization"; |
||||
|
|
||||
|
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); |
||||
|
|
||||
|
@Autowired |
||||
|
private OAuth2Configuration oAuth2Configuration; |
||||
|
|
||||
|
@Override |
||||
|
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2ClientRegistrationInfo clientRegistration) { |
||||
|
OAuth2MapperConfig config = clientRegistration.getMapperConfig(); |
||||
|
Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper(); |
||||
|
String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); |
||||
|
Map<String, Object> attributes = token.getPrincipal().getAttributes(); |
||||
|
OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); |
||||
|
return getOrCreateSecurityUserFromOAuth2User(oAuth2User, clientRegistration); |
||||
|
} |
||||
|
|
||||
|
private synchronized String getEmail(String emailUrl, String oauth2Token) { |
||||
|
restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token); |
||||
|
|
||||
|
RestTemplate restTemplate = restTemplateBuilder.build(); |
||||
|
GithubEmailsResponse githubEmailsResponse; |
||||
|
try { |
||||
|
githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody(); |
||||
|
if (githubEmailsResponse == null){ |
||||
|
throw new RuntimeException("Empty Github response!"); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("There was an error during connection to Github API", e); |
||||
|
throw new RuntimeException("Unable to login. Please contact your Administrator!"); |
||||
|
} |
||||
|
Optional<String> emailOpt = githubEmailsResponse.stream() |
||||
|
.filter(GithubEmailResponse::isPrimary) |
||||
|
.map(GithubEmailResponse::getEmail) |
||||
|
.findAny(); |
||||
|
if (emailOpt.isPresent()){ |
||||
|
return emailOpt.get(); |
||||
|
} else { |
||||
|
log.error("Could not find primary email from {}.", githubEmailsResponse); |
||||
|
throw new RuntimeException("Unable to login. Please contact your Administrator!"); |
||||
|
} |
||||
|
} |
||||
|
private static class GithubEmailsResponse extends ArrayList<GithubEmailResponse> {} |
||||
|
|
||||
|
@Data |
||||
|
@ToString |
||||
|
private static class GithubEmailResponse { |
||||
|
private String email; |
||||
|
private boolean verified; |
||||
|
private boolean primary; |
||||
|
private String visibility; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.sms.SmsSender; |
||||
|
import org.thingsboard.rule.engine.api.sms.exception.SmsParseException; |
||||
|
|
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class AbstractSmsSender implements SmsSender { |
||||
|
|
||||
|
private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$"); |
||||
|
|
||||
|
private static final int MAX_SMS_MESSAGE_LENGTH = 1600; |
||||
|
private static final int MAX_SMS_SEGMENT_LENGTH = 70; |
||||
|
|
||||
|
protected String validatePhoneNumber(String phoneNumber) throws SmsParseException { |
||||
|
phoneNumber = phoneNumber.trim(); |
||||
|
if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) { |
||||
|
throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format."); |
||||
|
} |
||||
|
return phoneNumber; |
||||
|
} |
||||
|
|
||||
|
protected String prepareMessage(String message) { |
||||
|
message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n"); |
||||
|
if (message.length() > MAX_SMS_MESSAGE_LENGTH) { |
||||
|
log.warn("SMS message exceeds maximum symbols length and will be truncated"); |
||||
|
message = message.substring(0, MAX_SMS_MESSAGE_LENGTH); |
||||
|
} |
||||
|
return message; |
||||
|
} |
||||
|
|
||||
|
protected int countMessageSegments(String message) { |
||||
|
return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.rule.engine.api.sms.SmsSender; |
||||
|
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; |
||||
|
import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration; |
||||
|
import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration; |
||||
|
import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration; |
||||
|
import org.thingsboard.server.service.sms.aws.AwsSmsSender; |
||||
|
import org.thingsboard.server.service.sms.twilio.TwilioSmsSender; |
||||
|
|
||||
|
@Component |
||||
|
public class DefaultSmsSenderFactory implements SmsSenderFactory { |
||||
|
|
||||
|
@Override |
||||
|
public SmsSender createSmsSender(SmsProviderConfiguration config) { |
||||
|
switch (config.getType()) { |
||||
|
case AWS_SNS: |
||||
|
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); |
||||
|
case TWILIO: |
||||
|
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); |
||||
|
default: |
||||
|
throw new RuntimeException("Unknown SMS provider type " + config.getType()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.core.NestedRuntimeException; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.rule.engine.api.SmsService; |
||||
|
import org.thingsboard.rule.engine.api.sms.SmsSender; |
||||
|
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; |
||||
|
import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration; |
||||
|
import org.thingsboard.server.common.data.sms.config.TestSmsRequest; |
||||
|
import org.thingsboard.server.common.data.AdminSettings; |
||||
|
import org.thingsboard.server.common.data.ApiUsageRecordKey; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.dao.settings.AdminSettingsService; |
||||
|
import org.thingsboard.server.dao.util.mapping.JacksonUtil; |
||||
|
import org.thingsboard.server.queue.usagestats.TbApiUsageClient; |
||||
|
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
|
||||
|
@Service |
||||
|
@Slf4j |
||||
|
public class DefaultSmsService implements SmsService { |
||||
|
|
||||
|
private final SmsSenderFactory smsSenderFactory; |
||||
|
private final AdminSettingsService adminSettingsService; |
||||
|
private final TbApiUsageStateService apiUsageStateService; |
||||
|
private final TbApiUsageClient apiUsageClient; |
||||
|
|
||||
|
private SmsSender smsSender; |
||||
|
|
||||
|
public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) { |
||||
|
this.smsSenderFactory = smsSenderFactory; |
||||
|
this.adminSettingsService = adminSettingsService; |
||||
|
this.apiUsageStateService = apiUsageStateService; |
||||
|
this.apiUsageClient = apiUsageClient; |
||||
|
} |
||||
|
|
||||
|
@PostConstruct |
||||
|
private void init() { |
||||
|
updateSmsConfiguration(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
private void destroy() { |
||||
|
if (this.smsSender != null) { |
||||
|
this.smsSender.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void updateSmsConfiguration() { |
||||
|
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms"); |
||||
|
if (settings != null) { |
||||
|
try { |
||||
|
JsonNode jsonConfig = settings.getJsonValue(); |
||||
|
SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class); |
||||
|
SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration); |
||||
|
if (this.smsSender != null) { |
||||
|
this.smsSender.destroy(); |
||||
|
} |
||||
|
this.smsSender = newSmsSender; |
||||
|
} catch (Exception e) { |
||||
|
log.error("Failed to create SMS sender", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int sendSms(String numberTo, String message) throws ThingsboardException { |
||||
|
if (this.smsSender == null) { |
||||
|
throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL); |
||||
|
} |
||||
|
return this.sendSms(this.smsSender, numberTo, message); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void sendSms(TenantId tenantId, String[] numbersTo, String message) throws ThingsboardException { |
||||
|
if (apiUsageStateService.getApiUsageState(tenantId).isSmsSendEnabled()) { |
||||
|
int smsCount = 0; |
||||
|
try { |
||||
|
for (String numberTo : numbersTo) { |
||||
|
smsCount += this.sendSms(numberTo, message); |
||||
|
} |
||||
|
} finally { |
||||
|
if (smsCount > 0) { |
||||
|
apiUsageClient.report(tenantId, ApiUsageRecordKey.SMS_EXEC_COUNT, smsCount); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
throw new RuntimeException("SMS sending is disabled due to API limits!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException { |
||||
|
SmsSender testSmsSender; |
||||
|
try { |
||||
|
testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration()); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage()); |
||||
|
testSmsSender.destroy(); |
||||
|
} |
||||
|
|
||||
|
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException { |
||||
|
try { |
||||
|
return smsSender.sendSms(numberTo, message); |
||||
|
} catch (Exception e) { |
||||
|
throw handleException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ThingsboardException handleException(Exception exception) { |
||||
|
String message; |
||||
|
if (exception instanceof NestedRuntimeException) { |
||||
|
message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage(); |
||||
|
} else { |
||||
|
message = exception.getMessage(); |
||||
|
} |
||||
|
log.warn("Unable to send SMS: {}", message); |
||||
|
return new ThingsboardException(String.format("Unable to send SMS: %s", message), |
||||
|
ThingsboardErrorCode.GENERAL); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms; |
||||
|
|
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.common.util.AbstractListeningExecutor; |
||||
|
|
||||
|
@Component |
||||
|
public class SmsExecutorService extends AbstractListeningExecutor { |
||||
|
|
||||
|
@Value("${actors.rule.sms_thread_pool_size}") |
||||
|
private int smsExecutorThreadPoolSize; |
||||
|
|
||||
|
@Override |
||||
|
protected int getThreadPollSize() { |
||||
|
return smsExecutorThreadPoolSize; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms.aws; |
||||
|
|
||||
|
import com.amazonaws.auth.AWSCredentials; |
||||
|
import com.amazonaws.auth.AWSStaticCredentialsProvider; |
||||
|
import com.amazonaws.auth.BasicAWSCredentials; |
||||
|
import com.amazonaws.services.sns.AmazonSNS; |
||||
|
import com.amazonaws.services.sns.AmazonSNSClient; |
||||
|
import com.amazonaws.services.sns.model.PublishRequest; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.sms.exception.SmsException; |
||||
|
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; |
||||
|
import org.thingsboard.server.service.sms.AbstractSmsSender; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class AwsSmsSender extends AbstractSmsSender { |
||||
|
|
||||
|
private AmazonSNS snsClient; |
||||
|
|
||||
|
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) { |
||||
|
if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) { |
||||
|
throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!"); |
||||
|
} |
||||
|
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey()); |
||||
|
AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); |
||||
|
this.snsClient = AmazonSNSClient.builder() |
||||
|
.withCredentials(credProvider) |
||||
|
.withRegion(config.getRegion()) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int sendSms(String numberTo, String message) throws SmsException { |
||||
|
numberTo = this.validatePhoneNumber(numberTo); |
||||
|
message = this.prepareMessage(message); |
||||
|
try { |
||||
|
PublishRequest publishRequest = new PublishRequest() |
||||
|
.withPhoneNumber(numberTo) |
||||
|
.withMessage(message); |
||||
|
this.snsClient.publish(publishRequest); |
||||
|
return this.countMessageSegments(message); |
||||
|
} catch (Exception e) { |
||||
|
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
if (this.snsClient != null) { |
||||
|
try { |
||||
|
this.snsClient.shutdown(); |
||||
|
} catch (Exception e) { |
||||
|
log.error("Failed to shutdown SNS client during destroy()", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.sms.twilio; |
||||
|
|
||||
|
import com.twilio.http.TwilioRestClient; |
||||
|
import com.twilio.rest.api.v2010.account.Message; |
||||
|
import com.twilio.type.PhoneNumber; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.sms.exception.SmsException; |
||||
|
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; |
||||
|
import org.thingsboard.server.service.sms.AbstractSmsSender; |
||||
|
|
||||
|
public class TwilioSmsSender extends AbstractSmsSender { |
||||
|
|
||||
|
private TwilioRestClient twilioRestClient; |
||||
|
private String numberFrom; |
||||
|
|
||||
|
public TwilioSmsSender(TwilioSmsProviderConfiguration config) { |
||||
|
if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) { |
||||
|
throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!"); |
||||
|
} |
||||
|
this.numberFrom = this.validatePhoneNumber(config.getNumberFrom()); |
||||
|
this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int sendSms(String numberTo, String message) throws SmsException { |
||||
|
numberTo = this.validatePhoneNumber(numberTo); |
||||
|
message = this.prepareMessage(message); |
||||
|
try { |
||||
|
String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments(); |
||||
|
return Integer.valueOf(numSegments); |
||||
|
} catch (Exception e) { |
||||
|
throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 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.telemetry; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 27.03.18. |
||||
|
*/ |
||||
|
public interface InternalTelemetryService extends RuleEngineTelemetryService { |
||||
|
|
||||
|
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Integer> callback); |
||||
|
|
||||
|
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Integer> callback); |
||||
|
|
||||
|
void saveAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice, FutureCallback<Void> callback); |
||||
|
|
||||
|
void saveLatestAndNotifyInternal(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback); |
||||
|
|
||||
|
void deleteAndNotifyInternal(TenantId tenantId, EntityId entityId, String scope, List<String> keys, FutureCallback<Void> callback); |
||||
|
|
||||
|
void deleteLatestInternal(TenantId tenantId, EntityId entityId, List<String> keys, FutureCallback<Void> callback); |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,145 @@ |
|||||
|
<#-- |
||||
|
|
||||
|
Copyright © 2016-2020 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. |
||||
|
|
||||
|
--> |
||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" |
||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
||||
|
<html xmlns="http://www.w3.org/1999/xhtml" |
||||
|
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> |
||||
|
<head> |
||||
|
<meta name="viewport" content="width=device-width"/> |
||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> |
||||
|
<title>Thingsboard - Api Usage State</title> |
||||
|
|
||||
|
|
||||
|
<style type="text/css"> |
||||
|
img { |
||||
|
max-width: 100%; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-webkit-text-size-adjust: none; |
||||
|
width: 100% !important; |
||||
|
height: 100%; |
||||
|
line-height: 1.6em; |
||||
|
} |
||||
|
|
||||
|
body { |
||||
|
background-color: #f6f6f6; |
||||
|
} |
||||
|
|
||||
|
@media only screen and (max-width: 640px) { |
||||
|
body { |
||||
|
padding: 0 !important; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-weight: 800 !important; |
||||
|
margin: 20px 0 5px !important; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-weight: 800 !important; |
||||
|
margin: 20px 0 5px !important; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-weight: 800 !important; |
||||
|
margin: 20px 0 5px !important; |
||||
|
} |
||||
|
|
||||
|
h4 { |
||||
|
font-weight: 800 !important; |
||||
|
margin: 20px 0 5px !important; |
||||
|
} |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 22px !important; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 18px !important; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 16px !important; |
||||
|
} |
||||
|
|
||||
|
.container { |
||||
|
padding: 0 !important; |
||||
|
width: 100% !important; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
padding: 0 !important; |
||||
|
} |
||||
|
|
||||
|
.content-wrap { |
||||
|
padding: 10px !important; |
||||
|
} |
||||
|
|
||||
|
.invoice { |
||||
|
width: 100% !important; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body itemscope itemtype="http://schema.org/EmailMessage" |
||||
|
style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" |
||||
|
bgcolor="#f6f6f6"> |
||||
|
|
||||
|
<table class="main" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6"> |
||||
|
<tbody> |
||||
|
<tr style="box-sizing: border-box; margin: 0px;"> |
||||
|
<td class="content-wrap" style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 20px;" align="center" valign="top"> |
||||
|
<table style="box-sizing: border-box; border: 1px solid #e9e9e9; border-radius: 3px; margin: 0px; height: 367px; background-color: #ffffff; width: 600px; max-width: 600px !important;" width="600" cellspacing="0" cellpadding="0"> |
||||
|
<tbody> |
||||
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> |
||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px; height: 110px;" valign="top"><img src="https://media.thingsboard.io/email/head.png" alt="" width="598" height="91" /></td> |
||||
|
</tr> |
||||
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; margin: 0;"> |
||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #000000; box-sizing: border-box; font-size: 16px; margin: 0px; padding: 0px 32px; height: 66px; vertical-align: middle;" valign="middle">Your ThingsBoard account feature was <strong>disabled</strong></td> |
||||
|
</tr> |
||||
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; line-height: 24px; margin: 0;"> |
||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0px; padding: 0px 32px; height: 93px; vertical-align: top;" valign="top"> |
||||
|
<div style="padding: 16px; margin-bottom: 24px; border: solid 2px #EB5757; border-radius: 6px; background: rgba(235, 87, 87, 0.05);"><img style="vertical-align: middle; padding-right: 6px;" src="https://media.thingsboard.io/email/alarm.png" alt="" width="20" height="20" /> |
||||
|
<div style="display: inline; vertical-align: middle;">We have <strong>disabled</strong> the ${apiFeature} for your account because ThingsBoard has already <strong>${apiLimitValueLabel}.</strong></div> |
||||
|
</div> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> |
||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 59px;" valign="top">Please contact your system administrator to resolve the issue.</td> |
||||
|
</tr> |
||||
|
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"> |
||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 40px;" valign="top">— The ThingsBoard</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<table style="color: #999999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; margin: 0px auto; height: 64px; background-color: #f6f6f6; width: 100%;" cellpadding="0px 0px 20px"> |
||||
|
<tbody> |
||||
|
<tr style="box-sizing: border-box; margin: 0px;"> |
||||
|
<td class="aligncenter content-block" style="box-sizing: border-box; font-size: 12px; margin: 0px; padding: 0px 0px 20px; width: 600px; text-align: center; vertical-align: middle;" align="center" valign="top">This email was sent to <a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a> by ThingsBoard.</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</body> |
||||
|
</html> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue