From 52eb7b1afe87c4e96d6975a983dcc69a2989de23 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:10:05 +0100 Subject: [PATCH 01/12] Added a barebones runnercard controller ref #77 --- src/controllers/RunnerCardController.ts | 99 +++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/controllers/RunnerCardController.ts diff --git a/src/controllers/RunnerCardController.ts b/src/controllers/RunnerCardController.ts new file mode 100644 index 0000000..d01ab60 --- /dev/null +++ b/src/controllers/RunnerCardController.ts @@ -0,0 +1,99 @@ +import { JsonController } from 'routing-controllers'; +import { OpenAPI } from 'routing-controllers-openapi'; +import { getConnectionManager, Repository } from 'typeorm'; +import { RunnerCard } from '../models/entities/RunnerCard'; + +@JsonController('/cards') +@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) +export class RunnerCardController { + private cardRepository: Repository; + + /** + * Gets the repository of this controller's model/entity. + */ + constructor() { + this.cardRepository = getConnectionManager().get().getRepository(RunnerCard); + } + + // @Get() + // @Authorized("CARD:GET") + // @ResponseSchema(ResponseTrack, { isArray: true }) + // @OpenAPI({ description: 'Lists all tracks.' }) + // async getAll() { + // let responseTracks: ResponseTrack[] = new Array(); + // const tracks = await this.trackRepository.find(); + // tracks.forEach(track => { + // responseTracks.push(new ResponseTrack(track)); + // }); + // return responseTracks; + // } + + // @Get('/:id') + // @Authorized("CARD:GET") + // @ResponseSchema(ResponseTrack) + // @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) + // @OnUndefined(TrackNotFoundError) + // @OpenAPI({ description: "Lists all information about the track whose id got provided." }) + // async getOne(@Param('id') id: number) { + // let track = await this.trackRepository.findOne({ id: id }); + // if (!track) { throw new TrackNotFoundError(); } + // return new ResponseTrack(track); + // } + + // @Post() + // @Authorized("CARD:CREATE") + // @ResponseSchema(ResponseTrack) + // @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) + // @OpenAPI({ description: "Create a new track.
Please remember that the track\'s distance must be greater than 0." }) + // async post( + // @Body({ validate: true }) + // track: CreateTrack + // ) { + // return new ResponseTrack(await this.trackRepository.save(track.toTrack())); + // } + + // @Put('/:id') + // @Authorized("CARD:UPDATE") + // @ResponseSchema(ResponseTrack) + // @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) + // @ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 }) + // @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) + // @OpenAPI({ description: "Update the track whose id you provided.
Please remember that ids can't be changed." }) + // async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) { + // let oldTrack = await this.trackRepository.findOne({ id: id }); + + // if (!oldTrack) { + // throw new TrackNotFoundError(); + // } + + // if (oldTrack.id != updateTrack.id) { + // throw new TrackIdsNotMatchingError(); + // } + // await this.trackRepository.save(await updateTrack.updateTrack(oldTrack)); + + // return new ResponseTrack(await this.trackRepository.findOne({ id: id })); + // } + + // @Delete('/:id') + // @Authorized("CARD:DELETE") + // @ResponseSchema(ResponseTrack) + // @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, @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); + // } +} \ No newline at end of file From af3a9e5ce249950ebd9d54c9b5521c9bd0aaab8c Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:15:29 +0100 Subject: [PATCH 02/12] Added basic response calss for runner cards ref #77 --- src/models/entities/RunnerCard.ts | 3 +- src/models/responses/ResponseRunnerCard.ts | 47 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/models/responses/ResponseRunnerCard.ts diff --git a/src/models/entities/RunnerCard.ts b/src/models/entities/RunnerCard.ts index 4ea40e2..25ce61c 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -7,6 +7,7 @@ import { IsString } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { ResponseRunnerCard } from '../responses/ResponseRunnerCard'; import { Runner } from "./Runner"; import { TrackScan } from "./TrackScan"; @@ -62,6 +63,6 @@ export class RunnerCard { * Turns this entity into it's response class. */ public toResponse() { - return new Error("NotImplemented"); + return new ResponseRunnerCard(this); } } diff --git a/src/models/responses/ResponseRunnerCard.ts b/src/models/responses/ResponseRunnerCard.ts new file mode 100644 index 0000000..94c7148 --- /dev/null +++ b/src/models/responses/ResponseRunnerCard.ts @@ -0,0 +1,47 @@ +import { IsBoolean, IsEAN, IsInt, IsNotEmpty, IsObject, IsString } from "class-validator"; +import { RunnerCard } from '../entities/RunnerCard'; +import { ResponseRunner } from './ResponseRunner'; + +/** + * Defines the runner card response. +*/ +export class ResponseRunnerCard { + /** + * The card's id. + */ + @IsInt() + id: number;; + + /** + * The card's associated runner. + * This is important to link scans to runners. + */ + @IsObject() + runner: ResponseRunner; + + /** + * The card's code. + */ + @IsEAN() + @IsString() + @IsNotEmpty() + code: string; + + /** + * Is the enabled valid (for fraud reasons). + * The determination of validity will work differently for every child class. + */ + @IsBoolean() + enabled: boolean = true; + + /** + * Creates a ResponseRunnerCard object from a runner card. + * @param card The card the response shall be build for. + */ + public constructor(card: RunnerCard) { + this.id = card.id; + this.runner = card.runner.toResponse() || null; + this.code = card.code; + this.enabled = card.enabled; + } +} From 98f7bf366f916d98cbce0b502923d1054044badf Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:21:52 +0100 Subject: [PATCH 03/12] Added card permission target ref #77 --- 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 dae9192..551ea5c 100644 --- a/src/models/enums/PermissionTargets.ts +++ b/src/models/enums/PermissionTargets.ts @@ -12,5 +12,6 @@ export enum PermissionTarget { STATSCLIENT = 'STATSCLIENT', DONOR = 'DONOR', SCAN = 'SCAN', - STATION = 'STATION' + STATION = 'STATION', + CARD = 'CARD' } \ No newline at end of file From 4faeddc3f3086727432bfbf9bebf4f38d73b74aa Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:23:12 +0100 Subject: [PATCH 04/12] Added runner card get endpoints ref #77 --- src/controllers/RunnerCardController.ts | 52 +++++++++++++------------ src/errors/RunnerCardErrors.ts | 25 ++++++++++++ 2 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 src/errors/RunnerCardErrors.ts diff --git a/src/controllers/RunnerCardController.ts b/src/controllers/RunnerCardController.ts index d01ab60..08942d8 100644 --- a/src/controllers/RunnerCardController.ts +++ b/src/controllers/RunnerCardController.ts @@ -1,7 +1,9 @@ -import { JsonController } from 'routing-controllers'; -import { OpenAPI } from 'routing-controllers-openapi'; +import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; +import { RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; import { RunnerCard } from '../models/entities/RunnerCard'; +import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard'; @JsonController('/cards') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -15,30 +17,30 @@ export class RunnerCardController { this.cardRepository = getConnectionManager().get().getRepository(RunnerCard); } - // @Get() - // @Authorized("CARD:GET") - // @ResponseSchema(ResponseTrack, { isArray: true }) - // @OpenAPI({ description: 'Lists all tracks.' }) - // async getAll() { - // let responseTracks: ResponseTrack[] = new Array(); - // const tracks = await this.trackRepository.find(); - // tracks.forEach(track => { - // responseTracks.push(new ResponseTrack(track)); - // }); - // return responseTracks; - // } + @Get() + @Authorized("CARD:GET") + @ResponseSchema(ResponseRunnerCard, { isArray: true }) + @OpenAPI({ description: 'Lists all card.' }) + async getAll() { + let responseCards: ResponseRunnerCard[] = new Array(); + const cards = await this.cardRepository.find({ relations: ['runner'] }); + cards.forEach(card => { + responseCards.push(new ResponseRunnerCard(card)); + }); + return responseCards; + } - // @Get('/:id') - // @Authorized("CARD:GET") - // @ResponseSchema(ResponseTrack) - // @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) - // @OnUndefined(TrackNotFoundError) - // @OpenAPI({ description: "Lists all information about the track whose id got provided." }) - // async getOne(@Param('id') id: number) { - // let track = await this.trackRepository.findOne({ id: id }); - // if (!track) { throw new TrackNotFoundError(); } - // return new ResponseTrack(track); - // } + @Get('/:id') + @Authorized("CARD:GET") + @ResponseSchema(ResponseRunnerCard) + @ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 }) + @OnUndefined(RunnerCardNotFoundError) + @OpenAPI({ description: "Lists all information about the card whose id got provided." }) + async getOne(@Param('id') id: number) { + let card = await this.cardRepository.findOne({ id: id }, { relations: ['runner'] }); + if (!card) { throw new RunnerCardNotFoundError(); } + return card.toResponse(); + } // @Post() // @Authorized("CARD:CREATE") diff --git a/src/errors/RunnerCardErrors.ts b/src/errors/RunnerCardErrors.ts new file mode 100644 index 0000000..bb0c6f3 --- /dev/null +++ b/src/errors/RunnerCardErrors.ts @@ -0,0 +1,25 @@ +import { IsString } from 'class-validator'; +import { NotAcceptableError, NotFoundError } from 'routing-controllers'; + +/** + * Error to throw when a card couldn't be found. + */ +export class RunnerCardNotFoundError extends NotFoundError { + @IsString() + name = "RunnerCardNotFoundError" + + @IsString() + message = "Card not found!" +} + +/** + * Error to throw when two cards' ids don't match. + * Usually occurs when a user tries to change a card's id. + */ +export class RunnerCardIdsNotMatchingError extends NotAcceptableError { + @IsString() + name = "RunnerCardIdsNotMatchingError" + + @IsString() + message = "The ids don't match! \n And if you wanted to change a cards's id: This isn't allowed" +} \ No newline at end of file From a5bfe4e3d5072718f29e2d4ca324d63bd7393291 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:28:59 +0100 Subject: [PATCH 05/12] Added card deletion + errors ref #77 --- src/controllers/RunnerCardController.ts | 47 +++++++++++++------------ src/errors/RunnerCardErrors.ts | 13 ++++++- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/controllers/RunnerCardController.ts b/src/controllers/RunnerCardController.ts index 08942d8..a11b626 100644 --- a/src/controllers/RunnerCardController.ts +++ b/src/controllers/RunnerCardController.ts @@ -1,9 +1,11 @@ -import { Authorized, Get, JsonController, OnUndefined, Param } from 'routing-controllers'; +import { Authorized, Delete, Get, JsonController, OnUndefined, Param, QueryParam } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; -import { RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; +import { RunnerCardHasScansError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; import { RunnerCard } from '../models/entities/RunnerCard'; +import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard'; +import { ScanController } from './ScanController'; @JsonController('/cards') @OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] }) @@ -76,26 +78,27 @@ export class RunnerCardController { // return new ResponseTrack(await this.trackRepository.findOne({ id: id })); // } - // @Delete('/:id') - // @Authorized("CARD:DELETE") - // @ResponseSchema(ResponseTrack) - // @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, @QueryParam("force") force: boolean) { - // let track = await this.trackRepository.findOne({ id: id }); - // if (!track) { return null; } + @Delete('/:id') + @Authorized("CARD:DELETE") + @ResponseSchema(ResponseRunnerCard) + @ResponseSchema(ResponseEmpty, { statusCode: 204 }) + @ResponseSchema(RunnerCardHasScansError, { statusCode: 406 }) + @OnUndefined(204) + @OpenAPI({ description: "Delete the card whose id you provided.
If no card with this id exists it will just return 204(no content).
If the card still has scans associated you have to provide the force=true query param (warning: this deletes all scans associated with by this card - please disable it instead or just remove the runner association)." }) + async remove(@Param("id") id: number, @QueryParam("force") force: boolean) { + let card = await this.cardRepository.findOne({ id: id }); + if (!card) { 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); - // } + const cardScans = (await this.cardRepository.findOne({ id: id }, { relations: ["scans"] })).scans; + if (cardScans.length != 0 && !force) { + throw new RunnerCardHasScansError(); + } + const scanController = new ScanController; + for (let scan of cardScans) { + scanController.remove(scan.id, force); + } - // await this.trackRepository.delete(track); - // return new ResponseTrack(track); - // } + await this.cardRepository.delete(card); + return card.toResponse(); + } } \ No newline at end of file diff --git a/src/errors/RunnerCardErrors.ts b/src/errors/RunnerCardErrors.ts index bb0c6f3..08edb44 100644 --- a/src/errors/RunnerCardErrors.ts +++ b/src/errors/RunnerCardErrors.ts @@ -22,4 +22,15 @@ export class RunnerCardIdsNotMatchingError extends NotAcceptableError { @IsString() message = "The ids don't match! \n And if you wanted to change a cards's id: This isn't allowed" -} \ No newline at end of file +} + +/** + * Error to throw when a station still has scans associated. + */ +export class RunnerCardHasScansError extends NotAcceptableError { + @IsString() + name = "RunnerCardHasScansError" + + @IsString() + message = "This card still has scans associated with it. \n If you want to delete this card with all it's scans add `?force` to your query. \n Otherwise please consider just diableing it." +} From 36ecae7e6e1eda637bafbc260d3dfa5815a472ff Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:48:13 +0100 Subject: [PATCH 06/12] Added card creation #17 --- src/controllers/RunnerCardController.ts | 25 ++++++------ src/models/actions/CreateRunnerCard.ts | 45 ++++++++++++++++++++++ src/models/entities/RunnerCard.ts | 26 ++++++------- src/models/responses/ResponseRunnerCard.ts | 5 ++- 4 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 src/models/actions/CreateRunnerCard.ts diff --git a/src/controllers/RunnerCardController.ts b/src/controllers/RunnerCardController.ts index a11b626..a1eee62 100644 --- a/src/controllers/RunnerCardController.ts +++ b/src/controllers/RunnerCardController.ts @@ -1,7 +1,9 @@ -import { Authorized, Delete, Get, JsonController, OnUndefined, Param, QueryParam } 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 { RunnerCardHasScansError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; +import { RunnerNotFoundError } from '../errors/RunnerErrors'; +import { CreateRunnerCard } from '../models/actions/CreateRunnerCard'; import { RunnerCard } from '../models/entities/RunnerCard'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard'; @@ -44,17 +46,16 @@ export class RunnerCardController { return card.toResponse(); } - // @Post() - // @Authorized("CARD:CREATE") - // @ResponseSchema(ResponseTrack) - // @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) - // @OpenAPI({ description: "Create a new track.
Please remember that the track\'s distance must be greater than 0." }) - // async post( - // @Body({ validate: true }) - // track: CreateTrack - // ) { - // return new ResponseTrack(await this.trackRepository.save(track.toTrack())); - // } + @Post() + @Authorized("CARD:CREATE") + @ResponseSchema(ResponseRunnerCard) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @OpenAPI({ description: "Create a new card.
You can provide a associated runner by id but you don't have to." }) + async post(@Body({ validate: true }) createCard: CreateRunnerCard) { + let card = await createCard.toEntity(); + card = await this.cardRepository.save(card); + return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner'] })).toResponse(); + } // @Put('/:id') // @Authorized("CARD:UPDATE") diff --git a/src/models/actions/CreateRunnerCard.ts b/src/models/actions/CreateRunnerCard.ts new file mode 100644 index 0000000..2baf1e9 --- /dev/null +++ b/src/models/actions/CreateRunnerCard.ts @@ -0,0 +1,45 @@ +import { IsBoolean, IsInt, IsOptional } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../errors/RunnerErrors'; +import { Runner } from '../entities/Runner'; +import { RunnerCard } from '../entities/RunnerCard'; + +/** + * This classed is used to create a new RunnerCard entity from a json body (post request). + */ +export class CreateRunnerCard { + /** + * The card's associated runner. + */ + @IsInt() + @IsOptional() + runner?: number; + + /** + * Is the new card enabled (for fraud reasons)? + * Default: true + */ + @IsBoolean() + enabled: boolean = true; + + /** + * Creates a new RunnerCard entity from this. + */ + public async toEntity(): Promise { + let newCard: RunnerCard = new RunnerCard(); + + newCard.enabled = this.enabled; + newCard.runner = await this.getRunner(); + + return newCard; + } + + public async getRunner(): Promise { + if (!this.runner) { return null; } + 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/RunnerCard.ts b/src/models/entities/RunnerCard.ts index 25ce61c..0517008 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -1,10 +1,9 @@ import { IsBoolean, - IsEAN, + IsInt, - IsNotEmpty, - IsOptional, - IsString + + IsOptional } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { ResponseRunnerCard } from '../responses/ResponseRunnerCard'; @@ -33,17 +32,6 @@ export class RunnerCard { @ManyToOne(() => Runner, runner => runner.cards, { nullable: true }) runner: Runner; - /** - * The card's code. - * This has to be able to being converted to something barcode compatible. - * Will get automaticlly generated (not implemented yet). - */ - @Column() - @IsEAN() - @IsString() - @IsNotEmpty() - code: string; - /** * Is the card enabled (for fraud reasons)? * Default: true @@ -59,6 +47,14 @@ export class RunnerCard { @OneToMany(() => TrackScan, scan => scan.track, { nullable: true }) scans: TrackScan[]; + /** + * Generates a ean-13 compliant string for barcode generation. + */ + public get code(): string { + //TODO: Implement the real deal + return '0000000000000' + } + /** * Turns this entity into it's response class. */ diff --git a/src/models/responses/ResponseRunnerCard.ts b/src/models/responses/ResponseRunnerCard.ts index 94c7148..97b9435 100644 --- a/src/models/responses/ResponseRunnerCard.ts +++ b/src/models/responses/ResponseRunnerCard.ts @@ -17,7 +17,7 @@ export class ResponseRunnerCard { * This is important to link scans to runners. */ @IsObject() - runner: ResponseRunner; + runner: ResponseRunner | null; /** * The card's code. @@ -40,7 +40,8 @@ export class ResponseRunnerCard { */ public constructor(card: RunnerCard) { this.id = card.id; - this.runner = card.runner.toResponse() || null; + if (!card.runner) { this.runner = null } + else { this.runner = card.runner.toResponse(); } this.code = card.code; this.enabled = card.enabled; } From 32fda46f0a7e4b63b1119754e9c0ba5327511ceb Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:55:32 +0100 Subject: [PATCH 07/12] Implemented runner updateing ref #77 --- src/controllers/RunnerCardController.ts | 41 ++++++++++---------- src/models/actions/UpdateRunnerCard.ts | 51 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 src/models/actions/UpdateRunnerCard.ts diff --git a/src/controllers/RunnerCardController.ts b/src/controllers/RunnerCardController.ts index a1eee62..2263f07 100644 --- a/src/controllers/RunnerCardController.ts +++ b/src/controllers/RunnerCardController.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 { RunnerCardHasScansError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; +import { RunnerCardHasScansError, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors'; import { RunnerNotFoundError } from '../errors/RunnerErrors'; import { CreateRunnerCard } from '../models/actions/CreateRunnerCard'; +import { UpdateRunnerCard } from '../models/actions/UpdateRunnerCard'; import { RunnerCard } from '../models/entities/RunnerCard'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseRunnerCard } from '../models/responses/ResponseRunnerCard'; @@ -57,27 +58,27 @@ export class RunnerCardController { return (await this.cardRepository.findOne({ id: card.id }, { relations: ['runner'] })).toResponse(); } - // @Put('/:id') - // @Authorized("CARD:UPDATE") - // @ResponseSchema(ResponseTrack) - // @ResponseSchema(TrackNotFoundError, { statusCode: 404 }) - // @ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 }) - // @ResponseSchema(TrackLapTimeCantBeNegativeError, { statusCode: 406 }) - // @OpenAPI({ description: "Update the track whose id you provided.
Please remember that ids can't be changed." }) - // async put(@Param('id') id: number, @Body({ validate: true }) updateTrack: UpdateTrack) { - // let oldTrack = await this.trackRepository.findOne({ id: id }); + @Put('/:id') + @Authorized("CARD:UPDATE") + @ResponseSchema(ResponseRunnerCard) + @ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) + @ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 }) + @OpenAPI({ description: "Update the card whose id you provided.
Scans created via this card will still be associated with the old runner.
Please remember that ids can't be changed." }) + async put(@Param('id') id: number, @Body({ validate: true }) card: UpdateRunnerCard) { + let oldCard = await this.cardRepository.findOne({ id: id }); - // if (!oldTrack) { - // throw new TrackNotFoundError(); - // } + if (!oldCard) { + throw new RunnerCardNotFoundError(); + } - // if (oldTrack.id != updateTrack.id) { - // throw new TrackIdsNotMatchingError(); - // } - // await this.trackRepository.save(await updateTrack.updateTrack(oldTrack)); + if (oldCard.id != card.id) { + throw new RunnerCardIdsNotMatchingError(); + } - // return new ResponseTrack(await this.trackRepository.findOne({ id: id })); - // } + await this.cardRepository.save(await card.update(oldCard)); + return (await this.cardRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse(); + } @Delete('/:id') @Authorized("CARD:DELETE") diff --git a/src/models/actions/UpdateRunnerCard.ts b/src/models/actions/UpdateRunnerCard.ts new file mode 100644 index 0000000..2ee34ab --- /dev/null +++ b/src/models/actions/UpdateRunnerCard.ts @@ -0,0 +1,51 @@ +import { IsBoolean, IsInt, IsOptional, IsPositive } from 'class-validator'; +import { getConnection } from 'typeorm'; +import { RunnerNotFoundError } from '../../errors/RunnerErrors'; +import { Runner } from '../entities/Runner'; +import { RunnerCard } from '../entities/RunnerCard'; + +/** + * This class is used to update a RunnerCard entity (via put request). + */ +export class UpdateRunnerCard { + /** + * The updated card'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() + @IsPositive() + id?: number; + + /** + * The updated card's associated runner. + */ + @IsInt() + @IsOptional() + runner?: number; + + /** + * Is the updated card enabled (for fraud reasons)? + * Default: true + */ + @IsBoolean() + enabled: boolean = true; + + /** + * Creates a new RunnerCard entity from this. + */ + public async update(card: RunnerCard): Promise { + card.enabled = this.enabled; + card.runner = await this.getRunner(); + + return card; + } + + public async getRunner(): Promise { + if (!this.runner) { return null; } + const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner }); + if (!runner) { + throw new RunnerNotFoundError(); + } + return runner; + } +} \ No newline at end of file From df39166279723f13d38288dd09f3120c26a628f1 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 11:59:20 +0100 Subject: [PATCH 08/12] Added card get tests ref #77 --- src/tests/cards/cards_get.spec.ts | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/tests/cards/cards_get.spec.ts diff --git a/src/tests/cards/cards_get.spec.ts b/src/tests/cards/cards_get.spec.ts new file mode 100644 index 0000000..6ca41ec --- /dev/null +++ b/src/tests/cards/cards_get.spec.ts @@ -0,0 +1,46 @@ +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/cards sucessfully', () => { + it('basic get should return 200', async () => { + const res = await axios.get(base + '/api/cards', axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('GET /api/cards illegally', () => { + it('get for non-existant track should return 404', async () => { + const res = await axios.get(base + '/api/cards/-1', axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + getting cards (no runner)', () => { + let added_card; + it('correct distance and runner input should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + added_card = 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/cards/' + added_card.id, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + }); +}); \ No newline at end of file From 860680d001191ac8ab4f5190618b4b0937915992 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 12:24:05 +0100 Subject: [PATCH 09/12] Implmented the EAN generation ref #77 --- src/errors/RunnerCardErrors.ts | 14 ++++++++++++- src/models/entities/RunnerCard.ts | 24 ++++++++++++++++++++-- src/models/responses/ResponseRunnerCard.ts | 7 ++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/errors/RunnerCardErrors.ts b/src/errors/RunnerCardErrors.ts index 08edb44..63c3485 100644 --- a/src/errors/RunnerCardErrors.ts +++ b/src/errors/RunnerCardErrors.ts @@ -25,7 +25,7 @@ export class RunnerCardIdsNotMatchingError extends NotAcceptableError { } /** - * Error to throw when a station still has scans associated. + * Error to throw when a card still has scans associated. */ export class RunnerCardHasScansError extends NotAcceptableError { @IsString() @@ -34,3 +34,15 @@ export class RunnerCardHasScansError extends NotAcceptableError { @IsString() message = "This card still has scans associated with it. \n If you want to delete this card with all it's scans add `?force` to your query. \n Otherwise please consider just diableing it." } + +/** + * Error to throw when a card's id is too big to generate a ean-13 barcode for it. + * This error should never reach a enduser. + */ +export class RunnerCardIdOutOfRangeError extends Error { + @IsString() + name = "RunnerCardIdOutOfRangeError" + + @IsString() + message = "The card's id is too big to fit into a ean-13 barcode. \n This has a very low probability of happening but means that you might want to switch your barcode format for something that can accept numbers over 9999999999." +} \ No newline at end of file diff --git a/src/models/entities/RunnerCard.ts b/src/models/entities/RunnerCard.ts index 0517008..a7bccc4 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -6,6 +6,7 @@ import { IsOptional } from "class-validator"; import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors'; import { ResponseRunnerCard } from '../responses/ResponseRunnerCard'; import { Runner } from "./Runner"; import { TrackScan } from "./TrackScan"; @@ -51,8 +52,27 @@ export class RunnerCard { * Generates a ean-13 compliant string for barcode generation. */ public get code(): string { - //TODO: Implement the real deal - return '0000000000000' + const multiply = [1, 3]; + let total = 0; + this.paddedId.split('').forEach((letter, index) => { + total += parseInt(letter, 10) * multiply[index % 2]; + }); + const checkSum = (Math.ceil(total / 10) * 10) - total; + return this.paddedId + checkSum.toString(); + } + + /** + * Returns this card's id as a string padded to the length of 12 characters with leading zeros. + */ + private get paddedId(): string { + let id: string = this.id.toString(); + + if (id.length > 12) { + throw new RunnerCardIdOutOfRangeError(); + } + while (id.length < 12) { id = '0' + id; } + + return id; } /** diff --git a/src/models/responses/ResponseRunnerCard.ts b/src/models/responses/ResponseRunnerCard.ts index 97b9435..f895a9c 100644 --- a/src/models/responses/ResponseRunnerCard.ts +++ b/src/models/responses/ResponseRunnerCard.ts @@ -42,7 +42,12 @@ export class ResponseRunnerCard { this.id = card.id; if (!card.runner) { this.runner = null } else { this.runner = card.runner.toResponse(); } - this.code = card.code; + try { + this.code = card.code; + } catch (error) { + this.code = "0000000000000" + } + this.enabled = card.enabled; } } From 8463bee25312b4ddf5badb2af26716c2bcf9d9dc Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 12:32:47 +0100 Subject: [PATCH 10/12] added card add tests ref #77 --- src/tests/cards/cards_add.spec.ts | 144 ++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/tests/cards/cards_add.spec.ts diff --git a/src/tests/cards/cards_add.spec.ts b/src/tests/cards/cards_add.spec.ts new file mode 100644 index 0000000..c89e55b --- /dev/null +++ b/src/tests/cards/cards_add.spec.ts @@ -0,0 +1,144 @@ +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/cards illegally', () => { + it('non-existant runner input should return 404', async () => { + const res = await axios.post(base + '/api/cards', { + "runner": 999999999999999999999999 + }, axios_config); + expect(res.status).toEqual(404); + expect(res.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('POST /api/cards successfully (without runner)', () => { + it('creating a card with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": null, + "enabled": true + }); + }); + it('creating a disabled card should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "enabled": false + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": null, + "enabled": false + }); + }); + it('creating a enabled card should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "enabled": true + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": null, + "enabled": true + }); + }); +}); +// --------------- +describe('POST /api/cards successfully (with runner)', () => { + 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 card with the minimum amount of parameters should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "runner": added_runner.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": added_runner, + "enabled": true + }); + }); + it('creating a card with runner (no optional params) should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "runner": added_runner.id + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": added_runner, + "enabled": true + }); + }); + it('creating a enabled card with runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "runner": added_runner.id, + "enabled": true + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": added_runner, + "enabled": true + }); + }); + it('creating a disabled card with runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', { + "runner": added_runner.id, + "enabled": false + }, axios_config); + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json"); + delete res.data.id; + delete res.data.code; + expect(res.data).toEqual({ + "runner": added_runner, + "enabled": false + }); + }); +}); \ No newline at end of file From ebf66821a2a0956905a2e7d3e7bbdd0cd2296152 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 12:41:59 +0100 Subject: [PATCH 11/12] Added card delete tests ref #77 --- src/tests/cards/cards_delete.spec.ts | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/tests/cards/cards_delete.spec.ts diff --git a/src/tests/cards/cards_delete.spec.ts b/src/tests/cards/cards_delete.spec.ts new file mode 100644 index 0000000..1e657c8 --- /dev/null +++ b/src/tests/cards/cards_delete.spec.ts @@ -0,0 +1,45 @@ +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 card', () => { + let added_card; + it('creating card without runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + added_card = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('delete card', async () => { + const res2 = await axios.delete(base + '/api/cards/' + added_card.id, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json") + expect(res2.data).toEqual(added_card); + }); + it('check if card really was deleted', async () => { + const res3 = await axios.get(base + '/api/cards/' + added_card.id, axios_config); + expect(res3.status).toEqual(404); + expect(res3.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('DELETE card (non-existant)', () => { + it('delete', async () => { + const res2 = await axios.delete(base + '/api/cards/0', axios_config); + expect(res2.status).toEqual(204); + }); +}); From 35ea3154d132d095fc29d1d302964d680d082b4e Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Sat, 9 Jan 2021 12:42:41 +0100 Subject: [PATCH 12/12] Added card update tests ref #77 --- src/tests/cards/cards_update.spec.ts | 163 +++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/tests/cards/cards_update.spec.ts diff --git a/src/tests/cards/cards_update.spec.ts b/src/tests/cards/cards_update.spec.ts new file mode 100644 index 0000000..4ef31ee --- /dev/null +++ b/src/tests/cards/cards_update.spec.ts @@ -0,0 +1,163 @@ +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_card; + it('creating card without runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + added_card = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('updating empty should return 400', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.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/cards/' + added_card.id, { + "id": added_card.id + 1 + }, axios_config); + expect(res2.status).toEqual(406); + 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/cards/' + added_card.id, { + "id": added_card.id, + "runner": 9999999999999999999999999 + }, axios_config); + expect(res2.status).toEqual(404); + expect(res2.headers['content-type']).toContain("application/json") + }); +}); +// --------------- +describe('adding + updating card.runner successfully', () => { + let added_org; + let added_runner; + let added_runner2; + let added_card; + 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 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('creating card without runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + added_card = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('valid runner update (add runner) should return 200', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.id, { + "id": added_card.id, + "runner": added_runner.id + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual({ + "id": added_card.id, + "runner": added_runner, + "enabled": true, + "code": added_card.code + }); + }); + it('valid runner update (change runner) should return 200', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.id, { + "id": added_card.id, + "runner": added_runner2.id + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual({ + "id": added_card.id, + "runner": added_runner2, + "enabled": true, + "code": added_card.code + }); + }); +}); +// --------------- +describe('adding + updating other values successfully', () => { + let added_card; + it('creating card without runner should return 200', async () => { + const res = await axios.post(base + '/api/cards', null, axios_config); + added_card = res.data; + expect(res.status).toEqual(200); + expect(res.headers['content-type']).toContain("application/json") + }); + it('valid update changeing nothing should return 200', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.id, { + "id": added_card.id + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual(added_card); + }); + it('valid disable update should return 200', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.id, { + "id": added_card.id, + "enabled": false + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual({ + "id": added_card.id, + "runner": null, + "enabled": false, + "code": added_card.code + }); + }); + it('valid enable update should return 200', async () => { + const res2 = await axios.put(base + '/api/cards/' + added_card.id, { + "id": added_card.id, + "enabled": true + }, axios_config); + expect(res2.status).toEqual(200); + expect(res2.headers['content-type']).toContain("application/json"); + expect(res2.data).toEqual({ + "id": added_card.id, + "runner": null, + "enabled": true, + "code": added_card.code + }); + }); +}); +