diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index aef01c92..35832b5d 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -126,7 +126,7 @@ export class ChirpstackGatewayController { @Put("updateGatewayOrganization/:id") @ApiProduces("application/json") - @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) + @ApiOperation({ summary: "Update gateway organization" }) @ApiBadRequestResponse() @GatewayAdmin() async changeOrganization( diff --git a/src/controllers/admin-controller/data-target-log.controller.ts b/src/controllers/admin-controller/data-target-log.controller.ts index 506eaf6a..1f602ff5 100644 --- a/src/controllers/admin-controller/data-target-log.controller.ts +++ b/src/controllers/admin-controller/data-target-log.controller.ts @@ -2,10 +2,13 @@ import { ComposeAuthGuard } from "@auth/compose-auth.guard"; import { Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ApiAuth } from "@auth/swagger-auth-decorator"; +import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { DatatargetLog } from "@entities/datatarget-log.entity"; -import { Controller, Get, Param, ParseIntPipe, UseGuards } from "@nestjs/common"; +import { ApplicationAccessScope, checkIfUserHasAccessToApplication } from "@helpers/security-helper"; +import { Controller, Get, Param, ParseIntPipe, Req, UseGuards } from "@nestjs/common"; import { ApiForbiddenResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; import { InjectRepository } from "@nestjs/typeorm"; +import { DataTargetService } from "@services/data-targets/data-target.service"; import { Repository } from "typeorm"; @ApiTags("Data Target Logs") @@ -18,11 +21,18 @@ import { Repository } from "typeorm"; export class DatatargetLogController { constructor( @InjectRepository(DatatargetLog) - private datatargetLogRepository: Repository + private datatargetLogRepository: Repository, + private dataTargetService: DataTargetService ) {} @Get(":datatargetId") - async getDatatargetLogs(@Param("datatargetId", new ParseIntPipe()) datatargetId: number): Promise { + async getDatatargetLogs( + @Req() req: AuthenticatedRequest, + @Param("datatargetId", new ParseIntPipe()) datatargetId: number + ): Promise { + const dataTarget = await this.dataTargetService.findOne(datatargetId); + checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); + return await this.datatargetLogRepository.find({ where: { datatarget: { id: datatargetId }, diff --git a/src/controllers/admin-controller/data-target.controller.ts b/src/controllers/admin-controller/data-target.controller.ts index fff560af..c560f80e 100644 --- a/src/controllers/admin-controller/data-target.controller.ts +++ b/src/controllers/admin-controller/data-target.controller.ts @@ -75,12 +75,18 @@ export class DataTargetController { @Get(":id") @ApiOperation({ summary: "Find DataTarget by id" }) async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + let dataTarget; + try { + dataTarget = await this.dataTargetService.findOneWithHasRecentError(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + try { - const dataTarget = await this.dataTargetService.findOneWithHasRecentError(id); checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); return dataTarget; } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); + throw err; } } @@ -197,8 +203,13 @@ export class DataTargetController { @Post("testDataTarget") @ApiOperation({ summary: "Send a ping or test data packet to a data target" }) - async testDataTarget(@Body() testDto: TestDataTargetDto): Promise { + async testDataTarget( + @Req() req: AuthenticatedRequest, + @Body() testDto: TestDataTargetDto + ): Promise { + const dataTarget = await this.dataTargetService.findOne(testDto.dataTargetId); + checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); // Send package - return await this.dataTargetService.testDataTarget(testDto); + return await this.dataTargetService.testDataTarget(testDto, dataTarget); } } diff --git a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts index 29eeedde..ec7c734e 100644 --- a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts +++ b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts @@ -56,38 +56,6 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { private iotDeviceService: IoTDeviceService ) {} - @Get() - @ApiProduces("application/json") - @ApiOperation({ - summary: "Find all connections between IoT-Devices, PayloadDecoders and DataTargets (paginated)", - }) - @ApiResponse({ - status: 200, - description: "Success", - type: ListAllApplicationsResponseDto, - }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAndCountWithPagination(query); - } else { - const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.service.findAndCountWithPagination(query, allowed); - } - } - - @Get(":id") - @ApiNotFoundResponse({ - description: "If the id of the entity doesn't exist", - }) - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - return await this.service.findOne(id); - } @Get("byIoTDevice/:id") @ApiOperation({ @@ -104,24 +72,6 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } } - @Get("byPayloadDecoder/:id") - @ApiOperation({ - summary: "Find all connections by PayloadDecoder id", - }) - async findByPayloadDecoderId( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAllByPayloadDecoderId(id); - } else { - return await this.service.findAllByPayloadDecoderId( - id, - req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead() - ); - } - } - @Get("byDataTarget/:id") @ApiOperation({ summary: "Find all connections by DataTarget id", @@ -196,7 +146,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } private async checkIfUpdateIsAllowed(updateDto: UpdateConnectionDto, req: AuthenticatedRequest, id: number) { - const newIotDevice = await this.iotDeviceService.findOne(updateDto.iotDeviceIds[0]); + const newIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(updateDto.iotDeviceIds[0]); checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write); const oldConnection = await this.service.findOne(id); await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req); diff --git a/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts b/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts index 8761e50f..bf4edc82 100644 --- a/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts +++ b/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts @@ -31,7 +31,29 @@ export class IoTDevicePayloadDecoderController { @Query() query: PayloadDecoderIoDeviceMinimalQuery ): Promise { try { - return await this.iotDeviceService.findAllByPayloadDecoder(req, payloadDecoderId, +query.limit, +query.offset); + const iotDevices = await this.iotDeviceService.findAllByPayloadDecoder( + req, + payloadDecoderId, + +query.limit, + +query.offset + ); + + if (req.user.permissions.isGlobalAdmin) { + return iotDevices; + } + + const allowedAppIds = req.user.permissions.getAllApplicationsWithAtLeastRead(); + + const filteredIotDevices = iotDevices.data.filter(device => + allowedAppIds.find(appId => appId === device.applicationId) + ); + + const response: ListAllIoTDevicesMinimalResponseDto = { + data: filteredIotDevices, + count: filteredIotDevices.length, + }; + + return response; } catch (err) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index f3144859..3bfc8a28 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -433,6 +433,7 @@ export class IoTDeviceController { return new StreamableFile(csvFile); } catch (err) { this.logger.error(err); + throw err; } } } diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts index f225f51f..c67b31a5 100644 --- a/src/controllers/user-management/new-kombit-creation.controller.ts +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -19,7 +19,6 @@ import { Param, ParseIntPipe, Put, - Query, Req, UseGuards, } from "@nestjs/common"; @@ -35,6 +34,7 @@ import { OrganizationService } from "@services/user-management/organization.serv import { PermissionService } from "@services/user-management/permission.service"; import { UserService } from "@services/user-management/user.service"; import { ApiAuth } from "@auth/swagger-auth-decorator"; +import { checkIfUserHasAccessToUser } from "@helpers/security-helper"; @UseGuards(JwtAuthGuard) @ApiAuth() @@ -133,19 +133,28 @@ export class NewKombitCreationController { @Get(":id") @ApiOperation({ summary: "Get one user" }) - async find( - @Param("id", new ParseIntPipe()) id: number, - @Query("extendedInfo") extendedInfo?: boolean - ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + async find(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + let dbUser; + + try { + dbUser = await this.userService.findOne(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + try { + checkIfUserHasAccessToUser(req, dbUser); + + dbUser.permissions.forEach(perm => { + delete perm.organization; + }); + // Don't leak the passwordHash - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.findOne(id, getExtendedInfo); + const { passwordHash: _, ...user } = dbUser; return user; } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); + throw err; } } } diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index cba6b4eb..03d16f9f 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -89,15 +89,6 @@ export class OrganizationController { } } - @Get("minimal") - @ApiOperation({ - summary: "Get list of the minimal representation of organizations, i.e. id and name.", - }) - @Read() - async findAllMinimal(): Promise { - return await this.organizationService.findAllMinimal(); - } - @Get() @ApiOperation({ summary: "Get list of all Organizations" }) @UserAdmin() diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 30b22f1d..c75fcb71 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -28,6 +28,7 @@ import { UserResponseDto } from "@dto/user-response.dto"; import { ErrorCodes } from "@entities/enum/error-codes.enum"; import { checkIfUserHasAccessToOrganization, + checkIfUserHasAccessToUser, checkIfUserIsGlobalAdmin, OrganizationAccessScope, } from "@helpers/security-helper"; @@ -54,12 +55,6 @@ export class UserController { constructor(private userService: UserService, private organizationService: OrganizationService) {} - @Get("minimal") - @ApiOperation({ summary: "Get all id,names of users" }) - async findAllMinimal(): Promise { - return await this.userService.findAllMinimal(); - } - @Post() @ApiOperation({ summary: "Create a new User" }) async create(@Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto): Promise { @@ -189,18 +184,28 @@ export class UserController { @Get(":id") @ApiOperation({ summary: "Get one user" }) - async find( - @Param("id", new ParseIntPipe()) id: number, - @Query("extendedInfo") extendedInfo?: boolean - ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + async find(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + let dbUser; + + try { + dbUser = await this.userService.findOne(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + try { + checkIfUserHasAccessToUser(req, dbUser); + + dbUser.permissions.forEach(perm => { + delete perm.organization; + }); + // Don't leak the passwordHash - const { passwordHash: _, ...user } = await this.userService.findOne(id, getExtendedInfo); + const { passwordHash: _, ...user } = dbUser; return user; } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); + throw err; } } @@ -213,13 +218,13 @@ export class UserController { @Param("organizationId", new ParseIntPipe()) organizationId: number, @Query() query?: ListAllEntitiesDto ): Promise { - try { - // Check if user has access to organization - if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { - throw new ForbiddenException("User does not have org admin permissions for this organization"); - } + // Check if user has access to organization + if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { + throw new ForbiddenException("User does not have org admin permissions for this organization"); + } - // Get user objects + // Get user objects + try { return await this.userService.getUsersOnOrganization(organizationId, query); } catch (err) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); diff --git a/src/entities/dto/create-open-data-dk-dataset.dto.ts b/src/entities/dto/create-open-data-dk-dataset.dto.ts index 4efa10c8..5a1ffb9e 100644 --- a/src/entities/dto/create-open-data-dk-dataset.dto.ts +++ b/src/entities/dto/create-open-data-dk-dataset.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator"; +import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator"; export class CreateOpenDataDkDatasetDto { @ApiProperty({ required: true }) @@ -17,6 +17,11 @@ export class CreateOpenDataDkDatasetDto { @IsString({ each: true, always: true }) keywords?: string[]; + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + keywordTags: string; + @ApiProperty({ required: true }) @IsString() @IsUrl({ protocols: ["http", "https"] }) @@ -36,4 +41,18 @@ export class CreateOpenDataDkDatasetDto { @IsString() @IsNotEmpty() resourceTitle: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + updateFrequency: string = "UNKNOWN"; + + @ApiProperty({ required: false }) + @IsOptional() + @IsUrl() + documentationUrl: string; + + @ApiProperty({ required: true }) + @IsBoolean() + dataDirectory: boolean; } diff --git a/src/entities/dto/open-data-dk-dcat.dto.ts b/src/entities/dto/open-data-dk-dcat.dto.ts index c5aad646..5fe15a0c 100644 --- a/src/entities/dto/open-data-dk-dcat.dto.ts +++ b/src/entities/dto/open-data-dk-dcat.dto.ts @@ -37,6 +37,9 @@ export class Dataset { distribution: Distribution[]; spatial: string; theme: string[]; + documentation: string; + frequency: string | undefined; + dataDirectory: boolean; } export class DCATRootObject { diff --git a/src/entities/open-data-dk-dataset.entity.ts b/src/entities/open-data-dk-dataset.entity.ts index 399c7120..a9d9bad9 100644 --- a/src/entities/open-data-dk-dataset.entity.ts +++ b/src/entities/open-data-dk-dataset.entity.ts @@ -18,6 +18,9 @@ export class OpenDataDkDataset extends DbBaseEntity { @Column("text", { array: true, nullable: true }) keywords?: string[]; + @Column({ nullable: true }) + keywordTags?: string; + @Column() license: string; @@ -29,4 +32,13 @@ export class OpenDataDkDataset extends DbBaseEntity { @Column({ nullable: false, default: "" }) resourceTitle: string; + + @Column({ nullable: true }) + updateFrequency?: string; + + @Column({ nullable: true }) + documentationUrl?: string; + + @Column({ nullable: false, default: false }) + dataDirectory: boolean; } diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index 6ba3d16b..e0e98f62 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -4,6 +4,7 @@ import { PermissionType } from "@enum/permission-type.enum"; import { ForbiddenException, BadRequestException } from "@nestjs/common"; import * as _ from "lodash"; import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; +import { User } from "@entities/user.entity"; export enum OrganizationAccessScope { ApplicationRead, @@ -75,6 +76,16 @@ export function checkIfUserHasAccessToApplication( checkIfGlobalAdminOrInList(req, allowedOrganizations, applicationId); } +export function checkIfUserHasAccessToUser(req: AuthenticatedRequest, user: User) { + const orgs = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); + + const hasAccess = user.permissions.some(perm => orgs.includes(perm.organization?.id)); + + if (!hasAccess && !req.user.permissions.isGlobalAdmin) { + throw new ForbiddenException(); + } +} + export function checkIfUserIsGlobalAdmin(req: AuthenticatedRequest): void { if (!req.user.permissions.isGlobalAdmin) { throw new ForbiddenException(); diff --git a/src/migration/1762524771197-added-new-fields-to-oddk-datatarget.ts b/src/migration/1762524771197-added-new-fields-to-oddk-datatarget.ts new file mode 100644 index 00000000..14494751 --- /dev/null +++ b/src/migration/1762524771197-added-new-fields-to-oddk-datatarget.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddedNewFieldsToOddkDatatarget1762524771197 implements MigrationInterface { + name = 'AddedNewFieldsToOddkDatatarget1762524771197' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD "keywordTags" character varying`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD "updateFrequency" character varying`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD "documentationUrl" character varying`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD "dataDirectory" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP COLUMN "dataDirectory"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP COLUMN "documentationUrl"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP COLUMN "updateFrequency"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP COLUMN "keywordTags"`); + } + +} diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index 0401c72f..39efbe17 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -1,7 +1,7 @@ import { - Gateway as ChirpstackGateway, CreateGatewayRequest, DeleteGatewayRequest, + Gateway as ChirpstackGateway, GetGatewayMetricsRequest, GetGatewayMetricsResponse, GetGatewayRequest, @@ -53,6 +53,12 @@ import { Repository } from "typeorm"; @Injectable() export class ChirpstackGatewayService extends GenericChirpstackConfigurationService { + GATEWAY_STATS_INTERVAL_IN_DAYS = 29; + GATEWAY_LAST_ACTIVE_SINCE_IN_MINUTES = 3; + private readonly logger = new Logger(ChirpstackGatewayService.name, { + timestamp: true, + }); + constructor( @InjectRepository(DbGateway) private gatewayRepository: Repository, @@ -62,11 +68,6 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ ) { super(); } - GATEWAY_STATS_INTERVAL_IN_DAYS = 29; - GATEWAY_LAST_ACTIVE_SINCE_IN_MINUTES = 3; - private readonly logger = new Logger(ChirpstackGatewayService.name, { - timestamp: true, - }); async createNewGateway(dto: CreateGatewayDto, userId: number): Promise { dto.gateway = await this.updateDtoContents(dto.gateway); @@ -87,7 +88,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const gatewayChirpstack = await this.mapToChirpstackGateway(dto, chirpstackLocation); Object.entries(dto.gateway.tags).forEach(([key, value]) => { - gatewayChirpstack.getTagsMap().set(key, value); + gatewayChirpstack.getTagsMap().set(key, value.toString()); }); req.setGateway(gatewayChirpstack); @@ -302,54 +303,6 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } - //TODO: This could be moved to a helper function in the future, since it has a lot of similarities with metrics from chirpstack devices. - private mapPackets(metrics: GetGatewayMetricsResponse) { - const gatewayResponseDto: GatewayStatsElementDto[] = []; - const packetCounts: { [timestamp: string]: { rx: number; tx: number } } = {}; - - const rxTimestamps = metrics.getRxPackets().getTimestampsList(); - const rxPackets = metrics - .getRxPackets() - .getDatasetsList() - .find(e => e.getLabel() === "rx_count") - .getDataList(); - - this.processPackets(rxTimestamps, rxPackets, "rx", packetCounts); - - const txTimestamps = metrics.getTxPackets().getTimestampsList(); - const txPackets = metrics - .getTxPackets() - .getDatasetsList() - .find(e => e.getLabel() === "tx_count") - .getDataList(); - - this.processPackets(txTimestamps, txPackets, "tx", packetCounts); - - Object.keys(packetCounts).forEach(timestamp => { - const packetCount = packetCounts[timestamp]; - const dto: GatewayStatsElementDto = { - timestamp, - rxPacketsReceived: packetCount.rx, - txPacketsEmitted: packetCount.tx, - }; - gatewayResponseDto.push(dto); - }); - return gatewayResponseDto; - } - - private processPackets( - timestamps: Array, - packets: number[], - key: string, - packetCounts: { [timestamp: string]: { rx: number; tx: number } } - ) { - timestamps.forEach((timestamp, index) => { - const isoTimestamp = timestamp.toDate().toISOString(); - packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { rx: 0, tx: 0 }; - (packetCounts[isoTimestamp] as any)[key] = packets[index]; - }); - } - async modifyGateway( gatewayId: string, dto: UpdateGatewayDto, @@ -370,7 +323,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const gatewayCs = await this.mapToChirpstackGateway(dto, location, gatewayId); Object.entries(dto.gateway.tags).forEach(([key, value]) => { - gatewayCs.getTagsMap().set(key, value); + gatewayCs.getTagsMap().set(key, value.toString()); }); request.setGateway(gatewayCs); @@ -447,20 +400,6 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } - private async updateDtoContents( - contentsDto: GatewayContentsDto | UpdateGatewayContentsDto - ): Promise { - if (contentsDto?.tagsString) { - contentsDto.tags = JSON.parse(contentsDto.tagsString); - } else { - contentsDto.tags = {}; - } - - contentsDto.id = contentsDto.gatewayId; - - return contentsDto; - } - public mapContentsDtoToGateway(dto: GatewayContentsDto) { const gateway = new DbGateway(); gateway.name = dto.name; @@ -536,24 +475,6 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ return gateway; } - private mapGatewayToResponseDto(gateway: DbGateway, forMap = false): GatewayResponseDto { - const responseDto = gateway as unknown as GatewayResponseDto; - responseDto.organizationId = gateway.organization.id; - responseDto.organizationName = gateway.organization.name; - - const commonLocation = new CommonLocationDto(); - commonLocation.latitude = gateway.location.coordinates[1]; - commonLocation.longitude = gateway.location.coordinates[0]; - - if (!forMap) { - commonLocation.altitude = gateway.altitude; - responseDto.tags = JSON.parse(gateway.tags); - } - - responseDto.location = commonLocation; - - return responseDto; - } async getAllGatewaysFromChirpstack(): Promise { const limit = 1000; @@ -587,24 +508,6 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ return responseList; } - private getSortingForGateways(query: ListAllEntitiesDto) { - let orderBy = "gateway.id"; - - if (!query.orderOn) { - return orderBy; - } - - if (query.orderOn === "organizationName") { - orderBy = "organization.name"; - } else if (query.orderOn === "status") { - orderBy = "gateway.lastSeenAt"; - } else { - orderBy = `gateway.${query.orderOn}`; - } - - return orderBy; - } - validatePackageAlarmInput(dto: UpdateGatewayDto) { if (dto.gateway.minimumPackages > dto.gateway.maximumPackages) { throw new BadRequestException({ @@ -628,6 +531,105 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ } } + //TODO: This could be moved to a helper function in the future, since it has a lot of similarities with metrics from chirpstack devices. + private mapPackets(metrics: GetGatewayMetricsResponse) { + const gatewayResponseDto: GatewayStatsElementDto[] = []; + const packetCounts: { [timestamp: string]: { rx: number; tx: number } } = {}; + + const rxTimestamps = metrics.getRxPackets().getTimestampsList(); + const rxPackets = metrics + .getRxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "rx_count") + .getDataList(); + + this.processPackets(rxTimestamps, rxPackets, "rx", packetCounts); + + const txTimestamps = metrics.getTxPackets().getTimestampsList(); + const txPackets = metrics + .getTxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "tx_count") + .getDataList(); + + this.processPackets(txTimestamps, txPackets, "tx", packetCounts); + + Object.keys(packetCounts).forEach(timestamp => { + const packetCount = packetCounts[timestamp]; + const dto: GatewayStatsElementDto = { + timestamp, + rxPacketsReceived: packetCount.rx, + txPacketsEmitted: packetCount.tx, + }; + gatewayResponseDto.push(dto); + }); + return gatewayResponseDto; + } + + private processPackets( + timestamps: Array, + packets: number[], + key: string, + packetCounts: { [timestamp: string]: { rx: number; tx: number } } + ) { + timestamps.forEach((timestamp, index) => { + const isoTimestamp = timestamp.toDate().toISOString(); + packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { rx: 0, tx: 0 }; + (packetCounts[isoTimestamp] as any)[key] = packets[index]; + }); + } + + private async updateDtoContents( + contentsDto: GatewayContentsDto | UpdateGatewayContentsDto + ): Promise { + if (contentsDto?.tagsString) { + contentsDto.tags = JSON.parse(contentsDto.tagsString); + } else { + contentsDto.tags = {}; + } + + contentsDto.id = contentsDto.gatewayId; + + return contentsDto; + } + + private mapGatewayToResponseDto(gateway: DbGateway, forMap = false): GatewayResponseDto { + const responseDto = gateway as unknown as GatewayResponseDto; + responseDto.organizationId = gateway.organization.id; + responseDto.organizationName = gateway.organization.name; + + const commonLocation = new CommonLocationDto(); + commonLocation.latitude = gateway.location.coordinates[1]; + commonLocation.longitude = gateway.location.coordinates[0]; + + if (!forMap) { + commonLocation.altitude = gateway.altitude; + responseDto.tags = JSON.parse(gateway.tags); + } + + responseDto.location = commonLocation; + + return responseDto; + } + + private getSortingForGateways(query: ListAllEntitiesDto) { + let orderBy = "gateway.id"; + + if (!query.orderOn) { + return orderBy; + } + + if (query.orderOn === "organizationName") { + orderBy = "organization.name"; + } else if (query.orderOn === "status") { + orderBy = "gateway.lastSeenAt"; + } else { + orderBy = `gateway.${query.orderOn}`; + } + + return orderBy; + } + private async checkForNotificationUnusualPackagesAlarms(gateway: GatewayResponseDto) { if (!gateway.lastSeenAt) { return; diff --git a/src/services/chirpstack/device-profile.service.ts b/src/services/chirpstack/device-profile.service.ts index 5696a59a..fa43d4ba 100644 --- a/src/services/chirpstack/device-profile.service.ts +++ b/src/services/chirpstack/device-profile.service.ts @@ -52,7 +52,7 @@ export class DeviceProfileService extends GenericChirpstackConfigurationService const deviceProfile = this.mapToChirpstackDto(dto, true); Object.entries(dto.deviceProfile.tags).forEach(([key, value]) => { - deviceProfile.getTagsMap().set(key, value); + deviceProfile.getTagsMap().set(key, value.toString()); }); req.setDeviceProfile(deviceProfile); diff --git a/src/services/data-management/open-data-dk-sharing.service.ts b/src/services/data-management/open-data-dk-sharing.service.ts index aeea7864..ebea09a6 100644 --- a/src/services/data-management/open-data-dk-sharing.service.ts +++ b/src/services/data-management/open-data-dk-sharing.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; -import { DCATRootObject, Dataset, ContactPoint, Distribution } from "@dto/open-data-dk-dcat.dto"; +import { ContactPoint, Dataset, DCATRootObject, Distribution } from "@dto/open-data-dk-dcat.dto"; import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { Organization } from "@entities/organization.entity"; import { PayloadDecoderExecutorService } from "./payload-decoder-executor.service"; @@ -17,6 +17,9 @@ import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device. @Injectable() export class OpenDataDkSharingService { + private readonly BACKEND_BASE_URL = configuration()["backend"]["baseurl"]; + private readonly logger = new Logger(OpenDataDkSharingService.name); + constructor( @InjectRepository(OpenDataDkDataset) private repository: Repository, @@ -24,9 +27,6 @@ export class OpenDataDkSharingService { private chirpstackDeviceService: ChirpstackDeviceService ) {} - private readonly BACKEND_BASE_URL = configuration()["backend"]["baseurl"]; - private readonly logger = new Logger(OpenDataDkSharingService.name); - async getDecodedDataInDataset(dataset: OpenDataDkDataset): Promise { const rawData = await this.repository .createQueryBuilder("dataset") @@ -46,6 +46,25 @@ export class OpenDataDkSharingService { return await this.decodeData(rawData); } + async createDCAT(organization: Organization): Promise { + const datasets = await this.getAllOpenDataDkSharesForOrganization(organization); + + return this.mapToDCAT(organization, datasets); + } + + async findById(shareId: number, organizationId: number): Promise { + return await this.findDatasetWithRelations() + .where("dataset.id = :datasetId and org.id = :organizationId", { + datasetId: shareId, + organizationId: organizationId, + }) + .getOne(); + } + + async getAllOpenDataDkSharesForOrganization(organization: Organization): Promise { + return this.findDatasetWithRelations().where("org.id = :orgId", { orgId: organization.id }).getMany(); + } + private async decodeData(rawData: OpenDataDkDataset) { const results: any[] = []; for (const connection of rawData.dataTarget.connections) { @@ -96,25 +115,6 @@ export class OpenDataDkSharingService { } } - async createDCAT(organization: Organization): Promise { - const datasets = await this.getAllOpenDataDkSharesForOrganization(organization); - - return this.mapToDCAT(organization, datasets); - } - - async findById(shareId: number, organizationId: number): Promise { - return await this.findDatasetWithRelations() - .where("dataset.id = :datasetId and org.id = :organizationId", { - datasetId: shareId, - organizationId: organizationId, - }) - .getOne(); - } - - async getAllOpenDataDkSharesForOrganization(organization: Organization): Promise { - return this.findDatasetWithRelations().where("org.id = :orgId", { orgId: organization.id }).getMany(); - } - private findDatasetWithRelations() { return this.repository .createQueryBuilder("dataset") @@ -146,7 +146,8 @@ export class OpenDataDkSharingService { ds.landingPage = undefined; ds.title = dataset.name; ds.description = dataset.description; - ds.keyword = dataset.keywords != null ? dataset.keywords : []; + ds.theme = dataset.keywords != null ? dataset.keywords : []; + ds.keyword = dataset.keywordTags != null ? dataset.keywordTags.split(",") : []; ds.issued = dataset.createdAt; ds.modified = dataset.updatedAt; ds.publisher = { @@ -156,6 +157,9 @@ export class OpenDataDkSharingService { ds.contactPoint["@type"] = "vcard:Contact"; ds.contactPoint.fn = dataset.authorName; ds.contactPoint.hasEmail = `mailto:${dataset.authorEmail}`; + ds.documentation = dataset.documentationUrl; + ds.frequency = dataset.updateFrequency; + ds.dataDirectory = dataset.dataDirectory; ds.distribution = [this.mapDistribution(organization, dataset)]; diff --git a/src/services/data-targets/data-target.service.ts b/src/services/data-targets/data-target.service.ts index 92c61d62..b9c52ede 100644 --- a/src/services/data-targets/data-target.service.ts +++ b/src/services/data-targets/data-target.service.ts @@ -223,8 +223,7 @@ export class DataTargetService { ); } - public async testDataTarget(testDto: TestDataTargetDto): Promise { - const dataTarget = await this.findOne(testDto.dataTargetId); + public async testDataTarget(testDto: TestDataTargetDto, dataTarget: DataTarget): Promise { let iotDevice = await this.iotDeviceService.findOne(testDto.iotDeviceId); if (dataTarget.type === DataTargetType.MQTT && !testDto.dataPackage) { @@ -308,7 +307,12 @@ export class DataTargetService { o.description = dto.description; o.keywords = dto.keywords; + o.keywordTags = dto.keywordTags; o.resourceTitle = dto.resourceTitle; + + o.updateFrequency = dto.updateFrequency; + o.documentationUrl = dto.documentationUrl; + o.dataDirectory = dto.dataDirectory; return o; } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index c0026638..70c133d4 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -73,14 +73,8 @@ export class UserService { }); } - async findOne(id: number, getExtendedInformation: boolean = false): Promise { - const relations = ["permissions", "requestedOrganizations"]; - const extendedBoolean = this.parseBoolean(getExtendedInformation); - if (extendedBoolean) { - relations.push("permissions.organization"); - relations.push("permissions.users"); - relations.push("permissions.type"); - } + async findOne(id: number): Promise { + const relations = ["permissions", "requestedOrganizations", "permissions.organization"]; return await this.userRepository.findOne({ where: { id },