Skip to content

Commit 81c0913

Browse files
fcv-iteratorItGufCabos2gitFritsenAugustHA-Iterator
authored
Roadmap features: Mqtt broker/subscriber support (#213)
* Added missing packages to package lock * Create .licrc for licensebat scan * Deleting licensebat file as it is obsolete * IOT-1416: Minor changes for supporting the front-end-changes for having OpenDataDK datatargets more separate from HTTP-Push.. * Finished implementing new flow for sending ODDK-registration-mails directly from OS2IoT, instead of having to go through users mail-client.. * Shell framework for new device type, currently not working * Renamed the new entity for OpenDataDK datatargets, to match naming-convention of other ODDK-files.. * Now possible to create broker, not fully populated * Added new migration with correct enum for authenticationType * Most broker options done. Cert version not implemented * Fix missed enum file rename * MQTT publisher and subscriber almost fully implemented * Added encryption of iot-device secret fields * added openssl to dockerfile * csv export half baked * More unfinished work on csv generator * Csv generator done * Small fixes * updated vm2 version * Pr fixes * Reworked marking device as invalid for mqtt-clients * made fallback on ENCRYPTION_SYMMETRIC_KEY and changed fallback on CA_KEY_PASSWORD. * Added error checking for required fields on mqtt units * Added CA certificate to brokers using password auth on fetch * Changed error message on mqtt certificate rows * Moved hashed version of mqtt password to own column * Added new columns to csv export * Removed redundant todo * Add id back to csv export * Removed some unused code * Recreate mqtt subscriber client on edit * Validate create many input * Sæt min length on mqtt username and password (Previously an empty string was accepted * Deleted wrongly placed migration * fixed port error and made sure that mqtt will be removed when unable to connect. * Renamed MqttBroker -> MqttInternalBroker and MqttSubscriber -> MqttExternalBroker * Added migrations for renaming --------- Co-authored-by: Nikolaj Gustafsson <[email protected]> Co-authored-by: OS2 primary account <[email protected]> Co-authored-by: Chris Johansen <[email protected]> Co-authored-by: August Andersen <[email protected]>
1 parent 485ed4c commit 81c0913

File tree

43 files changed

+1568
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1568
-176
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# compiled output
22
/dist
33
/node_modules
4-
4+
ca.crt
5+
ca.key
56
# Logs
67
logs
78
*.log

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ ENV NODE_ENV build
55

66
RUN npm install -g nest eslint jest
77

8+
RUN apk add openssl
9+
810
USER node
911

1012
# ENV NPM_CONFIG_PREFIX=/home/node/.npm-global

package-lock.json

Lines changed: 170 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@types/geojson": "^7946.0.7",
4545
"@types/kafkajs": "^1.9.0",
4646
"@types/passport-saml": "^1.1.3",
47+
"@types/pem": "^1.9.6",
4748
"@types/uuid": "^8.3.0",
4849
"@types/ws": "^8.5.3",
4950
"@types/xml2js": "^0.4.7",
@@ -57,6 +58,7 @@
5758
"class-validator": "^0.14.0",
5859
"compression": "^1.7.4",
5960
"cookie-parser": "^1.4.5",
61+
"crypto-js": "^4.1.1",
6062
"kafkajs": "^2.2.4",
6163
"lodash": "^4.17.20",
6264
"mqtt": "^4.3.7",
@@ -67,6 +69,7 @@
6769
"passport-jwt": "^4.0.0",
6870
"passport-local": "^1.0.0",
6971
"passport-saml": "^3.2.4",
72+
"pem": "^1.14.7",
7073
"pg": "^8.5.1",
7174
"protobufjs": "^6.11.3",
7275
"reflect-metadata": "^0.1.13",
@@ -75,7 +78,7 @@
7578
"swagger-ui-express": "^4.1.5",
7679
"typeorm": "^0.3.10",
7780
"uuid": "^8.3.2",
78-
"vm2": "^3.9.11",
81+
"vm2": "^3.9.17",
7982
"wait-for-expect": "^3.0.2"
8083
},
8184
"devDependencies": {
@@ -87,6 +90,7 @@
8790
"@types/compression": "^1.7.0",
8891
"@types/cookie-parser": "^1.4.2",
8992
"@types/cron": "^1.7.2",
93+
"@types/crypto-js": "^4.1.1",
9094
"@types/express": "^4.17.9",
9195
"@types/geojson": "^7946.0.7",
9296
"@types/kafkajs": "^1.9.0",

src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
ApiUnauthorizedResponse,
2525
} from "@nestjs/swagger";
2626

27-
import { ComposeAuthGuard } from '@auth/compose-auth.guard';
27+
import { ComposeAuthGuard } from "@auth/compose-auth.guard";
2828
import { Read, ApplicationAdmin } from "@auth/roles.decorator";
2929
import { RolesGuard } from "@auth/roles.guard";
3030
import { CreateIoTDevicePayloadDecoderDataTargetConnectionDto } from "@dto/create-iot-device-payload-decoder-data-target-connection.dto";
@@ -36,7 +36,10 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto";
3636
import { UpdateIoTDevicePayloadDecoderDataTargetConnectionDto as UpdateConnectionDto } from "@dto/update-iot-device-payload-decoder-data-target-connection.dto";
3737
import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity";
3838
import { ErrorCodes } from "@enum/error-codes.enum";
39-
import { checkIfUserHasAccessToApplication, ApplicationAccessScope } from "@helpers/security-helper";
39+
import {
40+
checkIfUserHasAccessToApplication,
41+
ApplicationAccessScope,
42+
} from "@helpers/security-helper";
4043
import { IoTDevicePayloadDecoderDataTargetConnectionService } from "@services/device-management/iot-device-payload-decoder-data-target-connection.service";
4144
import { IoTDeviceService } from "@services/device-management/iot-device.service";
4245
import { AuditLog } from "@services/audit-log.service";
@@ -184,7 +187,11 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController {
184187
) {
185188
const iotDevices = await this.iotDeviceService.findManyByIds(ids);
186189
iotDevices.forEach(x => {
187-
checkIfUserHasAccessToApplication(req, x.application.id, ApplicationAccessScope.Write);
190+
checkIfUserHasAccessToApplication(
191+
req,
192+
x.application.id,
193+
ApplicationAccessScope.Write
194+
);
188195
});
189196
}
190197

@@ -231,7 +238,11 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController {
231238
const newIotDevice = await this.iotDeviceService.findOne(
232239
updateDto.iotDeviceIds[0]
233240
);
234-
checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write);
241+
checkIfUserHasAccessToApplication(
242+
req,
243+
newIotDevice.application.id,
244+
ApplicationAccessScope.Write
245+
);
235246
const oldConnection = await this.service.findOne(id);
236247
await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req);
237248
const oldIds = oldConnection.iotDevices.map(x => x.id);

