From 2151b8502d34f1e7693be1d58fdef1c136414684 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 18:48:05 +0100 Subject: [PATCH 01/47] Added Scan permission target ref #67 --- src/models/enums/PermissionTargets.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index 728cf2a..de69ff0 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -10,5 +10,6 @@ export enum PermissionTarget { USERGROUP = 'USERGROUP', PERMISSION = 'PERMISSION', STATSCLIENT = 'STATSCLIENT', - DONOR = 'DONOR' + DONOR = 'DONOR', + SCAN = 'SCAN' } \ No newline at end of file From ee2433a5ae2d3797477e46bc63ddfb362a0ece24 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 18:49:33 +0100 Subject: [PATCH 02/47] Added barebones scans controller ref #67 --- src/controllers/ScanController.ts | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/controllers/ScanController.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts new file mode 100644 index 0000000..a9cea38 --- /dev/null +++ b/src/controllers/ScanController.ts @@ -0,0 +1,101 @@ +import { Authorized, Get, JsonController } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { Scan } from '../models/entities/Scan'; +import { ResponseDonor } from '../models/responses/ResponseDonor'; + +@JsonController('/donors') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class ScanController { + private scanRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.scanRepository = getConnectionManager().get().getRepository(Scan); + } + + @Get() + @Authorized("SCAN:GET") + @ResponseSchema(ResponseDonor, { isArray: true }) + @OpenAPI({ description: 'Lists all runners from all teams/orgs.
This includes the runner\'s group and distance ran.' }) + async getAll() { + let responseScans: ResponseScan[] = new Array(); + const scans = await this.scanRepository.find(); + scans.forEach(scan => { + responseScans.push(new ResponseScan(scan)); + }); + return responseScans; + } + + // @Get('/:id') + // @Authorized("DONOR:GET") + // @ResponseSchema(ResponseDonor) + // @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + // @OnUndefined(DonorNotFoundError) + // @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + // async getOne(@Param('id') id: number) { + // let donor = await this.donorRepository.findOne({ id: id }) + // if (!donor) { throw new DonorNotFoundError(); } + // return new ResponseDonor(donor); + // } + + // @Post() + // @Authorized("DONOR:CREATE") + // @ResponseSchema(ResponseDonor) + // @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + // async post(@Body({ validate: true }) createRunner: CreateDonor) { + // let donor; + // try { + // donor = await createRunner.toDonor(); + // } catch (error) { + // throw error; + // } + + // donor = await this.donorRepository.save(donor) + // return new ResponseDonor(await this.donorRepository.findOne(donor)); + // } + + // @Put('/:id') + // @Authorized("DONOR:UPDATE") + // @ResponseSchema(ResponseDonor) + // @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) + // @ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 }) + // @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) + // async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) { + // let oldDonor = await this.donorRepository.findOne({ id: id }); + + // if (!oldDonor) { + // throw new DonorNotFoundError(); + // } + + // if (oldDonor.id != donor.id) { + // throw new DonorIdsNotMatchingError(); + // } + + // await this.donorRepository.save(await donor.updateDonor(oldDonor)); + // return new ResponseDonor(await this.donorRepository.findOne({ id: id })); + // } + + // @Delete('/:id') + // @Authorized("DONOR:DELETE") + // @ResponseSchema(ResponseDonor) + // @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + // @OnUndefined(204) + // @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + // async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + // let donor = await this.donorRepository.findOne({ id: id }); + // if (!donor) { return null; } + // const responseDonor = await this.donorRepository.findOne(donor); + + // if (!donor) { + // throw new DonorNotFoundError(); + // } + + // //TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66) + + // await this.donorRepository.delete(donor); + // return new ResponseDonor(responseDonor); + // } +} From a4b0dfe43ed6707cc682ac100941930c80738ea9 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 19:02:06 +0100 Subject: [PATCH 03/47] Defined responses for scans and trackscans ref #67 --- src/models/responses/ResponseScan.ts | 46 ++++++++++++++++++++++ src/models/responses/ResponseTrackScan.ts | 48 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/models/responses/ResponseScan.ts create mode 100644 src/models/responses/ResponseTrackScan.ts diff --git a/src/models/responses/ResponseScan.ts b/src/models/responses/ResponseScan.ts new file mode 100644 index 0000000..b5883bd --- /dev/null +++ b/src/models/responses/ResponseScan.ts @@ -0,0 +1,46 @@ +import { IsBoolean, IsInt, IsNotEmpty, IsPositive } from "class-validator"; +import { Scan } from '../entities/Scan'; +import { ResponseRunner } from './ResponseRunner'; + +/** + * Defines the scan response. +*/ +export class ResponseScan { + /** + * The scans's id. + */ + @IsInt() + id: number;; + + /** + * The scan's associated runner. + * This is important to link ran distances to runners. + */ + @IsNotEmpty() + runner: ResponseRunner; + + /** + * Is the scan valid (for fraud reasons). + * The determination of validity will work differently for every child class. + */ + @IsBoolean() + valid: boolean = true; + + /** + * The scans's length/distance in meters. + */ + @IsInt() + @IsPositive() + distance: number; + + /** + * Creates a ResponseScan object from a scan. + * @param scan The scan the response shall be build for. + */ + public constructor(scan: Scan) { + this.id = scan.id; + this.runner = new ResponseRunner(scan.runner); + this.distance = scan.distance; + this.valid = scan.valid; + } +} diff --git a/src/models/responses/ResponseTrackScan.ts b/src/models/responses/ResponseTrackScan.ts new file mode 100644 index 0000000..724a8fd --- /dev/null +++ b/src/models/responses/ResponseTrackScan.ts @@ -0,0 +1,48 @@ +import { IsDateString, IsNotEmpty } from "class-validator"; +import { RunnerCard } from '../entities/RunnerCard'; +import { ScanStation } from '../entities/ScanStation'; +import { TrackScan } from '../entities/TrackScan'; +import { ResponseScan } from './ResponseScan'; +import { ResponseTrack } from './ResponseTrack'; + +/** + * Defines the trackScan response. +*/ +export class ResponseTrackScan extends ResponseScan { + /** + * The scan's associated track. + */ + @IsNotEmpty() + track: ResponseTrack; + + /** + * The runnerCard associated with the scan. + */ + @IsNotEmpty() + card: RunnerCard; + + /** + * The scanning station that created the scan. + */ + @IsNotEmpty() + station: ScanStation; + + /** + * The scan's creation timestamp. + */ + @IsDateString() + @IsNotEmpty() + timestamp: string; + + /** + * Creates a ResponseTrackScan object from a scan. + * @param scan The trackSscan the response shall be build for. + */ + public constructor(scan: TrackScan) { + super(scan); + this.track = new ResponseTrack(scan.track); + this.card = scan.card; + this.station = scan.station; + this.timestamp = scan.timestamp; + } +} From 58156e0d616dab65ad3366a383426945fd207250 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 19:09:06 +0100 Subject: [PATCH 04/47] Implemented the first route of the toResponse normalisationf for all classes ref #67 --- src/models/entities/RunnerOrganisation.ts | 8 ++++++++ src/models/entities/RunnerTeam.ts | 8 ++++++++ src/models/entities/Scan.ts | 8 ++++++++ src/models/entities/ScanStation.ts | 7 +++++++ src/models/entities/StatsClient.ts | 8 ++++++++ src/models/entities/Track.ts | 8 ++++++++ src/models/entities/TrackScan.ts | 8 ++++++++ src/models/entities/UserAction.ts | 7 +++++++ 8 files changed, 62 insertions(+) diff --git a/src/models/entities/RunnerOrganisation.ts b/src/models/entities/RunnerOrganisation.ts index f0f8c78..ebae8fd 100644 --- a/src/models/entities/RunnerOrganisation.ts +++ b/src/models/entities/RunnerOrganisation.ts @@ -1,5 +1,6 @@ import { IsInt, IsOptional } from "class-validator"; import { ChildEntity, ManyToOne, OneToMany } from "typeorm"; +import { ResponseRunnerOrganisation } from '../responses/ResponseRunnerOrganisation'; import { Address } from './Address'; import { IAddressUser } from './IAddressUser'; import { Runner } from './Runner'; @@ -54,4 +55,11 @@ export class RunnerOrganisation extends RunnerGroup implements IAddressUser { public get distanceDonationAmount(): number { return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0); } + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseRunnerOrganisation { + return new ResponseRunnerOrganisation(this); + } } \ No newline at end of file diff --git a/src/models/entities/RunnerTeam.ts b/src/models/entities/RunnerTeam.ts index e84ee9b..ba4884d 100644 --- a/src/models/entities/RunnerTeam.ts +++ b/src/models/entities/RunnerTeam.ts @@ -1,5 +1,6 @@ import { IsNotEmpty } from "class-validator"; import { ChildEntity, ManyToOne } from "typeorm"; +import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam'; import { RunnerGroup } from "./RunnerGroup"; import { RunnerOrganisation } from "./RunnerOrganisation"; @@ -17,4 +18,11 @@ export class RunnerTeam extends RunnerGroup { @IsNotEmpty() @ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true }) parentGroup?: RunnerOrganisation; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseRunnerTeam { + return new ResponseRunnerTeam(this); + } } \ No newline at end of file diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index 1b66851..3c1cb67 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -6,6 +6,7 @@ import { IsPositive } from "class-validator"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; +import { ResponseScan } from '../responses/ResponseScan'; import { Runner } from "./Runner"; /** @@ -46,4 +47,11 @@ export abstract class Scan { @Column() @IsBoolean() valid: boolean = true; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseScan { + return new ResponseScan(this); + } } \ No newline at end of file diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index 1df5abf..20c5f05 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -61,4 +61,11 @@ export class ScanStation { */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/StatsClient.ts b/src/models/entities/StatsClient.ts index 493a8da..eb48cbf 100644 --- a/src/models/entities/StatsClient.ts +++ b/src/models/entities/StatsClient.ts @@ -1,5 +1,6 @@ import { IsInt, IsOptional, IsString } from "class-validator"; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseStatsClient } from '../responses/ResponseStatsClient'; /** * Defines the StatsClient entity. * StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on). @@ -45,4 +46,11 @@ export class StatsClient { @IsString() @IsOptional() cleartextkey?: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseStatsClient { + return new ResponseStatsClient(this); + } } \ No newline at end of file diff --git a/src/models/entities/Track.ts b/src/models/entities/Track.ts index df18762..5e172f7 100644 --- a/src/models/entities/Track.ts +++ b/src/models/entities/Track.ts @@ -6,6 +6,7 @@ import { IsString } from "class-validator"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseTrack } from '../responses/ResponseTrack'; import { ScanStation } from "./ScanStation"; import { TrackScan } from "./TrackScan"; @@ -61,4 +62,11 @@ export class Track { */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseTrack { + return new ResponseTrack(this); + } } diff --git a/src/models/entities/TrackScan.ts b/src/models/entities/TrackScan.ts index 45be379..8c4143b 100644 --- a/src/models/entities/TrackScan.ts +++ b/src/models/entities/TrackScan.ts @@ -6,6 +6,7 @@ import { IsPositive } from "class-validator"; import { ChildEntity, Column, ManyToOne } from "typeorm"; +import { ResponseTrackScan } from '../responses/ResponseTrackScan'; import { RunnerCard } from "./RunnerCard"; import { Scan } from "./Scan"; import { ScanStation } from "./ScanStation"; @@ -59,4 +60,11 @@ export class TrackScan extends Scan { @IsDateString() @IsNotEmpty() timestamp: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseTrackScan { + return new ResponseTrackScan(this); + } } diff --git a/src/models/entities/UserAction.ts b/src/models/entities/UserAction.ts index d22bd37..d598be0 100644 --- a/src/models/entities/UserAction.ts +++ b/src/models/entities/UserAction.ts @@ -52,4 +52,11 @@ export class UserAction { @IsOptional() @IsString() changed: string; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file From 2cad2ac2e95d0ece9ec7a7f294aa6e7901915b0c Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 19:18:31 +0100 Subject: [PATCH 05/47] Implemented the second round of the toResponse normalisationf for all classes ref #67 --- src/models/entities/Address.ts | 7 +++++++ src/models/entities/DistanceDonation.ts | 7 +++++++ src/models/entities/Donation.ts | 7 +++++++ src/models/entities/Donor.ts | 8 ++++++++ src/models/entities/FixedDonation.ts | 7 +++++++ src/models/entities/GroupContact.ts | 7 +++++++ src/models/entities/IAddressUser.ts | 5 +++++ src/models/entities/Participant.ts | 6 ++++++ src/models/entities/Permission.ts | 8 ++++++++ src/models/entities/Runner.ts | 8 ++++++++ src/models/entities/RunnerCard.ts | 7 +++++++ src/models/entities/RunnerGroup.ts | 6 ++++++ 12 files changed, 83 insertions(+) diff --git a/src/models/entities/Address.ts b/src/models/entities/Address.ts index 561808f..dbb7eb3 100644 --- a/src/models/entities/Address.ts +++ b/src/models/entities/Address.ts @@ -80,4 +80,11 @@ export class Address { */ @OneToMany(() => IAddressUser, addressUser => addressUser.address, { nullable: true }) addressUsers: IAddressUser[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/DistanceDonation.ts b/src/models/entities/DistanceDonation.ts index d263e90..6b9ba5d 100644 --- a/src/models/entities/DistanceDonation.ts +++ b/src/models/entities/DistanceDonation.ts @@ -39,4 +39,11 @@ export class DistanceDonation extends Donation { } return calculatedAmount; } + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/Donation.ts b/src/models/entities/Donation.ts index eea23b5..46d7d45 100644 --- a/src/models/entities/Donation.ts +++ b/src/models/entities/Donation.ts @@ -32,4 +32,11 @@ export abstract class Donation { * The exact implementation may differ for each type of donation. */ abstract amount: number; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/Donor.ts b/src/models/entities/Donor.ts index 188ed09..01b365c 100644 --- a/src/models/entities/Donor.ts +++ b/src/models/entities/Donor.ts @@ -1,5 +1,6 @@ import { IsBoolean } from "class-validator"; import { ChildEntity, Column, OneToMany } from "typeorm"; +import { ResponseDonor } from '../responses/ResponseDonor'; import { Donation } from './Donation'; import { Participant } from "./Participant"; @@ -22,4 +23,11 @@ export class Donor extends Participant { */ @OneToMany(() => Donation, donation => donation.donor, { nullable: true }) donations: Donation[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseDonor { + return new ResponseDonor(this); + } } \ No newline at end of file diff --git a/src/models/entities/FixedDonation.ts b/src/models/entities/FixedDonation.ts index db53e2f..6a32066 100644 --- a/src/models/entities/FixedDonation.ts +++ b/src/models/entities/FixedDonation.ts @@ -16,4 +16,11 @@ export class FixedDonation extends Donation { @IsInt() @IsPositive() amount: number; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/GroupContact.ts b/src/models/entities/GroupContact.ts index 4dbcc5e..f650259 100644 --- a/src/models/entities/GroupContact.ts +++ b/src/models/entities/GroupContact.ts @@ -81,4 +81,11 @@ export class GroupContact implements IAddressUser { */ @OneToMany(() => RunnerGroup, group => group.contact, { nullable: true }) groups: RunnerGroup[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } \ No newline at end of file diff --git a/src/models/entities/IAddressUser.ts b/src/models/entities/IAddressUser.ts index 3a7762a..3d8eaf9 100644 --- a/src/models/entities/IAddressUser.ts +++ b/src/models/entities/IAddressUser.ts @@ -12,4 +12,9 @@ export abstract class IAddressUser { @ManyToOne(() => Address, address => address.addressUsers, { nullable: true }) address?: Address + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(); } diff --git a/src/models/entities/Participant.ts b/src/models/entities/Participant.ts index de13b5f..fa40a8f 100644 --- a/src/models/entities/Participant.ts +++ b/src/models/entities/Participant.ts @@ -9,6 +9,7 @@ import { } from "class-validator"; import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; import { config } from '../../config'; +import { ResponseParticipant } from '../responses/ResponseParticipant'; import { Address } from "./Address"; import { IAddressUser } from './IAddressUser'; @@ -74,4 +75,9 @@ export abstract class Participant implements IAddressUser { @IsOptional() @IsEmail() email?: string; + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(): ResponseParticipant; } \ No newline at end of file diff --git a/src/models/entities/Permission.ts b/src/models/entities/Permission.ts index 6a07312..4bba550 100644 --- a/src/models/entities/Permission.ts +++ b/src/models/entities/Permission.ts @@ -6,6 +6,7 @@ import { import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import { PermissionAction } from '../enums/PermissionAction'; import { PermissionTarget } from '../enums/PermissionTargets'; +import { ResponsePermission } from '../responses/ResponsePermission'; import { Principal } from './Principal'; /** * Defines the Permission entity. @@ -51,4 +52,11 @@ export class Permission { public toString(): string { return this.target + ":" + this.action; } + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponsePermission { + return new ResponsePermission(this); + } } \ No newline at end of file diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index 5c51c11..f391103 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -1,5 +1,6 @@ import { IsInt, IsNotEmpty } from "class-validator"; import { ChildEntity, ManyToOne, OneToMany } from "typeorm"; +import { ResponseRunner } from '../responses/ResponseRunner'; import { DistanceDonation } from "./DistanceDonation"; import { Participant } from "./Participant"; import { RunnerCard } from "./RunnerCard"; @@ -66,4 +67,11 @@ export class Runner extends Participant { public get distanceDonationAmount(): number { return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance; } + + /** + * Turns this entity into it's response class. + */ + public toResponse(): ResponseRunner { + return new ResponseRunner(this); + } } \ No newline at end of file diff --git a/src/models/entities/RunnerCard.ts b/src/models/entities/RunnerCard.ts index 5f48257..4ea40e2 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -57,4 +57,11 @@ export class RunnerCard { */ @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + + /** + * Turns this entity into it's response class. + */ + public toResponse() { + return new Error("NotImplemented"); + } } diff --git a/src/models/entities/RunnerGroup.ts b/src/models/entities/RunnerGroup.ts index b5bcfcd..5620aca 100644 --- a/src/models/entities/RunnerGroup.ts +++ b/src/models/entities/RunnerGroup.ts @@ -5,6 +5,7 @@ import { IsString } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm"; +import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup'; import { GroupContact } from "./GroupContact"; import { Runner } from "./Runner"; @@ -60,4 +61,9 @@ export abstract class RunnerGroup { public get distanceDonationAmount(): number { return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0); } + + /** + * Turns this entity into it's response class. + */ + public abstract toResponse(): ResponseRunnerGroup; } \ No newline at end of file From f9889bea3d5d049b08d471ad60264f190aaaad54 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 19:26:06 +0100 Subject: [PATCH 06/47] Implemented scans get including the response classes ref #67 --- src/controllers/ScanController.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index a9cea38..a9e0cd8 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -2,9 +2,10 @@ import { Authorized, Get, JsonController } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { Scan } from '../models/entities/Scan'; -import { ResponseDonor } from '../models/responses/ResponseDonor'; +import { ResponseScan } from '../models/responses/ResponseScan'; +import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; -@JsonController('/donors') +@JsonController('/scans') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) export class ScanController { private scanRepository: Repository; @@ -18,13 +19,14 @@ export class ScanController { @Get() @Authorized("SCAN:GET") - @ResponseSchema(ResponseDonor, { isArray: true }) + @ResponseSchema(ResponseScan, { isArray: true }) + @ResponseSchema(ResponseTrackScan, { isArray: true }) @OpenAPI({ description: 'Lists all runners from all teams/orgs.
This includes the runner\'s group and distance ran.' }) async getAll() { let responseScans: ResponseScan[] = new Array(); const scans = await this.scanRepository.find(); scans.forEach(scan => { - responseScans.push(new ResponseScan(scan)); + responseScans.push(scan.toResponse()); }); return responseScans; } From aeec2e1c3255557bab0452ac931d346335269396 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sun, 3 Jan 2021 19:36:38 +0100 Subject: [PATCH 07/47] Added single scan get w/ errors ref #67 --- src/controllers/ScanController.ts | 28 +++++++++++++++------------- src/errors/ScanErrors.ts | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 src/errors/ScanErrors.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index a9e0cd8..6127e8a 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -1,6 +1,7 @@ -import { Authorized, Get, JsonController } from 'routing-controllers'; +import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; +import { ScanNotFoundError } from '../errors/ScanErrors'; import { Scan } from '../models/entities/Scan'; import { ResponseScan } from '../models/responses/ResponseScan'; import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; @@ -21,7 +22,7 @@ export class ScanController { @Authorized("SCAN:GET") @ResponseSchema(ResponseScan, { isArray: true }) @ResponseSchema(ResponseTrackScan, { isArray: true }) - @OpenAPI({ description: 'Lists all runners from all teams/orgs.
This includes the runner\'s group and distance ran.' }) + @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) async getAll() { let responseScans: ResponseScan[] = new Array(); const scans = await this.scanRepository.find(); @@ -31,17 +32,18 @@ export class ScanController { return responseScans; } - // @Get('/:id') - // @Authorized("DONOR:GET") - // @ResponseSchema(ResponseDonor) - // @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) - // @OnUndefined(DonorNotFoundError) - // @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) - // async getOne(@Param('id') id: number) { - // let donor = await this.donorRepository.findOne({ id: id }) - // if (!donor) { throw new DonorNotFoundError(); } - // return new ResponseDonor(donor); - // } + @Get('/:id') + @Authorized("DONOR:GET") + @ResponseSchema(ResponseScan) + @ResponseSchema(ResponseTrackScan) + @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) + @OnUndefined(ScanNotFoundError) + @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + async getOne(@Param('id') id: number) { + let scan = await this.scanRepository.findOne({ id: id }) + if (!scan) { throw new ScanNotFoundError(); } + return scan; + } // @Post() // @Authorized("DONOR:CREATE") diff --git a/src/errors/ScanErrors.ts b/src/errors/ScanErrors.ts new file mode 100644 index 0000000..77894df --- /dev/null +++ b/src/errors/ScanErrors.ts @@ -0,0 +1,25 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw when a Scan couldn't be found. + */ +export class ScanNotFoundError extends NotFoundError { + @IsString() + name = "ScanNotFoundError" + + @IsString() + message = "Scan not found!" +} + +/** + * Error to throw when two Scans' ids don't match. + * Usually occurs when a user tries to change a Scan's id. + */ +export class ScanIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "ScanIdsNotMatchingError" + + @IsString() + message = "The ids don't match! \n And if you wanted to change a Scan's id: This isn't allowed!" +} \ No newline at end of file From 72b5ca415340d96649ecce943d19f0e0b2b82fda Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Wed, 6 Jan 2021 19:44:20 +0100 Subject: [PATCH 08/47] Added basics for scan creation (to be tested after scanstations got added) ref #67 --- src/controllers/ScanController.ts | 29 ++++----- src/models/actions/CreateScan.ts | 11 ++++ src/models/actions/CreateTrackScan.ts | 84 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 src/models/actions/CreateScan.ts create mode 100644 src/models/actions/CreateTrackScan.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 6127e8a..fb99d3a 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -1,7 +1,8 @@ -import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; +import { Authorized, Body, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { ScanNotFoundError } from '../errors/ScanErrors'; +import { CreateScan } from '../models/actions/CreateScan'; import { Scan } from '../models/entities/Scan'; import { ResponseScan } from '../models/responses/ResponseScan'; import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; @@ -33,7 +34,7 @@ export class ScanController { } @Get('/:id') - @Authorized("DONOR:GET") + @Authorized("SCAN:GET") @ResponseSchema(ResponseScan) @ResponseSchema(ResponseTrackScan) @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) @@ -45,21 +46,15 @@ export class ScanController { return scan; } - // @Post() - // @Authorized("DONOR:CREATE") - // @ResponseSchema(ResponseDonor) - // @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) - // async post(@Body({ validate: true }) createRunner: CreateDonor) { - // let donor; - // try { - // donor = await createRunner.toDonor(); - // } catch (error) { - // throw error; - // } - - // donor = await this.donorRepository.save(donor) - // return new ResponseDonor(await this.donorRepository.findOne(donor)); - // } + @Post() + @Authorized("SCAN:CREATE") + @ResponseSchema(ResponseScan) + @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + async post(@Body({ validate: true }) createScan: CreateScan) { + let scan = await createScan.toScan(); + scan = await this.scanRepository.save(scan) + return (await this.scanRepository.findOne(scan)).toResponse(); + } // @Put('/:id') // @Authorized("DONOR:UPDATE") diff --git a/src/models/actions/CreateScan.ts b/src/models/actions/CreateScan.ts new file mode 100644 index 0000000..170703b --- /dev/null +++ b/src/models/actions/CreateScan.ts @@ -0,0 +1,11 @@ +import { Scan } from '../entities/Scan'; + +/** + * This classed is used to create a new Scan entity from a json body (post request). + */ +export abstract class CreateScan { + /** + * Creates a new Scan entity from this. + */ + public abstract toScan(): Promise; +} \ No newline at end of file diff --git a/src/models/actions/CreateTrackScan.ts b/src/models/actions/CreateTrackScan.ts new file mode 100644 index 0000000..2303352 --- /dev/null +++ b/src/models/actions/CreateTrackScan.ts @@ -0,0 +1,84 @@ +import { IsNotEmpty } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../errors/RunnerErrors'; +import { RunnerCard } from '../entities/RunnerCard'; +import { ScanStation } from '../entities/ScanStation'; +import { TrackScan } from '../entities/TrackScan'; +import { CreateScan } from './CreateScan'; + +/** + * This classed is used to create a new Scan entity from a json body (post request). + */ +export class CreateTrackScan extends CreateScan { + + /** + * The scan's associated track. + * This is used to determine the scan's distance. + */ + @IsNotEmpty() + track: number; + + /** + * The runnerCard associated with the scan. + * This get's saved for documentation and management purposes. + */ + @IsNotEmpty() + card: number; + + /** + * The scanning station that created the scan. + * Mainly used for logging and traceing back scans (or errors) + */ + @IsNotEmpty() + station: number; + + /** + * Creates a new Track entity from this. + */ + public async toScan(): Promise { + let newScan: TrackScan = new TrackScan(); + + newScan.station = await this.getStation(); + newScan.card = await this.getCard(); + + newScan.track = newScan.station.track; + newScan.runner = newScan.card.runner; + + if (!newScan.runner) { + throw new RunnerNotFoundError(); + } + + newScan.timestamp = new Date(Date.now()).toString(); + newScan.valid = await this.validateScan(newScan); + + return newScan; + } + + public async getCard(): Promise { + const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] }); + if (!track) { + throw new Error(); + } + return track; + } + + public async getStation(): Promise { + const track = await getConnection().getRepository(ScanStation).findOne({ id: this.card }, { relations: ["track"] }); + if (!track) { + throw new Error(); + } + return track; + } + + public async validateScan(scan: TrackScan): Promise { + const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner }, relations: ["track"] }); + if (scans.length == 0) { return true; } + + const newestScan = scans[0]; + if ((new Date(scan.timestamp).getTime() - new Date(newestScan.timestamp).getTime()) > scan.track.minimumLapTime) { + return true; + } + + return false; + } +} \ No newline at end of file From d6a41d5a82e3c1e3593a494f6292eb25f1f6533d Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 16:13:31 +0100 Subject: [PATCH 09/47] Ajusted the way scan distances are implemented --- src/models/entities/Scan.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index 3c1cb67..c525eb3 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -15,7 +15,7 @@ import { Runner } from "./Runner"; */ @Entity() @TableInheritance({ column: { name: "type", type: "varchar" } }) -export abstract class Scan { +export class Scan { /** * Autogenerated unique id (primary key). */ @@ -31,14 +31,6 @@ export abstract class Scan { @ManyToOne(() => Runner, runner => runner.scans, { nullable: false }) runner: Runner; - /** - * The scan's distance in meters. - * Can be set manually or derived from another object. - */ - @IsInt() - @IsPositive() - abstract distance: number; - /** * Is the scan valid (for fraud reasons). * The determination of validity will work differently for every child class. @@ -48,6 +40,24 @@ export abstract class Scan { @IsBoolean() valid: boolean = true; + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + @Column({ nullable: true }) + @IsInt() + private _distance?: number; + + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + @IsInt() + @IsPositive() + public get distance(): number { + return this._distance; + } + /** * Turns this entity into it's response class. */ From f1c7713da2db6d90df8161872887f61348fafd5b Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 16:13:31 +0100 Subject: [PATCH 10/47] Adusted the way scan distances are implemented ref #67 --- src/models/entities/Scan.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index 3c1cb67..c525eb3 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -15,7 +15,7 @@ import { Runner } from "./Runner"; */ @Entity() @TableInheritance({ column: { name: "type", type: "varchar" } }) -export abstract class Scan { +export class Scan { /** * Autogenerated unique id (primary key). */ @@ -31,14 +31,6 @@ export abstract class Scan { @ManyToOne(() => Runner, runner => runner.scans, { nullable: false }) runner: Runner; - /** - * The scan's distance in meters. - * Can be set manually or derived from another object. - */ - @IsInt() - @IsPositive() - abstract distance: number; - /** * Is the scan valid (for fraud reasons). * The determination of validity will work differently for every child class. @@ -48,6 +40,24 @@ export abstract class Scan { @IsBoolean() valid: boolean = true; + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + @Column({ nullable: true }) + @IsInt() + private _distance?: number; + + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + @IsInt() + @IsPositive() + public get distance(): number { + return this._distance; + } + /** * Turns this entity into it's response class. */ From 30502ec94991b31a26bdb9aa631a7323d3f2bccf Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 16:32:16 +0100 Subject: [PATCH 11/47] Fixed Creation of normal scans ref #67 --- src/controllers/ScanController.ts | 5 +-- src/models/actions/CreateScan.ts | 47 +++++++++++++++++++++++++++- src/models/entities/Scan.ts | 10 +++++- src/models/responses/ResponseScan.ts | 8 ++--- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index fb99d3a..cefef0d 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -52,8 +52,9 @@ export class ScanController { @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) async post(@Body({ validate: true }) createScan: CreateScan) { let scan = await createScan.toScan(); - scan = await this.scanRepository.save(scan) - return (await this.scanRepository.findOne(scan)).toResponse(); + scan = await this.scanRepository.save(scan); + console.log(scan); + return (await this.scanRepository.findOne({ id: scan.id })).toResponse(); } // @Put('/:id') diff --git a/src/models/actions/CreateScan.ts b/src/models/actions/CreateScan.ts index 170703b..ef6d0e4 100644 --- a/src/models/actions/CreateScan.ts +++ b/src/models/actions/CreateScan.ts @@ -1,11 +1,56 @@ +import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../errors/RunnerErrors'; +import { Runner } from '../entities/Runner'; import { Scan } from '../entities/Scan'; /** * This classed is used to create a new Scan entity from a json body (post request). */ export abstract class CreateScan { + /** + * The scan's associated runner. + * This is important to link ran distances to runners. + */ + @IsInt() + @IsPositive() + runner: number; + + /** + * Is the scan valid (for fraud reasons). + * The determination of validity will work differently for every child class. + * Default: true + */ + @IsBoolean() + @IsOptional() + valid?: boolean = true; + + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + @IsInt() + @IsPositive() + public distance: number; + /** * Creates a new Scan entity from this. */ - public abstract toScan(): Promise; + public async toScan(): Promise { + let newScan = new Scan(); + + newScan.distance = this.distance; + newScan.valid = this.valid; + newScan.runner = await this.getRunner(); + + return newScan; + } + + public async getRunner(): Promise { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } } \ No newline at end of file diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index c525eb3..2424a0c 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -42,7 +42,7 @@ export class Scan { /** * The scan's distance in meters. - * Can be set manually or derived from another object. + * This is the "real" value used by "normal" scans.. */ @Column({ nullable: true }) @IsInt() @@ -58,6 +58,14 @@ export class Scan { return this._distance; } + /** + * The scan's distance in meters. + * Can be set manually or derived from another object. + */ + public set distance(value: number) { + this._distance = value; + } + /** * Turns this entity into it's response class. */ diff --git a/src/models/responses/ResponseScan.ts b/src/models/responses/ResponseScan.ts index b5883bd..debda1e 100644 --- a/src/models/responses/ResponseScan.ts +++ b/src/models/responses/ResponseScan.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsInt, IsNotEmpty, IsPositive } from "class-validator"; +import { IsBoolean, IsInt, IsPositive } from "class-validator"; import { Scan } from '../entities/Scan'; import { ResponseRunner } from './ResponseRunner'; @@ -16,8 +16,8 @@ export class ResponseScan { * The scan's associated runner. * This is important to link ran distances to runners. */ - @IsNotEmpty() - runner: ResponseRunner; + // @IsNotEmpty() + runner?: ResponseRunner; /** * Is the scan valid (for fraud reasons). @@ -39,7 +39,7 @@ export class ResponseScan { */ public constructor(scan: Scan) { this.id = scan.id; - this.runner = new ResponseRunner(scan.runner); + this.runner = null; this.distance = scan.distance; this.valid = scan.valid; } From e67d1c56976994ffeae47f038d453123908dc08d Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 16:38:41 +0100 Subject: [PATCH 12/47] Fixed scan runner in response ref #67 --- src/controllers/ScanController.ts | 8 ++++---- src/models/responses/ResponseRunner.ts | 3 ++- src/models/responses/ResponseScan.ts | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index cefef0d..d82195e 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -26,7 +26,7 @@ export class ScanController { @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) async getAll() { let responseScans: ResponseScan[] = new Array(); - const scans = await this.scanRepository.find(); + const scans = await this.scanRepository.find({ relations: ['runner'] }); scans.forEach(scan => { responseScans.push(scan.toResponse()); }); @@ -41,9 +41,9 @@ export class ScanController { @OnUndefined(ScanNotFoundError) @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) async getOne(@Param('id') id: number) { - let scan = await this.scanRepository.findOne({ id: id }) + let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] }) if (!scan) { throw new ScanNotFoundError(); } - return scan; + return scan.toResponse(); } @Post() @@ -54,7 +54,7 @@ export class ScanController { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); console.log(scan); - return (await this.scanRepository.findOne({ id: scan.id })).toResponse(); + return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); } // @Put('/:id') diff --git a/src/models/responses/ResponseRunner.ts b/src/models/responses/ResponseRunner.ts index 5fae2ee..82402a8 100644 --- a/src/models/responses/ResponseRunner.ts +++ b/src/models/responses/ResponseRunner.ts @@ -29,7 +29,8 @@ export class ResponseRunner extends ResponseParticipant { */ public constructor(runner: Runner) { super(runner); - this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0); + if (!runner.scans) { this.distance = 0 } + else { this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0); } this.group = runner.group; } } diff --git a/src/models/responses/ResponseScan.ts b/src/models/responses/ResponseScan.ts index debda1e..e666442 100644 --- a/src/models/responses/ResponseScan.ts +++ b/src/models/responses/ResponseScan.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsInt, IsPositive } from "class-validator"; +import { IsBoolean, IsInt, IsNotEmpty, IsPositive } from "class-validator"; import { Scan } from '../entities/Scan'; import { ResponseRunner } from './ResponseRunner'; @@ -16,8 +16,8 @@ export class ResponseScan { * The scan's associated runner. * This is important to link ran distances to runners. */ - // @IsNotEmpty() - runner?: ResponseRunner; + @IsNotEmpty() + runner: ResponseRunner; /** * Is the scan valid (for fraud reasons). @@ -39,7 +39,7 @@ export class ResponseScan { */ public constructor(scan: Scan) { this.id = scan.id; - this.runner = null; + this.runner = scan.runner.toResponse(); this.distance = scan.distance; this.valid = scan.valid; } From edac1a224c8ce6fa44ff39302431e64f014b7137 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 16:59:57 +0100 Subject: [PATCH 13/47] Fixed runner scan validation bug ref #67 --- src/controllers/ScanController.ts | 1 - src/models/entities/Runner.ts | 2 +- src/models/responses/ResponseRunner.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index d82195e..272a9ee 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -53,7 +53,6 @@ export class ScanController { async post(@Body({ validate: true }) createScan: CreateScan) { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); - console.log(scan); return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); } diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index f391103..bc6bc5a 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -48,7 +48,7 @@ export class Runner extends Participant { * This is implemented here to avoid duplicate code in other files. */ public get validScans(): Scan[] { - return this.scans.filter(scan => { scan.valid === true }); + return this.scans.filter(scan => scan.valid == true); } /** diff --git a/src/models/responses/ResponseRunner.ts b/src/models/responses/ResponseRunner.ts index 82402a8..0d2fb67 100644 --- a/src/models/responses/ResponseRunner.ts +++ b/src/models/responses/ResponseRunner.ts @@ -30,7 +30,7 @@ export class ResponseRunner extends ResponseParticipant { public constructor(runner: Runner) { super(runner); if (!runner.scans) { this.distance = 0 } - else { this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0); } + else { this.distance = runner.validScans.reduce((sum, current) => sum + current.distance, 0); } this.group = runner.group; } } From 88a6a768c4849731619024c47e75094be70463de Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:03:40 +0100 Subject: [PATCH 14/47] Implemented scan deletion ref #67 --- src/controllers/ScanController.ts | 35 +++++++++++++------------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 272a9ee..259727c 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -1,9 +1,10 @@ -import { Authorized, Body, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { ScanNotFoundError } from '../errors/ScanErrors'; import { CreateScan } from '../models/actions/CreateScan'; import { Scan } from '../models/entities/Scan'; +import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseScan } from '../models/responses/ResponseScan'; import { ResponseTrackScan } from '../models/responses/ResponseTrackScan'; @@ -77,24 +78,18 @@ export class ScanController { // return new ResponseDonor(await this.donorRepository.findOne({ id: id })); // } - // @Delete('/:id') - // @Authorized("DONOR:DELETE") - // @ResponseSchema(ResponseDonor) - // @ResponseSchema(ResponseEmpty, { statusCode: 204 }) - // @OnUndefined(204) - // @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) - // async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { - // let donor = await this.donorRepository.findOne({ id: id }); - // if (!donor) { return null; } - // const responseDonor = await this.donorRepository.findOne(donor); + @Delete('/:id') + @Authorized("SCAN:DELETE") + @ResponseSchema(ResponseScan) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @OnUndefined(204) + @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + let scan = await this.scanRepository.findOne({ id: id }); + if (!scan) { return null; } + const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ["runner"] }); - // if (!donor) { - // throw new DonorNotFoundError(); - // } - - // //TODO: DELETE DONATIONS AND WARN FOR FORCE (https://git.odit.services/lfk/backend/issues/66) - - // await this.donorRepository.delete(donor); - // return new ResponseDonor(responseDonor); - // } + await this.scanRepository.delete(scan); + return responseScan.toResponse(); + } } From eec528430682fdf2209825d511852141a1c6bd2b Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:12:12 +0100 Subject: [PATCH 15/47] Implemented "normal" scan updateing ref #67 --- src/controllers/ScanController.ts | 41 +++++++++++---------- src/models/actions/UpdateScan.ts | 59 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 src/models/actions/UpdateScan.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 259727c..6674f36 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -1,8 +1,10 @@ -import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { ScanNotFoundError } from '../errors/ScanErrors'; +import { RunnerNotFoundError } from '../errors/RunnerErrors'; +import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors'; import { CreateScan } from '../models/actions/CreateScan'; +import { UpdateScan } from '../models/actions/UpdateScan'; import { Scan } from '../models/entities/Scan'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseScan } from '../models/responses/ResponseScan'; @@ -57,26 +59,27 @@ export class ScanController { return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); } - // @Put('/:id') - // @Authorized("DONOR:UPDATE") - // @ResponseSchema(ResponseDonor) - // @ResponseSchema(DonorNotFoundError, { statusCode: 404 }) - // @ResponseSchema(DonorIdsNotMatchingError, { statusCode: 406 }) - // @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) - // async put(@Param('id') id: number, @Body({ validate: true }) donor: UpdateDonor) { - // let oldDonor = await this.donorRepository.findOne({ id: id }); + @Put('/:id') + @Authorized("SCAN:UPDATE") + @ResponseSchema(ResponseScan) + @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) + async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) { + let oldScan = await this.scanRepository.findOne({ id: id }); - // if (!oldDonor) { - // throw new DonorNotFoundError(); - // } + if (!oldScan) { + throw new ScanNotFoundError(); + } - // if (oldDonor.id != donor.id) { - // throw new DonorIdsNotMatchingError(); - // } + if (oldScan.id != scan.id) { + throw new ScanIdsNotMatchingError(); + } - // await this.donorRepository.save(await donor.updateDonor(oldDonor)); - // return new ResponseDonor(await this.donorRepository.findOne({ id: id })); - // } + await this.scanRepository.save(await scan.updateScan(oldScan)); + return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse(); + } @Delete('/:id') @Authorized("SCAN:DELETE") diff --git a/src/models/actions/UpdateScan.ts b/src/models/actions/UpdateScan.ts new file mode 100644 index 0000000..a51ccea --- /dev/null +++ b/src/models/actions/UpdateScan.ts @@ -0,0 +1,59 @@ +import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../errors/RunnerErrors'; +import { Runner } from '../entities/Runner'; +import { Scan } from '../entities/Scan'; + +/** + * This classed is used to create a new Scan entity from a json body (post request). + */ +export abstract class UpdateScan { + /** + * The updated scan's id. + * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to). + */ + @IsInt() + id: number; + + /** + * The updated scan's associated runner. + * This is important to link ran distances to runners. + */ + @IsInt() + @IsPositive() + runner: number; + + /** + * Is the updated scan valid (for fraud reasons). + */ + @IsBoolean() + @IsOptional() + valid?: boolean = true; + + /** + * The updated scan's distance in meters. + */ + @IsInt() + @IsPositive() + public distance: number; + + /** + * Update a Scan entity based on this. + * @param scan The scan that shall be updated. + */ + public async updateScan(scan: Scan): Promise { + scan.distance = this.distance; + scan.valid = this.valid; + scan.runner = await this.getRunner(); + + return scan; + } + + public async getRunner(): Promise { + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file From eea656bd7b09c8a878235b88793a7a2aa4baf41b Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:16:36 +0100 Subject: [PATCH 16/47] Added a barebones scanstation controller ref #67 --- src/controllers/ScanStationController.ts | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/controllers/ScanStationController.ts diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts new file mode 100644 index 0000000..ac4aa11 --- /dev/null +++ b/src/controllers/ScanStationController.ts @@ -0,0 +1,91 @@ +import { JsonController } from 'routing-controllers'; +import { OpenAPI } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { ScanStation } from '../models/entities/ScanStation'; + +@JsonController('/stations') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class ScanStationController { + private stationRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.stationRepository = getConnectionManager().get().getRepository(ScanStation); + } + + // @Get() + // @Authorized("SCAN:GET") + // @ResponseSchema(ResponseScan, { isArray: true }) + // @ResponseSchema(ResponseTrackScan, { isArray: true }) + // @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) + // async getAll() { + // let responseScans: ResponseScan[] = new Array(); + // const scans = await this.scanRepository.find({ relations: ['runner'] }); + // scans.forEach(scan => { + // responseScans.push(scan.toResponse()); + // }); + // return responseScans; + // } + + // @Get('/:id') + // @Authorized("SCAN:GET") + // @ResponseSchema(ResponseScan) + // @ResponseSchema(ResponseTrackScan) + // @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) + // @OnUndefined(ScanNotFoundError) + // @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + // async getOne(@Param('id') id: number) { + // let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] }) + // if (!scan) { throw new ScanNotFoundError(); } + // return scan.toResponse(); + // } + + // @Post() + // @Authorized("SCAN:CREATE") + // @ResponseSchema(ResponseScan) + // @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + // async post(@Body({ validate: true }) createScan: CreateScan) { + // let scan = await createScan.toScan(); + // scan = await this.scanRepository.save(scan); + // return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); + // } + + // @Put('/:id') + // @Authorized("SCAN:UPDATE") + // @ResponseSchema(ResponseScan) + // @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) + // @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + // @ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 }) + // @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) + // async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) { + // let oldScan = await this.scanRepository.findOne({ id: id }); + + // if (!oldScan) { + // throw new ScanNotFoundError(); + // } + + // if (oldScan.id != scan.id) { + // throw new ScanIdsNotMatchingError(); + // } + + // await this.scanRepository.save(await scan.updateScan(oldScan)); + // return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse(); + // } + + // @Delete('/:id') + // @Authorized("SCAN:DELETE") + // @ResponseSchema(ResponseScan) + // @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + // @OnUndefined(204) + // @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + // async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + // let scan = await this.scanRepository.findOne({ id: id }); + // if (!scan) { return null; } + // const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ["runner"] }); + + // await this.scanRepository.delete(scan); + // return responseScan.toResponse(); + // } +} From 857de9ffcc637ab5e9761e8e5cc00067d4d21745 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:29:22 +0100 Subject: [PATCH 17/47] Added Creation class for ScanSatations ref #67 --- src/models/actions/CreateScanStation.ts | 53 +++++++++++++++++++++++++ src/models/entities/ScanStation.ts | 19 ++++++--- 2 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 src/models/actions/CreateScanStation.ts diff --git a/src/models/actions/CreateScanStation.ts b/src/models/actions/CreateScanStation.ts new file mode 100644 index 0000000..fd1d8ad --- /dev/null +++ b/src/models/actions/CreateScanStation.ts @@ -0,0 +1,53 @@ +import * as argon2 from "argon2"; +import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; +import crypto from 'crypto'; +import { getConnection } from 'typeorm'; +import * as uuid from 'uuid'; +import { TrackNotFoundError } from '../../errors/TrackErrors'; +import { ScanStation } from '../entities/ScanStation'; +import { Track } from '../entities/Track'; + +/** + * This classed is used to create a new StatsClient entity from a json body (post request). + */ +export class CreateStatsClient { + /** + * The new client's description. + */ + @IsString() + @IsOptional() + description?: string; + + /** + * The scan's associated track. + * This is used to determine the scan's distance. + */ + @IsInt() + @IsPositive() + track: number; + + /** + * Converts this to a ScanStation entity. + */ + public async toEntity(): Promise { + let newStation: ScanStation = new ScanStation(); + + newStation.description = this.description; + newStation.track = await this.getTrack(); + + let newUUID = uuid.v4().toUpperCase(); + newStation.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase(); + newStation.key = await argon2.hash(newStation.prefix + "." + newUUID); + newStation.cleartextkey = newStation.prefix + "." + newUUID; + + return newStation; + } + + public async getTrack(): Promise { + const track = await getConnection().getRepository(Track).findOne({ id: this.track }); + if (!track) { + throw new TrackNotFoundError(); + } + return track; + } +} \ No newline at end of file diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index 20c5f05..e9c03d2 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -1,5 +1,4 @@ import { - IsBoolean, IsInt, IsNotEmpty, IsOptional, @@ -39,6 +38,14 @@ export class ScanStation { @ManyToOne(() => Track, track => track.stations, { nullable: false }) track: Track; + /** + * The client's api key prefix. + * This is used identitfy a client by it's api key. + */ + @Column({ unique: true }) + @IsString() + prefix: string; + /** * The station's api key. * This is used to authorize a station against the api (not implemented yet). @@ -49,12 +56,12 @@ export class ScanStation { key: string; /** - * Is the station enabled (for fraud and setup reasons)? - * Default: true + * The client's api key in plain text. + * This will only be used to display the full key on creation and updates. */ - @Column() - @IsBoolean() - enabled: boolean = true; + @IsString() + @IsOptional() + cleartextkey?: string; /** * Used to link track scans to a scan station. From c447114297f2a5d2f64582a16cf5c6a2b258086c Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:31:44 +0100 Subject: [PATCH 18/47] Added a ScanStation response class ref #67 --- src/models/responses/ResponseScanStation.ts | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/models/responses/ResponseScanStation.ts diff --git a/src/models/responses/ResponseScanStation.ts b/src/models/responses/ResponseScanStation.ts new file mode 100644 index 0000000..6b147c9 --- /dev/null +++ b/src/models/responses/ResponseScanStation.ts @@ -0,0 +1,62 @@ +import { + + IsInt, + + IsNotEmpty, + + IsObject, + + IsOptional, + IsString +} from "class-validator"; +import { ScanStation } from '../entities/ScanStation'; +import { ResponseTrack } from './ResponseTrack'; + +/** + * Defines the statsClient response. +*/ +export class ResponseScanStation { + /** + * The client's id. + */ + @IsInt() + id: number; + + /** + * The client's description. + */ + @IsString() + @IsOptional() + description?: string; + + /** + * The client's api key. + * Only visible on creation or regeneration. + */ + @IsString() + @IsOptional() + key: string; + + /** + * The client's api key prefix. + */ + @IsString() + @IsNotEmpty() + prefix: string; + + @IsObject() + @IsNotEmpty() + track: ResponseTrack; + + /** + * Creates a ResponseStatsClient object from a statsClient. + * @param client The statsClient the response shall be build for. + */ + public constructor(station: ScanStation) { + this.id = station.id; + this.description = station.description; + this.prefix = station.prefix; + this.key = "Only visible on creation."; + this.track = station.track; + } +} From 3d2c93b5acae4c37278e45580f81ded3a5f088ec Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 17:35:36 +0100 Subject: [PATCH 19/47] Added (scan) stations as a new permission target ref #67 --- src/models/enums/PermissionTargets.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/enums/PermissionTargets.ts b/src/models/enums/PermissionTargets.ts index de69ff0..dae9192 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -11,5 +11,6 @@ export enum PermissionTarget { PERMISSION = 'PERMISSION', STATSCLIENT = 'STATSCLIENT', DONOR = 'DONOR', - SCAN = 'SCAN' + SCAN = 'SCAN', + STATION = 'STATION' } \ No newline at end of file From 82644a2ff49d678937ebe7648bc2f0013cad0031 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 18:05:54 +0100 Subject: [PATCH 20/47] Implmented getting all scan stations ref #67 --- src/controllers/ScanStationController.ts | 30 ++++++++++++------------ src/models/entities/ScanStation.ts | 5 ++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index ac4aa11..665a010 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -1,7 +1,8 @@ -import { JsonController } from 'routing-controllers'; -import { OpenAPI } from 'routing-controllers-openapi'; +import { Authorized, Get, JsonController } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { ScanStation } from '../models/entities/ScanStation'; +import { ResponseScanStation } from '../models/responses/ResponseScanStation'; @JsonController('/stations') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -15,19 +16,18 @@ export class ScanStationController { this.stationRepository = getConnectionManager().get().getRepository(ScanStation); } - // @Get() - // @Authorized("SCAN:GET") - // @ResponseSchema(ResponseScan, { isArray: true }) - // @ResponseSchema(ResponseTrackScan, { isArray: true }) - // @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) - // async getAll() { - // let responseScans: ResponseScan[] = new Array(); - // const scans = await this.scanRepository.find({ relations: ['runner'] }); - // scans.forEach(scan => { - // responseScans.push(scan.toResponse()); - // }); - // return responseScans; - // } + @Get() + @Authorized("STATION:GET") + @ResponseSchema(ResponseScanStation, { isArray: true }) + @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) + async getAll() { + let responseStations: ResponseScanStation[] = new Array(); + const stations = await this.stationRepository.find({ relations: ['track'] }); + stations.forEach(station => { + responseStations.push(station.toResponse()); + }); + return responseStations; + } // @Get('/:id') // @Authorized("SCAN:GET") diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index e9c03d2..93fde64 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -5,6 +5,7 @@ import { IsString } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseScanStation } from '../responses/ResponseScanStation'; import { Track } from "./Track"; import { TrackScan } from "./TrackScan"; @@ -72,7 +73,7 @@ export class ScanStation { /** * Turns this entity into it's response class. */ - public toResponse() { - return new Error("NotImplemented"); + public toResponse(): ResponseScanStation { + return new ResponseScanStation(this); } } From b9c0a328628cb6a68460d4c3264487ec18004a8b Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 18:35:19 +0100 Subject: [PATCH 21/47] Implemented single scan station get +e errors ref #67 --- src/controllers/ScanStationController.ts | 26 ++++++++++++------------ src/errors/ScanStationErrors.ts | 25 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 src/errors/ScanStationErrors.ts diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index 665a010..fc13028 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -1,6 +1,7 @@ -import { Authorized, Get, JsonController } from 'routing-controllers'; +import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; +import { ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { ScanStation } from '../models/entities/ScanStation'; import { ResponseScanStation } from '../models/responses/ResponseScanStation'; @@ -29,18 +30,17 @@ export class ScanStationController { return responseStations; } - // @Get('/:id') - // @Authorized("SCAN:GET") - // @ResponseSchema(ResponseScan) - // @ResponseSchema(ResponseTrackScan) - // @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) - // @OnUndefined(ScanNotFoundError) - // @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) - // async getOne(@Param('id') id: number) { - // let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] }) - // if (!scan) { throw new ScanNotFoundError(); } - // return scan.toResponse(); - // } + @Get('/:id') + @Authorized("STATION:GET") + @ResponseSchema(ResponseScanStation) + @ResponseSchema(ScanStationNotFoundError, { statusCode: 404 }) + @OnUndefined(ScanStationNotFoundError) + @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + async getOne(@Param('id') id: number) { + let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] }) + if (!scan) { throw new ScanStationNotFoundError(); } + return scan.toResponse(); + } // @Post() // @Authorized("SCAN:CREATE") diff --git a/src/errors/ScanStationErrors.ts b/src/errors/ScanStationErrors.ts new file mode 100644 index 0000000..8cc6d88 --- /dev/null +++ b/src/errors/ScanStationErrors.ts @@ -0,0 +1,25 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw, when a non-existant scan station get's loaded. + */ +export class ScanStationNotFoundError extends NotFoundError { + @IsString() + name = "ScanStationNotFoundError" + + @IsString() + message = "The scan station you provided couldn't be located in the system. \n Please check your request." +} + +/** + * Error to throw when two scan stations' ids don't match. + * Usually occurs when a user tries to change a scan station's id. + */ +export class ScanStationIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "ScanStationIdsNotMatchingError" + + @IsString() + message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!" +} \ No newline at end of file From 2628f6965165de7abbc100ed729b8d1f9c8c422d Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 18:39:38 +0100 Subject: [PATCH 22/47] Implemented scan station creation ref #67 --- src/controllers/ScanStationController.ts | 25 ++++++++++++++---------- src/models/actions/CreateScanStation.ts | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index fc13028..536c265 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -1,7 +1,9 @@ -import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; +import { Authorized, Body, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { ScanStationNotFoundError } from '../errors/ScanStationErrors'; +import { TrackNotFoundError } from '../errors/TrackErrors'; +import { CreateScanStation } from '../models/actions/CreateScanStation'; import { ScanStation } from '../models/entities/ScanStation'; import { ResponseScanStation } from '../models/responses/ResponseScanStation'; @@ -42,15 +44,18 @@ export class ScanStationController { return scan.toResponse(); } - // @Post() - // @Authorized("SCAN:CREATE") - // @ResponseSchema(ResponseScan) - // @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) - // async post(@Body({ validate: true }) createScan: CreateScan) { - // let scan = await createScan.toScan(); - // scan = await this.scanRepository.save(scan); - // return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); - // } + @Post() + @Authorized("STATION:CREATE") + @ResponseSchema(ResponseScanStation) + @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + async post(@Body({ validate: true }) createStation: CreateScanStation) { + let newStation = await createStation.toEntity(); + const station = await this.stationRepository.save(newStation); + let responseStation = (await this.stationRepository.findOne({ id: station.id }, { relations: ['track'] })).toResponse(); + responseStation.key = newStation.cleartextkey; + return responseStation; + } // @Put('/:id') // @Authorized("SCAN:UPDATE") diff --git a/src/models/actions/CreateScanStation.ts b/src/models/actions/CreateScanStation.ts index fd1d8ad..edd19cc 100644 --- a/src/models/actions/CreateScanStation.ts +++ b/src/models/actions/CreateScanStation.ts @@ -10,7 +10,7 @@ import { Track } from '../entities/Track'; /** * This classed is used to create a new StatsClient entity from a json body (post request). */ -export class CreateStatsClient { +export class CreateScanStation { /** * The new client's description. */ From 9b9ee702882730bc765d4e684ff85ec9e9b1ceb1 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 18:48:58 +0100 Subject: [PATCH 23/47] Implemented cascading station deletion ref #67 --- src/controllers/ScanStationController.ts | 42 +++++++++++++++--------- src/errors/ScanStationErrors.ts | 13 +++++++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index 536c265..d7b2095 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -1,11 +1,13 @@ -import { Authorized, Body, Get, JsonController, OnUndefined, Param, Post } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { ScanStationNotFoundError } from '../errors/ScanStationErrors'; +import { ScanStationHasScansError, ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { TrackNotFoundError } from '../errors/TrackErrors'; import { CreateScanStation } from '../models/actions/CreateScanStation'; import { ScanStation } from '../models/entities/ScanStation'; +import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseScanStation } from '../models/responses/ResponseScanStation'; +import { ScanController } from './ScanController'; @JsonController('/stations') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -79,18 +81,28 @@ export class ScanStationController { // return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse(); // } - // @Delete('/:id') - // @Authorized("SCAN:DELETE") - // @ResponseSchema(ResponseScan) - // @ResponseSchema(ResponseEmpty, { statusCode: 204 }) - // @OnUndefined(204) - // @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) - // async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { - // let scan = await this.scanRepository.findOne({ id: id }); - // if (!scan) { return null; } - // const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ["runner"] }); + @Delete('/:id') + @Authorized("STATION:DELETE") + @ResponseSchema(ResponseScanStation) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(ScanStationHasScansError, { statusCode: 406 }) + @OnUndefined(204) + @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + let station = await this.stationRepository.findOne({ id: id }); + if (!station) { return null; } - // await this.scanRepository.delete(scan); - // return responseScan.toResponse(); - // } + const stationScans = (await this.stationRepository.findOne({ id: station.id }, { relations: ["scans"] })).scans; + if (stationScans.length != 0 && !force) { + throw new ScanStationHasScansError(); + } + const scanController = new ScanController; + for (let scan of stationScans) { + scanController.remove(scan.id, force); + } + + const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track", "scans"] }); + await this.stationRepository.delete(station); + return responseStation.toResponse(); + } } diff --git a/src/errors/ScanStationErrors.ts b/src/errors/ScanStationErrors.ts index 8cc6d88..c013cf5 100644 --- a/src/errors/ScanStationErrors.ts +++ b/src/errors/ScanStationErrors.ts @@ -22,4 +22,15 @@ export class ScanStationIdsNotMatchingError extends NotAcceptableError { @IsString() message = "The ids don't match! \n And if you wanted to change a scan station's id: This isn't allowed!" -} \ No newline at end of file +} + +/** + * Error to throw when a station still has scans associated. + */ +export class ScanStationHasScansError extends NotAcceptableError { + @IsString() + name = "ScanStationHasScansError" + + @IsString() + message = "This station still has scans associated with it. \n If you want to delete this station with all it's scans add `?force` to your query." +} From 9776a35f9f31bcfaedd44a95b76c3b630179213f Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 18:53:09 +0100 Subject: [PATCH 24/47] Track deletion now recognizes associated stations ref #67 --- src/controllers/ScanStationController.ts | 2 +- src/controllers/TrackController.ts | 16 +++++++++++++--- src/errors/TrackErrors.ts | 8 ++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index d7b2095..d6edcd3 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -101,7 +101,7 @@ export class ScanStationController { scanController.remove(scan.id, force); } - const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track", "scans"] }); + const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] }); await this.stationRepository.delete(station); return responseStation.toResponse(); } diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index f03718f..094756c 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -1,12 +1,13 @@ -import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors"; +import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors"; import { CreateTrack } from '../models/actions/CreateTrack'; import { UpdateTrack } from '../models/actions/UpdateTrack'; import { Track } from '../models/entities/Track'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseTrack } from '../models/responses/ResponseTrack'; +import { ScanStationController } from './ScanStationController'; @JsonController('/tracks') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -85,10 +86,19 @@ export class TrackController { @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @OnUndefined(204) @OpenAPI({ description: "Delete the track whose id you provided.
If no track with this id exists it will just return 204(no content)." }) - async remove(@Param("id") id: number) { + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { let track = await this.trackRepository.findOne({ id: id }); if (!track) { return null; } + const trackStations = (await this.trackRepository.findOne({ id: id }, { relations: ["stations"] })).stations; + if (trackStations.length != 0 && !force) { + throw new TrackHasScanStationsError(); + } + const scanController = new ScanStationController; + for (let station of trackStations) { + scanController.remove(station.id, force); + } + await this.trackRepository.delete(track); return new ResponseTrack(track); } diff --git a/src/errors/TrackErrors.ts b/src/errors/TrackErrors.ts index e3d1902..a07f643 100644 --- a/src/errors/TrackErrors.ts +++ b/src/errors/TrackErrors.ts @@ -33,4 +33,12 @@ export class TrackLapTimeCantBeNegativeError extends NotAcceptableError { @IsString() message = "The minimum lap time you provided is negative - That isn't possible. \n If you wanted to disable it: Just set it to 0/null." +} + +export class TrackHasScanStationsError extends NotAcceptableError { + @IsString() + name = "TrackHasScanStationsError" + + @IsString() + message = "This track still has stations associated with it. \n If you want to delete this track with all it's stations and scans add `?force` to your query." } \ No newline at end of file From 3f23e4f1f14d07a252a49ed17bfb9ef680feb305 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 19:18:26 +0100 Subject: [PATCH 25/47] Added scan get tests ref #67 --- src/tests/scans/scans_get.spec.ts | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/tests/scans/scans_get.spec.ts diff --git a/src/tests/scans/scans_get.spec.ts b/src/tests/scans/scans_get.spec.ts new file mode 100644 index 0000000..943a884 --- /dev/null +++ b/src/tests/scans/scans_get.spec.ts @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('GET /api/scans sucessfully', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/scans', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/tracks illegally', () => { + it('get for non-existant track should return 404', async () => { + const res = await axios.get(base + '/api/scans/-1', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + getting scans', () => { + let added_org; + let added_runner; + let added_scan; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('correct distance and runner input should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 1000 + }, axios_config); + added_scan = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('check if scans was added (no parameter validation)', async () => { + const res = await axios.get(base + '/api/scans/' + added_scan.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file From 324d5709e3a11ae2f6e9a7532fe13d54be3d4d6f Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 19:19:21 +0100 Subject: [PATCH 26/47] Added tmp files to gitignore ref #67 --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ca4167..448b818 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,5 @@ build *.sqlite-jurnal /docs lib -/oss-attribution \ No newline at end of file +/oss-attribution +*.tmp \ No newline at end of file From 09b37f0ff23f1cfc05000648d567801ef2aba137 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 19:36:57 +0100 Subject: [PATCH 27/47] Fixed typo ref #67 --- src/tests/scans/scans_get.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/scans/scans_get.spec.ts b/src/tests/scans/scans_get.spec.ts index 943a884..da7d737 100644 --- a/src/tests/scans/scans_get.spec.ts +++ b/src/tests/scans/scans_get.spec.ts @@ -22,7 +22,7 @@ describe('GET /api/scans sucessfully', () => { }); }); // --------------- -describe('GET /api/tracks illegally', () => { +describe('GET /api/scans illegally', () => { it('get for non-existant track should return 404', async () => { const res = await axios.get(base + '/api/scans/-1', axios_config); expect(res.status).toEqual(404); From 4f01baaa23c658561a0f34046caa4c1d82cb7773 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 19:37:15 +0100 Subject: [PATCH 28/47] Added the enabled flag for scanstations ref #67 --- src/controllers/ScanController.ts | 3 +- src/controllers/ScanStationController.ts | 40 ++++++++++----------- src/models/actions/CreateScanStation.ts | 10 +++++- src/models/actions/UpdateScanStation.ts | 39 ++++++++++++++++++++ src/models/entities/ScanStation.ts | 8 +++++ src/models/responses/ResponseScanStation.ts | 8 +++++ 6 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 src/models/actions/UpdateScanStation.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 6674f36..c18a0eb 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -4,6 +4,7 @@ import { getConnectionManager, Repository } from 'typeorm'; import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors'; import { CreateScan } from '../models/actions/CreateScan'; +import { CreateTrackScan } from '../models/actions/CreateTrackScan'; import { UpdateScan } from '../models/actions/UpdateScan'; import { Scan } from '../models/entities/Scan'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; @@ -53,7 +54,7 @@ export class ScanController { @Authorized("SCAN:CREATE") @ResponseSchema(ResponseScan) @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) - async post(@Body({ validate: true }) createScan: CreateScan) { + async post(@Body({ validate: true }) createScan: CreateScan | CreateTrackScan) { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index d6edcd3..e85f31e 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -1,9 +1,10 @@ -import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { ScanStationHasScansError, ScanStationNotFoundError } from '../errors/ScanStationErrors'; +import { ScanStationHasScansError, ScanStationIdsNotMatchingError, ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { TrackNotFoundError } from '../errors/TrackErrors'; import { CreateScanStation } from '../models/actions/CreateScanStation'; +import { UpdateScanStation } from '../models/actions/UpdateScanStation'; import { ScanStation } from '../models/entities/ScanStation'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseScanStation } from '../models/responses/ResponseScanStation'; @@ -59,27 +60,26 @@ export class ScanStationController { return responseStation; } - // @Put('/:id') - // @Authorized("SCAN:UPDATE") - // @ResponseSchema(ResponseScan) - // @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) - // @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) - // @ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 }) - // @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) - // async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) { - // let oldScan = await this.scanRepository.findOne({ id: id }); + @Put('/:id') + @Authorized("STATION:UPDATE") + @ResponseSchema(ResponseScanStation) + @ResponseSchema(ScanStationNotFoundError, { statusCode: 404 }) + @ResponseSchema(ScanStationIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update the station whose id you provided.
Please remember that only the description and enabled state can be changed." }) + async put(@Param('id') id: number, @Body({ validate: true }) station: UpdateScanStation) { + let oldStation = await this.stationRepository.findOne({ id: id }); - // if (!oldScan) { - // throw new ScanNotFoundError(); - // } + if (!oldStation) { + throw new ScanStationNotFoundError(); + } - // if (oldScan.id != scan.id) { - // throw new ScanIdsNotMatchingError(); - // } + if (oldStation.id != station.id) { + throw new ScanStationNotFoundError(); + } - // await this.scanRepository.save(await scan.updateScan(oldScan)); - // return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse(); - // } + await this.stationRepository.save(await station.updateStation(oldStation)); + return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse(); + } @Delete('/:id') @Authorized("STATION:DELETE") diff --git a/src/models/actions/CreateScanStation.ts b/src/models/actions/CreateScanStation.ts index edd19cc..76d414a 100644 --- a/src/models/actions/CreateScanStation.ts +++ b/src/models/actions/CreateScanStation.ts @@ -1,5 +1,5 @@ import * as argon2 from "argon2"; -import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; +import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; import crypto from 'crypto'; import { getConnection } from 'typeorm'; import * as uuid from 'uuid'; @@ -26,6 +26,13 @@ export class CreateScanStation { @IsPositive() track: number; + /** + * Is this station enabled? + */ + @IsBoolean() + @IsOptional() + enabled?: boolean = true; + /** * Converts this to a ScanStation entity. */ @@ -33,6 +40,7 @@ export class CreateScanStation { let newStation: ScanStation = new ScanStation(); newStation.description = this.description; + newStation.enabled = this.enabled; newStation.track = await this.getTrack(); let newUUID = uuid.v4().toUpperCase(); diff --git a/src/models/actions/UpdateScanStation.ts b/src/models/actions/UpdateScanStation.ts new file mode 100644 index 0000000..24039a0 --- /dev/null +++ b/src/models/actions/UpdateScanStation.ts @@ -0,0 +1,39 @@ +import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator'; +import { ScanStation } from '../entities/ScanStation'; + +/** + * This classed is used to create a new StatsClient entity from a json body (post request). + */ +export class UpdateScanStation { + /** + * The updated station's id. + * This shouldn't have changed but it is here in case anyone ever wants to enable id changes (whyever they would want to). + */ + @IsInt() + id: number; + + /** + * The updated station's description. + */ + @IsString() + @IsOptional() + description?: string; + + /** + * Is this station enabled? + */ + @IsBoolean() + @IsOptional() + enabled?: boolean = true; + + /** + * Converts this to a ScanStation entity. + * TODO: + */ + public async updateStation(station: ScanStation): Promise { + station.description = this.description; + station.enabled = this.enabled; + + return station; + } +} \ No newline at end of file diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index 93fde64..38f7803 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -1,4 +1,5 @@ import { + IsBoolean, IsInt, IsNotEmpty, IsOptional, @@ -70,6 +71,13 @@ export class ScanStation { @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + /** + * Is this station enabled? + */ + @Column({ nullable: true }) + @IsBoolean() + enabled?: boolean = true; + /** * Turns this entity into it's response class. */ diff --git a/src/models/responses/ResponseScanStation.ts b/src/models/responses/ResponseScanStation.ts index 6b147c9..7d7dc48 100644 --- a/src/models/responses/ResponseScanStation.ts +++ b/src/models/responses/ResponseScanStation.ts @@ -1,5 +1,6 @@ import { + IsBoolean, IsInt, IsNotEmpty, @@ -48,6 +49,12 @@ export class ResponseScanStation { @IsNotEmpty() track: ResponseTrack; + /** + * Is this station enabled? + */ + @IsBoolean() + enabled?: boolean = true; + /** * Creates a ResponseStatsClient object from a statsClient. * @param client The statsClient the response shall be build for. @@ -58,5 +65,6 @@ export class ResponseScanStation { this.prefix = station.prefix; this.key = "Only visible on creation."; this.track = station.track; + this.enabled = station.enabled; } } From 7387f700fb75681e7e1e4be766db436a816db489 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 19:46:20 +0100 Subject: [PATCH 29/47] Added alias for posting track scans ref #67 --- src/controllers/ScanController.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index c18a0eb..7b61b02 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -54,12 +54,20 @@ export class ScanController { @Authorized("SCAN:CREATE") @ResponseSchema(ResponseScan) @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) - async post(@Body({ validate: true }) createScan: CreateScan | CreateTrackScan) { + async post(@Body({ validate: true }) createScan: CreateScan) { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse(); } + @Post("/trackscans") + @Authorized("SCAN:CREATE") + @ResponseSchema(ResponseScan) + @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans' }) + async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) { + return this.post(createScan); + } + @Put('/:id') @Authorized("SCAN:UPDATE") @ResponseSchema(ResponseScan) From a434173b545f3a7d97c62f00ff4daaa5ef46e71f Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:04:15 +0100 Subject: [PATCH 30/47] Added scan station get tests ref #67 --- .../scanstations/scanstations_get.spec.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/tests/scanstations/scanstations_get.spec.ts diff --git a/src/tests/scanstations/scanstations_get.spec.ts b/src/tests/scanstations/scanstations_get.spec.ts new file mode 100644 index 0000000..7280c31 --- /dev/null +++ b/src/tests/scanstations/scanstations_get.spec.ts @@ -0,0 +1,59 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('GET /api/stations sucessfully', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/stations', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/stations illegally', () => { + it('get for non-existant track should return 404', async () => { + const res = await axios.get(base + '/api/stations/-1', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + getting stations', () => { + let added_track; + let added_station; + it('creating a track should return 200', async () => { + const res1 = await axios.post(base + '/api/tracks', { + "name": "test123", + "distance": 123 + }, axios_config); + added_track = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('correct description and track input for station creation return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id, + "description": "I am but a simple test." + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('check if station was added (no parameter validation)', async () => { + const res = await axios.get(base + '/api/stations/' + added_station.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file From 5510cbb8e9e53dec141dd0e31168321e8253e121 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:16:14 +0100 Subject: [PATCH 31/47] Added scan station add tests ref #67 --- .../scanstations/scanstations_add.spec.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/tests/scanstations/scanstations_add.spec.ts diff --git a/src/tests/scanstations/scanstations_add.spec.ts b/src/tests/scanstations/scanstations_add.spec.ts new file mode 100644 index 0000000..e4c3570 --- /dev/null +++ b/src/tests/scanstations/scanstations_add.spec.ts @@ -0,0 +1,113 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + + +describe('POST /api/stations illegally', () => { + it('no track input should return 400', async () => { + const res = await axios.post(base + '/api/stations', { + "description": "string", + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('illegal track input should return 404', async () => { + const res = await axios.post(base + '/api/stations', { + "description": "string", + "track": -1 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/stations successfully', () => { + let added_track; + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.prefix; + delete res.data.key; + expect(res.data).toEqual({ + "track": added_track, + "description": null, + "enabled": true + }); + }); + it('creating a station with all parameters (optional set to true/empty) should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id, + "enabled": true, + "description": null + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.prefix; + delete res.data.key; + expect(res.data).toEqual({ + "track": added_track, + "description": null, + "enabled": true + }); + }); + it('creating a disabled station with all parameters (optional set to true/empty) should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id, + "enabled": false, + "description": null + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.prefix; + delete res.data.key; + expect(res.data).toEqual({ + "track": added_track, + "description": null, + "enabled": false + }); + }); + it('creating a station with all parameters (optional set) should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id, + "enabled": true, + "description": "test station for testing" + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.prefix; + delete res.data.key; + expect(res.data).toEqual({ + "track": added_track, + "description": "test station for testing", + "enabled": true + }); + }); +}); From c8f941a779d90e6661efca5aeeadc44fc612eb50 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:22:58 +0100 Subject: [PATCH 32/47] Fixed wrong error getting thrown ref #67 --- src/controllers/ScanStationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index e85f31e..7ad2630 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -74,7 +74,7 @@ export class ScanStationController { } if (oldStation.id != station.id) { - throw new ScanStationNotFoundError(); + throw new ScanStationIdsNotMatchingError(); } await this.stationRepository.save(await station.updateStation(oldStation)); From ccf2a3b6173744bcb48d066e3430cd8305227923 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:31:29 +0100 Subject: [PATCH 33/47] Added scan station update tests ref #67 --- .../scanstations/scanstations_update.spec.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/tests/scanstations/scanstations_update.spec.ts diff --git a/src/tests/scanstations/scanstations_update.spec.ts b/src/tests/scanstations/scanstations_update.spec.ts new file mode 100644 index 0000000..6593a13 --- /dev/null +++ b/src/tests/scanstations/scanstations_update.spec.ts @@ -0,0 +1,103 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('adding + updating illegally', () => { + let added_track; + let added_station; + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('updateing id should return 406', async () => { + const res2 = await axios.put(base + '/api/stations/' + added_station.id, { + "id": added_station.id + 1 + }, axios_config); + expect(res2.status).toEqual(406); + expect(res2.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating successfilly', () => { + let added_track; + let added_station; + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('updateing nothing should return 200', async () => { + const res = await axios.put(base + '/api/stations/' + added_station.id, { + "id": added_station.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.key; + delete added_station.key; + expect(res.data).toEqual(added_station); + }); + it('updateing description should return 200', async () => { + const res = await axios.put(base + '/api/stations/' + added_station.id, { + "id": added_station.id, + "description": "Hello there! General stationi you're a scanning one." + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.description).toEqual("Hello there! General stationi you're a scanning one."); + }); + it('updateing enabled to false should return 200', async () => { + const res = await axios.put(base + '/api/stations/' + added_station.id, { + "id": added_station.id, + "enabled": false + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.enabled).toEqual(false); + }); + it('updateing enabled to true should return 200', async () => { + const res = await axios.put(base + '/api/stations/' + added_station.id, { + "id": added_station.id, + "enabled": true + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + expect(res.data.enabled).toEqual(true); + }); +}); From a4f88c78f4d863e734c8c4b91f0165bbf36ae25d Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:34:36 +0100 Subject: [PATCH 34/47] Added scan station delete tests ref #67 --- .../scanstations/scanstations_delete.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/tests/scanstations/scanstations_delete.spec.ts diff --git a/src/tests/scanstations/scanstations_delete.spec.ts b/src/tests/scanstations/scanstations_delete.spec.ts new file mode 100644 index 0000000..61491c8 --- /dev/null +++ b/src/tests/scanstations/scanstations_delete.spec.ts @@ -0,0 +1,58 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +// --------------- +describe('DELETE track', () => { + let added_track; + let added_station; + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('delete station', async () => { + const res2 = await axios.delete(base + '/api/stations/' + added_station.id, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + delete res2.data.key; + delete added_station.key; + expect(res2.data).toEqual(added_station); + }); + it('check if station really was deleted', async () => { + const res3 = await axios.get(base + '/api/stations/' + added_station.id, axios_config); + expect(res3.status).toEqual(404); + expect(res3.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE station (non-existant)', () => { + it('delete', async () => { + const res2 = await axios.delete(base + '/api/stations/0', axios_config); + expect(res2.status).toEqual(204); + }); +}); From 09ab638239d37746f3acf22b7cb8762c66a66b89 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Thu, 7 Jan 2021 20:34:36 +0100 Subject: [PATCH 35/47] Added scan station delete tests ref #67 --- .../scanstations/scanstations_delete.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/tests/scanstations/scanstations_delete.spec.ts diff --git a/src/tests/scanstations/scanstations_delete.spec.ts b/src/tests/scanstations/scanstations_delete.spec.ts new file mode 100644 index 0000000..ad425c7 --- /dev/null +++ b/src/tests/scanstations/scanstations_delete.spec.ts @@ -0,0 +1,58 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +// --------------- +describe('DELETE station', () => { + let added_track; + let added_station; + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('delete station', async () => { + const res2 = await axios.delete(base + '/api/stations/' + added_station.id, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + delete res2.data.key; + delete added_station.key; + expect(res2.data).toEqual(added_station); + }); + it('check if station really was deleted', async () => { + const res3 = await axios.get(base + '/api/stations/' + added_station.id, axios_config); + expect(res3.status).toEqual(404); + expect(res3.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE station (non-existant)', () => { + it('delete', async () => { + const res2 = await axios.delete(base + '/api/stations/0', axios_config); + expect(res2.status).toEqual(204); + }); +}); From 102a860ba350783a08dd20fe66aa4c4b96655dfc Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 16:47:52 +0100 Subject: [PATCH 36/47] Added scan delete tests ref #67 --- src/tests/scans/scans_delete.spec.ts | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/tests/scans/scans_delete.spec.ts diff --git a/src/tests/scans/scans_delete.spec.ts b/src/tests/scans/scans_delete.spec.ts new file mode 100644 index 0000000..e88e812 --- /dev/null +++ b/src/tests/scans/scans_delete.spec.ts @@ -0,0 +1,68 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +// --------------- + +describe('DELETE scan', () => { + let added_org; + let added_runner; + let added_scan; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('correct distance and runner input should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 1000 + }, axios_config); + added_scan = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete scan', async () => { + const res2 = await axios.delete(base + '/api/scans/' + added_scan.id, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + expect(res2.data).toEqual(added_scan); + }); + it('check if scan really was deleted', async () => { + const res3 = await axios.get(base + '/api/scans/' + added_scan.id, axios_config); + expect(res3.status).toEqual(404); + expect(res3.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE scan (non-existant)', () => { + it('delete', async () => { + const res2 = await axios.delete(base + '/api/scans/0', axios_config); + expect(res2.status).toEqual(204); + }); +}); From 0c27df7754609d5f0c91db339f71738696f93491 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 17:27:56 +0100 Subject: [PATCH 37/47] Added scan add tests ref #67 --- src/tests/scans/scans_add.spec.ts | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/tests/scans/scans_add.spec.ts diff --git a/src/tests/scans/scans_add.spec.ts b/src/tests/scans/scans_add.spec.ts new file mode 100644 index 0000000..db4464e --- /dev/null +++ b/src/tests/scans/scans_add.spec.ts @@ -0,0 +1,135 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + + +describe('POST /api/scans illegally', () => { + let added_org; + let added_runner; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('no input should return 400', async () => { + const res = await axios.post(base + '/api/scans', null, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('no distance should return 400', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('illegal distance input should return 400', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": -1 + }, axios_config); + expect(res.status).toEqual(400); + expect(res.headers['content-type']).toContain("application/json") + }); + it('invalid runner input should return 404', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": 999999999999999999999999, + "distance": 100 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/scans successfully', () => { + let added_org; + let added_runner; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res2.data.group; + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('creating a scan with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": true + }); + }); + it('creating a valid scan should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200, + "valid": true + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": true + }); + }); + it('creating a invalid scan should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200, + "valid": false + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": false + }); + }); +}); From 975ad50afc87280fedbc4a9228ee9d071cb14c45 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 17:42:05 +0100 Subject: [PATCH 38/47] Added scan update tests ref #67 --- src/tests/scans/scans_update.spec.ts | 174 +++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/tests/scans/scans_update.spec.ts diff --git a/src/tests/scans/scans_update.spec.ts b/src/tests/scans/scans_update.spec.ts new file mode 100644 index 0000000..b4ff86c --- /dev/null +++ b/src/tests/scans/scans_update.spec.ts @@ -0,0 +1,174 @@ +import axios from 'axios'; +import { config } from '../../config'; +const base = "http://localhost:" + config.internal_port + +let access_token; +let axios_config; + +beforeAll(async () => { + const res = await axios.post(base + '/api/auth/login', { username: "demo", password: "demo" }); + access_token = res.data["access_token"]; + axios_config = { + headers: { "authorization": "Bearer " + access_token }, + validateStatus: undefined + }; +}); + +describe('adding + updating illegally', () => { + let added_org; + let added_runner; + let added_scan; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res2.data.group; + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('creating a scan with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_scan = res.data; + }); + it('updating empty should return 400', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, null, axios_config); + expect(res2.status).toEqual(400); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('updating with wrong id should return 406', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id + 1, + "runner": added_runner.id, + "distance": added_scan.distance + }, axios_config); + expect(res2.status).toEqual(406); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('update with negative distance should return 400', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id, + "runner": added_runner.id, + "distance": -1 + }, axios_config); + expect(res2.status).toEqual(400); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('update with invalid runner id should return 404', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id, + "runner": 9999999999999999999999999, + "distance": 123 + }, axios_config); + expect(res2.status).toEqual(404); + expect(res2.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating successfilly', () => { + let added_org; + let added_runner; + let added_runner2; + let added_scan; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res2.data.group; + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('creating a scan with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200 + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + added_scan = res.data; + }); + it('valid distance update should return 200', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id, + "runner": added_runner.id, + "distance": 100 + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + expect(res2.data).toEqual({ + "id": added_scan.id, + "runner": added_runner, + "distance": 100, + "valid": true + + }); + }); + it('valid valid update should return 200', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id, + "runner": added_runner.id, + "distance": 100, + "valid": false + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + expect(res2.data).toEqual({ + "id": added_scan.id, + "runner": added_runner, + "distance": 100, + "valid": false + }); + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res2.data.group; + added_runner2 = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('valid runner update should return 200', async () => { + const res2 = await axios.put(base + '/api/scans/' + added_scan.id, { + "id": added_scan.id, + "runner": added_runner2.id, + "distance": added_scan.distance + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual({ + "id": added_scan.id, + "runner": added_runner2, + "distance": added_scan.distance, + "valid": added_scan.valid + }); + }); +}); From db6fdf6baf074830a457e0e5ab56421bfc0ce4e3 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 17:50:29 +0100 Subject: [PATCH 39/47] Implemented scan auth middleware ref #67 --- src/controllers/ScanController.ts | 5 ++- src/middlewares/ScanAuth.ts | 68 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/middlewares/ScanAuth.ts diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 7b61b02..6512a9e 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -1,8 +1,9 @@ -import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers'; +import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam, UseBefore } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors'; +import StatsAuth from '../middlewares/StatsAuth'; import { CreateScan } from '../models/actions/CreateScan'; import { CreateTrackScan } from '../models/actions/CreateTrackScan'; import { UpdateScan } from '../models/actions/UpdateScan'; @@ -61,7 +62,7 @@ export class ScanController { } @Post("/trackscans") - @Authorized("SCAN:CREATE") + @UseBefore(StatsAuth) @ResponseSchema(ResponseScan) @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans' }) async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) { diff --git a/src/middlewares/ScanAuth.ts b/src/middlewares/ScanAuth.ts new file mode 100644 index 0000000..7b16420 --- /dev/null +++ b/src/middlewares/ScanAuth.ts @@ -0,0 +1,68 @@ +import * as argon2 from "argon2"; +import { Request, Response } from 'express'; +import { getConnectionManager } from 'typeorm'; +import { ScanStation } from '../models/entities/ScanStation'; +import authchecker from './authchecker'; + +/** + * This middleware handels the authentification of scan station api tokens. + * The tokens have to be provided via Bearer auth header. + * @param req Express request object. + * @param res Express response object. + * @param next Next function to call on success. + */ +const ScanAuth = async (req: Request, res: Response, next: () => void) => { + let provided_token: string = req.headers["authorization"]; + if (provided_token == "" || provided_token === undefined || provided_token === null) { + res.status(401).send("No api token provided."); + return; + } + + try { + provided_token = provided_token.replace("Bearer ", ""); + } catch (error) { + res.status(401).send("No valid jwt or api token provided."); + return; + } + + let prefix = ""; + try { + prefix = provided_token.split(".")[0]; + } + finally { + if (prefix == "" || prefix == undefined || prefix == null) { + res.status(401).send("Api token non-existant or invalid syntax."); + return; + } + } + + const station = await getConnectionManager().get().getRepository(ScanStation).findOne({ prefix: prefix }); + if (!station) { + let user_authorized = false; + try { + let action = { request: req, response: res, context: null, next: next } + user_authorized = await authchecker(action, ["SCAN:CREATE"]); + } + finally { + if (user_authorized == false) { + res.status(401).send("Api token non-existant or invalid syntax."); + return; + } + else { + next(); + } + } + } + else { + if (station.enabled == false) { + res.status(401).send("Station disabled."); + } + if (!(await argon2.verify(station.key, provided_token))) { + res.status(401).send("Api token invalid."); + return; + } + + next(); + } +} +export default ScanAuth; \ No newline at end of file From cf86520faeaf5e4ac2e7b3a3606d2ea8317d2eb6 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 18:08:13 +0100 Subject: [PATCH 40/47] Fixed wrong auth type being used ref #67 --- src/controllers/ScanController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 6512a9e..ce25e16 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -3,7 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors'; -import StatsAuth from '../middlewares/StatsAuth'; +import ScanAuth from '../middlewares/ScanAuth'; import { CreateScan } from '../models/actions/CreateScan'; import { CreateTrackScan } from '../models/actions/CreateTrackScan'; import { UpdateScan } from '../models/actions/UpdateScan'; @@ -52,7 +52,7 @@ export class ScanController { } @Post() - @Authorized("SCAN:CREATE") + @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) async post(@Body({ validate: true }) createScan: CreateScan) { @@ -62,7 +62,7 @@ export class ScanController { } @Post("/trackscans") - @UseBefore(StatsAuth) + @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans' }) async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) { From a005945e9e0b17d754ffc52dc5f5c2c6a010f3eb Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 18:09:47 +0100 Subject: [PATCH 41/47] Added scan add tests with the station based auth ref #67 --- src/tests/scans/scans_add.spec.ts | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/tests/scans/scans_add.spec.ts b/src/tests/scans/scans_add.spec.ts index db4464e..c08ee01 100644 --- a/src/tests/scans/scans_add.spec.ts +++ b/src/tests/scans/scans_add.spec.ts @@ -133,3 +133,99 @@ describe('POST /api/scans successfully', () => { }); }); }); +// --------------- +describe('POST /api/scans successfully via scan station', () => { + let added_org; + let added_runner; + let added_track; + let added_station; + it('creating a new org with just a name should return 200', async () => { + const res1 = await axios.post(base + '/api/organisations', { + "name": "test123" + }, axios_config); + added_org = res1.data + expect(res1.status).toEqual(200); + expect(res1.headers['content-type']).toContain("application/json") + }); + it('creating a new runner with only needed params should return 200', async () => { + const res2 = await axios.post(base + '/api/runners', { + "firstname": "first", + "lastname": "last", + "group": added_org.id + }, axios_config); + delete res2.data.group; + added_runner = res2.data; + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + }); + it('creating a track with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/tracks', { + "name": "testtrack", + "distance": 200, + }, axios_config); + added_track = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a station with minimum parameters should return 200', async () => { + const res = await axios.post(base + '/api/stations', { + "track": added_track.id + }, axios_config); + added_station = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); + it('creating a scan with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200 + }, { + headers: { "authorization": "Bearer " + added_station.key }, + validateStatus: undefined + }); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": true + }); + }); + it('creating a valid scan should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200, + "valid": true + }, { + headers: { "authorization": "Bearer " + added_station.key }, + validateStatus: undefined + }); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": true + }); + }); + it('creating a invalid scan should return 200', async () => { + const res = await axios.post(base + '/api/scans', { + "runner": added_runner.id, + "distance": 200, + "valid": false + }, { + headers: { "authorization": "Bearer " + added_station.key }, + validateStatus: undefined + }); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + expect(res.data).toEqual({ + "runner": added_runner, + "distance": 200, + "valid": false + }); + }); +}); From ce8fed350ecc84d5abe8ffd6b0789c89334c5ec1 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 18:25:29 +0100 Subject: [PATCH 42/47] Updated OPENAPI Descriptions for the new controllers ref #67 --- src/controllers/ScanController.ts | 16 +++++++++------- src/controllers/ScanStationController.ts | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index ce25e16..c716b50 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -28,10 +28,10 @@ export class ScanController { @Authorized("SCAN:GET") @ResponseSchema(ResponseScan, { isArray: true }) @ResponseSchema(ResponseTrackScan, { isArray: true }) - @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) + @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the scan\'s runner\'s distance ran.' }) async getAll() { let responseScans: ResponseScan[] = new Array(); - const scans = await this.scanRepository.find({ relations: ['runner'] }); + const scans = await this.scanRepository.find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] }); scans.forEach(scan => { responseScans.push(scan.toResponse()); }); @@ -44,9 +44,9 @@ export class ScanController { @ResponseSchema(ResponseTrackScan) @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) @OnUndefined(ScanNotFoundError) - @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + @OpenAPI({ description: 'Lists all information about the scan whose id got provided. This includes the scan\'s runner\'s distance ran.' }) async getOne(@Param('id') id: number) { - let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner'] }) + let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'runner.scans', 'runner.scans.track'] }) if (!scan) { throw new ScanNotFoundError(); } return scan.toResponse(); } @@ -54,7 +54,8 @@ export class ScanController { @Post() @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) - @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: 'Create a new scan.
Please remeber to provide the scan\'s runner\'s id and distance for normal scans.' }) async post(@Body({ validate: true }) createScan: CreateScan) { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); @@ -64,6 +65,7 @@ export class ScanController { @Post("/trackscans") @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans' }) async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) { return this.post(createScan); @@ -75,7 +77,7 @@ export class ScanController { @ResponseSchema(ScanNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 }) - @OpenAPI({ description: "Update the runner whose id you provided.
Please remember that ids can't be changed." }) + @OpenAPI({ description: "Update the scan whose id you provided.
Please remember that ids can't be changed and distances must be positive." }) async put(@Param('id') id: number, @Body({ validate: true }) scan: UpdateScan) { let oldScan = await this.scanRepository.findOne({ id: id }); @@ -96,7 +98,7 @@ export class ScanController { @ResponseSchema(ResponseScan) @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the scan whose id you provided.
If no scan with this id exists it will just return 204(no content).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { let scan = await this.scanRepository.findOne({ id: id }); if (!scan) { return null; } diff --git a/src/controllers/ScanStationController.ts b/src/controllers/ScanStationController.ts index 7ad2630..4e32971 100644 --- a/src/controllers/ScanStationController.ts +++ b/src/controllers/ScanStationController.ts @@ -25,7 +25,7 @@ export class ScanStationController { @Get() @Authorized("STATION:GET") @ResponseSchema(ResponseScanStation, { isArray: true }) - @OpenAPI({ description: 'Lists all scans (normal or track) from all runners.
This includes the runner\'s group and distance ran.' }) + @OpenAPI({ description: 'Lists all stations.
This includes their associated tracks.' }) async getAll() { let responseStations: ResponseScanStation[] = new Array(); const stations = await this.stationRepository.find({ relations: ['track'] }); @@ -40,7 +40,7 @@ export class ScanStationController { @ResponseSchema(ResponseScanStation) @ResponseSchema(ScanStationNotFoundError, { statusCode: 404 }) @OnUndefined(ScanStationNotFoundError) - @OpenAPI({ description: 'Lists all information about the runner whose id got provided.' }) + @OpenAPI({ description: 'Lists all information about the station whose id got provided.
This includes it\'s associated track.' }) async getOne(@Param('id') id: number) { let scan = await this.stationRepository.findOne({ id: id }, { relations: ['track'] }) if (!scan) { throw new ScanStationNotFoundError(); } @@ -51,7 +51,7 @@ export class ScanStationController { @Authorized("STATION:CREATE") @ResponseSchema(ResponseScanStation) @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) - @OpenAPI({ description: 'Create a new runner.
Please remeber to provide the runner\'s group\'s id.' }) + @OpenAPI({ description: 'Create a new station.
Please remeber to provide the station\'s track\'s id.
Please also remember that the station key is only visibe on creation.' }) async post(@Body({ validate: true }) createStation: CreateScanStation) { let newStation = await createStation.toEntity(); const station = await this.stationRepository.save(newStation); @@ -87,7 +87,7 @@ export class ScanStationController { @ResponseSchema(ResponseEmpty, { statusCode: 204 }) @ResponseSchema(ScanStationHasScansError, { statusCode: 406 }) @OnUndefined(204) - @OpenAPI({ description: 'Delete the runner whose id you provided.
If no runner with this id exists it will just return 204(no content).' }) + @OpenAPI({ description: 'Delete the station whose id you provided.
If no station with this id exists it will just return 204(no content).
If the station still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with/created by this station - please disable it instead).' }) async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { let station = await this.stationRepository.findOne({ id: id }); if (!station) { return null; } From 7728759bcd9cf311149ce80f356bdb027b402dd4 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 18:28:35 +0100 Subject: [PATCH 43/47] Added openapi sec scheme for the scan station auth ref #67 --- scripts/openapi_export.ts | 7 ++++++- src/controllers/ScanController.ts | 4 ++-- src/loaders/openapi.ts | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/openapi_export.ts b/scripts/openapi_export.ts index aec2c69..0902b22 100644 --- a/scripts/openapi_export.ts +++ b/scripts/openapi_export.ts @@ -48,7 +48,12 @@ const spec = routingControllersToSpec( "StatsApiToken": { "type": "http", "scheme": "bearer", - description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)." + description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats." + }, + "StationApiToken": { + "type": "http", + "scheme": "bearer", + description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans." } } }, diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index c716b50..ed7df91 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -55,7 +55,7 @@ export class ScanController { @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) - @OpenAPI({ description: 'Create a new scan.
Please remeber to provide the scan\'s runner\'s id and distance for normal scans.' }) + @OpenAPI({ description: 'Create a new scan.
Please remeber to provide the scan\'s runner\'s id and distance for normal scans.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async post(@Body({ validate: true }) createScan: CreateScan) { let scan = await createScan.toScan(); scan = await this.scanRepository.save(scan); @@ -66,7 +66,7 @@ export class ScanController { @UseBefore(ScanAuth) @ResponseSchema(ResponseScan) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) - @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans' }) + @OpenAPI({ description: 'Create a new track scan.
This is just a alias for posting /scans', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) { return this.post(createScan); } diff --git a/src/loaders/openapi.ts b/src/loaders/openapi.ts index 5ab892c..bd8318c 100644 --- a/src/loaders/openapi.ts +++ b/src/loaders/openapi.ts @@ -39,7 +39,12 @@ export default async (app: Application) => { "StatsApiToken": { "type": "http", "scheme": "bearer", - description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients)." + description: "Api token that can be obtained by creating a new stats client (post to /api/statsclients). Only valid for obtaining stats." + }, + "StationApiToken": { + "type": "http", + "scheme": "bearer", + description: "Api token that can be obtained by creating a new scan station (post to /api/stations). Only valid for creating scans." } } }, From c591c182b344cb09e237ae5046f1a662a5ed4c33 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 18:37:33 +0100 Subject: [PATCH 44/47] Updated comments ref #67 --- src/models/actions/CreateScan.ts | 5 ++++- src/models/actions/CreateScanStation.ts | 11 +++++++---- src/models/actions/UpdateScan.ts | 5 ++++- src/models/actions/UpdateScanStation.ts | 6 +++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/models/actions/CreateScan.ts b/src/models/actions/CreateScan.ts index ef6d0e4..e0d0efc 100644 --- a/src/models/actions/CreateScan.ts +++ b/src/models/actions/CreateScan.ts @@ -5,7 +5,7 @@ import { Runner } from '../entities/Runner'; import { Scan } from '../entities/Scan'; /** - * This classed is used to create a new Scan entity from a json body (post request). + * This class is used to create a new Scan entity from a json body (post request). */ export abstract class CreateScan { /** @@ -46,6 +46,9 @@ export abstract class CreateScan { return newScan; } + /** + * Gets a runner based on the runner id provided via this.runner. + */ public async getRunner(): Promise { const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); if (!runner) { diff --git a/src/models/actions/CreateScanStation.ts b/src/models/actions/CreateScanStation.ts index 76d414a..5d93b7c 100644 --- a/src/models/actions/CreateScanStation.ts +++ b/src/models/actions/CreateScanStation.ts @@ -8,19 +8,18 @@ import { ScanStation } from '../entities/ScanStation'; import { Track } from '../entities/Track'; /** - * This classed is used to create a new StatsClient entity from a json body (post request). + * This class is used to create a new StatsClient entity from a json body (post request). */ export class CreateScanStation { /** - * The new client's description. + * The new station's description. */ @IsString() @IsOptional() description?: string; /** - * The scan's associated track. - * This is used to determine the scan's distance. + * The station's associated track. */ @IsInt() @IsPositive() @@ -51,6 +50,10 @@ export class CreateScanStation { return newStation; } + /** + * Get's a track by it's id provided via this.track. + * Used to link the new station to a track. + */ public async getTrack(): Promise { const track = await getConnection().getRepository(Track).findOne({ id: this.track }); if (!track) { diff --git a/src/models/actions/UpdateScan.ts b/src/models/actions/UpdateScan.ts index a51ccea..00b375e 100644 --- a/src/models/actions/UpdateScan.ts +++ b/src/models/actions/UpdateScan.ts @@ -5,7 +5,7 @@ import { Runner } from '../entities/Runner'; import { Scan } from '../entities/Scan'; /** - * This classed is used to create a new Scan entity from a json body (post request). + * This class is used to update a Scan entity (via put request) */ export abstract class UpdateScan { /** @@ -49,6 +49,9 @@ export abstract class UpdateScan { return scan; } + /** + * Gets a runner based on the runner id provided via this.runner. + */ public async getRunner(): Promise { const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); if (!runner) { diff --git a/src/models/actions/UpdateScanStation.ts b/src/models/actions/UpdateScanStation.ts index 24039a0..a8ebc6a 100644 --- a/src/models/actions/UpdateScanStation.ts +++ b/src/models/actions/UpdateScanStation.ts @@ -2,7 +2,7 @@ import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator'; import { ScanStation } from '../entities/ScanStation'; /** - * This classed is used to create a new StatsClient entity from a json body (post request). + * This class is used to update a ScanStation entity (via put request) */ export class UpdateScanStation { /** @@ -27,8 +27,8 @@ export class UpdateScanStation { enabled?: boolean = true; /** - * Converts this to a ScanStation entity. - * TODO: + * Update a ScanStation entity based on this. + * @param station The station that shall be updated. */ public async updateStation(station: ScanStation): Promise { station.description = this.description; From c3b9e135b056edb108759e0d72c5a8d2d2079588 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 19:34:39 +0100 Subject: [PATCH 45/47] Finned node version for ci ref #67 --- .drone.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index f7393db..fd21759 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,6 @@ --- kind: pipeline -name: tests:node_latest +name: tests:node_14.0.0 clone: disable: true steps: @@ -11,7 +11,7 @@ steps: - git checkout $DRONE_SOURCE_BRANCH - mv .env.ci .env - name: run tests - image: node:alpine + image: node:14.15-alpine3.10 commands: - yarn - yarn test:ci @@ -39,7 +39,7 @@ steps: registry: registry.odit.services - name: run full license export depends_on: ["clone"] - image: node:alpine + image: node:14.15-alpine3.10 commands: - yarn - yarn licenses:export From e6576f4a540d822ed4c57e42ddecc68ac2311bbb Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 19:34:39 +0100 Subject: [PATCH 46/47] Finned node version for ci ref #67 --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index f7393db..bee9001 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ steps: - git checkout $DRONE_SOURCE_BRANCH - mv .env.ci .env - name: run tests - image: node:alpine + image: node:14.15.1-alpine3.12 commands: - yarn - yarn test:ci @@ -39,7 +39,7 @@ steps: registry: registry.odit.services - name: run full license export depends_on: ["clone"] - image: node:alpine + image: node:14.15.1-alpine3.12 commands: - yarn - yarn licenses:export From 4991d735bf4a12369043e8dea533ba387b9b48b9 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 8 Jan 2021 20:04:04 +0100 Subject: [PATCH 47/47] Pinned sqlite3 to 5.0.0 as a temporary bugfix ref #67 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c599d7..f552bec 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "reflect-metadata": "^0.1.13", "routing-controllers": "^0.9.0-alpha.6", "routing-controllers-openapi": "^2.1.0", - "sqlite3": "^5.0.0", + "sqlite3": "5.0.0", "typeorm": "^0.2.29", "typeorm-routing-controllers-extensions": "^0.2.0", "typeorm-seeding": "^1.6.1",