src/controllers/admin-controller/iot-device.controller.ts

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Post,
1313
Put,
1414
Req,
15+
StreamableFile,
1516
UseGuards,
1617
} from "@nestjs/common";
1718
import {
@@ -34,7 +35,10 @@ import { LoRaWANDeviceWithChirpstackDataDto } from "@dto/lorawan-device-with-chi
3435
import { UpdateIoTDeviceDto } from "@dto/update-iot-device.dto";
3536
import { IoTDevice } from "@entities/iot-device.entity";
3637
import { ErrorCodes } from "@enum/error-codes.enum";
37-
import { checkIfUserHasAccessToApplication, ApplicationAccessScope } from "@helpers/security-helper";
38+
import {
39+
ApplicationAccessScope,
40+
checkIfUserHasAccessToApplication,
41+
} from "@helpers/security-helper";
3842
import { IoTDeviceService } from "@services/device-management/iot-device.service";
3943
import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend-data.dto";
4044
import { CreateIoTDeviceDownlinkDto } from "@dto/create-iot-device-downlink.dto";
@@ -56,6 +60,8 @@ import {
5660
} from "@helpers/iot-device.helper";
5761
import { DeviceStatsResponseDto } from "@dto/chirpstack/device/device-stats.response.dto";
5862
import { GenericHTTPDevice } from "@entities/generic-http-device.entity";
63+
import { MQTTInternalBrokerDeviceDTO } from "@dto/mqtt-internal-broker-device.dto";
64+
import { MQTTExternalBrokerDeviceDTO } from "@dto/mqtt-external-broker-device.dto";
5965

6066
@ApiTags("IoT Device")
6167
@Controller("iot-device")
@@ -80,7 +86,11 @@ export class IoTDeviceController {
8086
@Req() req: AuthenticatedRequest,
8187
@Param("id", new ParseIntPipe()) id: number
8288
): Promise<
83-
IoTDevice | LoRaWANDeviceWithChirpstackDataDto | SigFoxDeviceWithBackendDataDto
89+
| IoTDevice
90+
| LoRaWANDeviceWithChirpstackDataDto
91+
| SigFoxDeviceWithBackendDataDto
92+
| MQTTInternalBrokerDeviceDTO
93+
| MQTTExternalBrokerDeviceDTO
8494
> {
8595
let result = undefined;
8696
try {
@@ -96,7 +106,11 @@ export class IoTDeviceController {
96106
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
97107
}
98108

99-
checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read);
109+
checkIfUserHasAccessToApplication(
110+
req,
111+
result.application.id,
112+
ApplicationAccessScope.Read
113+
);
100114

101115
return result;
102116
}
@@ -120,7 +134,11 @@ export class IoTDeviceController {
120134
if (!device) {
121135
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
122136
}
123-
checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read);
137+
checkIfUserHasAccessToApplication(
138+
req,
139+
device.application.id,
140+
ApplicationAccessScope.Read
141+
);
124142
if (device.type == IoTDeviceType.LoRaWAN) {
125143
return this.chirpstackDeviceService.getDownlinkQueue(
126144
(device as LoRaWANDevice).deviceEUI
@@ -141,7 +159,11 @@ export class IoTDeviceController {
141159
@Param("id", new ParseIntPipe()) id: number
142160
): Promise<DeviceStatsResponseDto[]> {
143161
const device = await this.iotDeviceService.findOne(id);
144-
checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read);
162+
checkIfUserHasAccessToApplication(
163+
req,
164+
device.application.id,
165+
ApplicationAccessScope.Read
166+
);
145167

146168
return this.iotDeviceService.findStats(device);
147169
}
@@ -155,7 +177,11 @@ export class IoTDeviceController {
155177
@Body() createDto: CreateIoTDeviceDto
156178
): Promise<IoTDevice> {
157179
try {
158-
checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write);
180+
checkIfUserHasAccessToApplication(
181+
req,
182+
createDto.applicationId,
183+
ApplicationAccessScope.Write
184+
);
159185
const device = await this.iotDeviceService.create(createDto, req.user.userId);
160186
AuditLog.success(
161187
ActionType.CREATE,
@@ -192,7 +218,11 @@ export class IoTDeviceController {
192218
if (!device) {
193219
throw new NotFoundException();
194220
}
195-
checkIfUserHasAccessToApplication(req, device?.application?.id, ApplicationAccessScope.Write);
221+
checkIfUserHasAccessToApplication(
222+
req,
223+
device?.application?.id,
224+
ApplicationAccessScope.Write
225+
);
196226
const result = await this.downlinkService.createDownlink(dto, device);
197227
AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId);
198228
return result;
@@ -217,10 +247,18 @@ export class IoTDeviceController {
217247
false
218248
);
219249
try {
220-
checkIfUserHasAccessToApplication(req, oldIotDevice.application.id, ApplicationAccessScope.Write);
250+
checkIfUserHasAccessToApplication(
251+
req,
252+
oldIotDevice.application.id,
253+
ApplicationAccessScope.Write
254+
);
221255
if (updateDto.applicationId !== oldIotDevice.application.id) {
222256
// New application
223-
checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write);
257+
checkIfUserHasAccessToApplication(
258+
req,
259+
updateDto.applicationId,
260+
ApplicationAccessScope.Write
261+
);
224262
}
225263
} catch (err) {
226264
AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id);
@@ -251,7 +289,13 @@ export class IoTDeviceController {
251289
@Body() createDto: CreateIoTDeviceBatchDto
252290
): Promise<IotDeviceBatchResponseDto[]> {
253291
try {
254-
createDto.data.forEach(createDto => checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write));
292+
createDto.data.forEach(createDto =>
293+
checkIfUserHasAccessToApplication(
294+
req,
295+
createDto.applicationId,
296+
ApplicationAccessScope.Write
297+
)
298+
);
255299

256300
const devices = await this.iotDeviceService.createMany(
257301
createDto.data,
@@ -348,7 +392,11 @@ export class IoTDeviceController {
348392
id,
349393
false
350394
);
351-
checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write);
395+
checkIfUserHasAccessToApplication(
396+
req,
397+
oldIotDevice?.application?.id,
398+
ApplicationAccessScope.Write
399+
);
352400
const result = await this.iotDeviceService.delete(oldIotDevice);
353401
AuditLog.success(ActionType.DELETE, IoTDevice.name, req.user.userId, id);
354402
return new DeleteResponseDto(result.affected);
@@ -364,16 +412,24 @@ export class IoTDeviceController {
364412
async resetHttpDeviceApiKey(
365413
@Req() req: AuthenticatedRequest,
366414
@Param("id", new ParseIntPipe()) id: number
367-
): Promise<Pick<GenericHTTPDevice, 'apiKey'>> {
415+
): Promise<Pick<GenericHTTPDevice, "apiKey">> {
368416
try {
369417
const oldIotDevice = await this.iotDeviceService.findOne(id);
370-
checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write);
418+
checkIfUserHasAccessToApplication(
419+
req,
420+
oldIotDevice?.application?.id,
421+
ApplicationAccessScope.Write
422+
);
371423

372424
if (oldIotDevice.type !== IoTDeviceType.GenericHttp) {
373-
throw new BadRequestException("The requested device is not a generic HTTP device");
425+
throw new BadRequestException(
426+
"The requested device is not a generic HTTP device"
427+
);
374428
}
375429

376-
const result = await this.iotDeviceService.resetHttpDeviceApiKey(oldIotDevice as GenericHTTPDevice);
430+
const result = await this.iotDeviceService.resetHttpDeviceApiKey(
431+
oldIotDevice as GenericHTTPDevice
432+
);
377433
AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, id);
378434
return {
379435
apiKey: result.apiKey,
@@ -383,4 +439,28 @@ export class IoTDeviceController {
383439
throw err;
384440
}
385441
}
442+
443+
@Get("getDevicesMetadataCsv/:applicationId")
444+
@ApiOperation({
445+
summary: "Get csv containing metadata for all devices in an application",
446+
})
447+
@ApiBadRequestResponse()
448+
async getDevicesMetadataCsv(
449+
@Req() req: AuthenticatedRequest,
450+
@Param("applicationId", new ParseIntPipe()) applicationId: number
451+
): Promise<StreamableFile> {
452+
try {
453+
checkIfUserHasAccessToApplication(
454+
req,
455+
applicationId,
456+
ApplicationAccessScope.Read
457+
);
458+
const csvFile = await this.iotDeviceService.getDevicesMetadataCsv(
459+
applicationId
460+
);
461+
return new StreamableFile(csvFile);
462+
} catch (err) {
463+
this.logger.error(err);
464+
}
465+
}
386466
}

src/entities/dto/create-iot-device.dto.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { CreateLoRaWANSettingsDto } from "./create-lorawan-settings.dto";
1818
import { CreateSigFoxSettingsDto } from "./create-sigfox-settings.dto";
1919
import { IsMetadataJson } from "@helpers/is-metadata-json.validator";
2020
import { nameof } from "@helpers/type-helper";
21+
import { CreateMqttInternalBrokerSettingsDto } from "@dto/create-mqtt-internal-broker-settings.dto";
22+
import { CreateMqttExternalBrokerSettingsDto } from "@dto/create-mqtt-external-broker-settings.dto";
2123

2224
export class CreateIoTDeviceDto {
2325
@ApiProperty({ required: true })
@@ -71,14 +73,26 @@ export class CreateIoTDeviceDto {
7173
deviceModelId?: number;
7274

7375
@ApiProperty({ required: false })
74-
@ValidateIf(o => o.type == IoTDeviceType.LoRaWAN)
76+
@ValidateIf(o => o.type === IoTDeviceType.LoRaWAN)
7577
@ValidateNested({ each: true })
7678
@Type(() => CreateLoRaWANSettingsDto)
7779
lorawanSettings?: CreateLoRaWANSettingsDto;
7880

7981
@ApiProperty({ required: false })
80-
@ValidateIf(o => o.type == IoTDeviceType.SigFox)
82+
@ValidateIf(o => o.type === IoTDeviceType.SigFox)
8183
@ValidateNested({ each: true })
8284
@Type(() => CreateSigFoxSettingsDto)
8385
sigfoxSettings?: CreateSigFoxSettingsDto;
86+
87+
@ApiProperty({ required: false })
88+
@ValidateIf(o => o.type === IoTDeviceType.MQTTInternalBroker)
89+
@ValidateNested({ each: true })
90+
@Type(() => CreateMqttInternalBrokerSettingsDto)
91+
mqttInternalBrokerSettings?: CreateMqttInternalBrokerSettingsDto;
92+
93+
@ApiProperty({ required: false })
94+
@ValidateIf(o => o.type === IoTDeviceType.MQTTExternalBroker)
95+
@ValidateNested({ each: true })
96+
@Type(() => CreateMqttExternalBrokerSettingsDto)
97+
mqttExternalBrokerSettings?: CreateMqttExternalBrokerSettingsDto;
8498
}

src/entities/dto/create-lorawan-settings.dto.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { ApiProperty, PickType } from "@nestjs/swagger";
2-
import { IsHexadecimal, IsIn, IsInt, IsNumber, IsOptional, IsString, Length, Matches, Min, ValidateIf } from "class-validator";
2+
import {
3+
IsHexadecimal,
4+
IsInt,
5+
IsNumber,
6+
IsString,
7+
Length,
8+
Min,
9+
ValidateIf,
10+
} from "class-validator";
311

412
import { ActivationType } from "@enum/lorawan-activation-type.enum";
513

@@ -37,7 +45,7 @@ export class CreateLoRaWANSettingsDto extends PickType(ChirpstackDeviceContentsD
3745
@ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP)
3846
@IsNumber()
3947
@IsInt()
40-
@Min(0)
48+
@Min(0)
4149
fCntUp?: number;
4250

4351
@ApiProperty({ required: false })

0 commit comments

Comments
 (0)