Merge pull request 'Alpha Release 0.0.11' (#87) from dev into main
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

Reviewed-on: #87
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
This commit is contained in:
Nicolai Ort 2021-01-10 17:16:09 +00:00
commit b3ce56c605
82 changed files with 2224 additions and 651 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@odit/lfk-backend",
"version": "0.0.10",
"version": "0.0.11",
"main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend",
"author": {
@ -22,11 +22,11 @@
],
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"argon2": "^0.27.0",
"argon2": "^0.27.1",
"body-parser": "^1.19.0",
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"class-validator-jsonschema": "^2.0.3",
"class-validator-jsonschema": "^2.1.0",
"consola": "^2.15.0",
"cookie": "^0.4.1",
"cookie-parser": "^1.4.5",
@ -39,33 +39,34 @@
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0-alpha.6",
"routing-controllers-openapi": "^2.1.0",
"routing-controllers-openapi": "^2.2.0",
"sqlite3": "5.0.0",
"typeorm": "^0.2.29",
"typeorm-routing-controllers-extensions": "^0.2.0",
"typeorm-seeding": "^1.6.1",
"uuid": "^8.3.1",
"uuid": "^8.3.2",
"validator": "^13.5.2"
},
"devDependencies": {
"@odit/license-exporter": "^0.0.8",
"@types/cors": "^2.8.8",
"@odit/license-exporter": "^0.0.9",
"@types/cors": "^2.8.9",
"@types/csvtojson": "^1.1.5",
"@types/express": "^4.17.9",
"@types/jest": "^26.0.16",
"@types/jsonwebtoken": "^8.5.0",
"@types/node": "^14.14.9",
"@types/node": "^14.14.20",
"@types/uuid": "^8.3.0",
"axios": "^0.21.0",
"axios": "^0.21.1",
"cp-cli": "^2.0.0",
"jest": "^26.6.3",
"nodemon": "^2.0.6",
"rimraf": "^2.7.1",
"start-server-and-test": "^1.11.6",
"nodemon": "^2.0.7",
"release-it": "^14.2.2",
"rimraf": "^3.0.2",
"start-server-and-test": "^1.11.7",
"ts-jest": "^26.4.4",
"ts-node": "^9.0.0",
"typedoc": "^0.19.2",
"typescript": "^4.1.2"
"ts-node": "^9.1.1",
"typedoc": "^0.20.14",
"typescript": "^4.1.3"
},
"scripts": {
"dev": "nodemon src/app.ts",
@ -76,7 +77,21 @@
"test:ci": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
"seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
"openapi:export": "ts-node scripts/openapi_export.ts",
"licenses:export": "license-exporter --md"
"licenses:export": "license-exporter --md",
"release": "release-it"
},
"release-it": {
"git": {
"requireCleanWorkingDir": false,
"requireBranch": "main",
"push": false,
"tag": true,
"tagName": "v${version}",
"tagAnnotation": "v${version}"
},
"npm": {
"publish": false
}
},
"nodemonConfig": {
"ignore": [

View File

@ -3,7 +3,7 @@ import consola from "consola";
import fs from "fs";
import "reflect-metadata";
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from 'routing-controllers-openapi';
import { generateSpec } from '../src/apispec';
import { config } from '../src/config';
import authchecker from "../src/middlewares/authchecker";
import { ErrorHandler } from '../src/middlewares/ErrorHandler';
@ -24,46 +24,7 @@ const schemas = validationMetadatasToSchemas({
});
//Spec creation based on the previously created schemas
const spec = routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
},
"RefreshTokenCookie": {
"type": "apiKey",
"in": "cookie",
"name": "lfk_backend__refresh_token",
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
},
"StatsApiToken": {
"type": "http",
"scheme": "bearer",
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."
}
}
},
info: {
description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API",
version: "0.0.8",
},
}
);
const spec = generateSpec(storage, schemas);
try {
fs.writeFileSync("./openapi.json", JSON.stringify(spec), { encoding: "utf-8" });

50
src/apispec.ts Normal file
View File

@ -0,0 +1,50 @@
import { MetadataArgsStorage } from 'routing-controllers';
import { routingControllersToSpec } from 'routing-controllers-openapi';
/**
* This function generates a the openapi spec from route metadata and type schemas.
* @param storage MetadataArgsStorage object generated by routing-controllers.
* @param schemas MetadataArgsStorage object generated by class-validator-jsonschema.
*/
export function generateSpec(storage: MetadataArgsStorage, schemas) {
return routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
},
"RefreshTokenCookie": {
"type": "apiKey",
"in": "cookie",
"name": "lfk_backend__refresh_token",
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
},
"StatsApiToken": {
"type": "http",
"scheme": "bearer",
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."
}
}
},
info: {
description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API",
version: process.env.npm_package_version
},
}
);
}

View File

@ -2,12 +2,12 @@ import { Body, CookieParam, JsonController, Param, Post, Req, Res } from 'routin
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
import { UserNotFoundError } from '../errors/UserErrors';
import { CreateAuth } from '../models/actions/CreateAuth';
import { CreateResetToken } from '../models/actions/CreateResetToken';
import { CreateAuth } from '../models/actions/create/CreateAuth';
import { CreateResetToken } from '../models/actions/create/CreateResetToken';
import { HandleLogout } from '../models/actions/HandleLogout';
import { RefreshAuth } from '../models/actions/RefreshAuth';
import { ResetPassword } from '../models/actions/ResetPassword';
import { Auth } from '../models/responses/ResponseAuth';
import { ResponseAuth } from '../models/responses/ResponseAuth';
import { Logout } from '../models/responses/ResponseLogout';
@JsonController('/auth')
@ -16,7 +16,7 @@ export class AuthController {
}
@Post("/login")
@ResponseSchema(Auth)
@ResponseSchema(ResponseAuth)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ -60,7 +60,7 @@ export class AuthController {
}
@Post("/refresh")
@ResponseSchema(Auth)
@ResponseSchema(ResponseAuth)
@ResponseSchema(JwtNotProvidedError)
@ResponseSchema(IllegalJWTError)
@ResponseSchema(UserNotFoundError)
@ -82,7 +82,7 @@ export class AuthController {
}
@Post("/reset")
@ResponseSchema(Auth)
@ResponseSchema(ResponseAuth)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@OpenAPI({ description: "Request a password reset token. <br> This will provide you with a reset token that you can use by posting to /api/auth/reset/{token}." })
@ -92,7 +92,7 @@ export class AuthController {
}
@Post("/reset/:token")
@ResponseSchema(Auth)
@ResponseSchema(ResponseAuth)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@OpenAPI({ description: "Reset a user's utilising a valid password reset token. <br> This will set the user's password to the one you provided in the body. <br> To get a reset token post to /api/auth/reset with your username." })

View File

@ -2,8 +2,8 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { DonorIdsNotMatchingError, DonorNotFoundError } from '../errors/DonorErrors';
import { CreateDonor } from '../models/actions/CreateDonor';
import { UpdateDonor } from '../models/actions/UpdateDonor';
import { CreateDonor } from '../models/actions/create/CreateDonor';
import { UpdateDonor } from '../models/actions/update/UpdateDonor';
import { Donor } from '../models/entities/Donor';
import { ResponseDonor } from '../models/responses/ResponseDonor';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
@ -52,7 +52,7 @@ export class DonorController {
async post(@Body({ validate: true }) createRunner: CreateDonor) {
let donor;
try {
donor = await createRunner.toDonor();
donor = await createRunner.toEntity();
} catch (error) {
throw error;
}
@ -78,7 +78,7 @@ export class DonorController {
throw new DonorIdsNotMatchingError();
}
await this.donorRepository.save(await donor.updateDonor(oldDonor));
await this.donorRepository.save(await donor.update(oldDonor));
return new ResponseDonor(await this.donorRepository.findOne({ id: id }));
}

View File

@ -3,8 +3,8 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
import { CreatePermission } from '../models/actions/CreatePermission';
import { UpdatePermission } from '../models/actions/UpdatePermission';
import { CreatePermission } from '../models/actions/create/CreatePermission';
import { UpdatePermission } from '../models/actions/update/UpdatePermission';
import { Permission } from '../models/entities/Permission';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponsePermission } from '../models/responses/ResponsePermission';
@ -58,7 +58,7 @@ export class PermissionController {
async post(@Body({ validate: true }) createPermission: CreatePermission) {
let permission;
try {
permission = await createPermission.toPermission();
permission = await createPermission.toEntity();
} catch (error) {
throw error;
}
@ -96,7 +96,7 @@ export class PermissionController {
return new ResponsePermission(existingPermission);
}
await this.permissionRepository.save(await permission.updatePermission(oldPermission));
await this.permissionRepository.save(await permission.update(oldPermission));
return new ResponsePermission(await this.permissionRepository.findOne({ id: permission.id }, { relations: ['principal'] }));
}

View File

@ -0,0 +1,106 @@
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, RunnerCardIdsNotMatchingError, RunnerCardNotFoundError } from '../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateRunnerCard } from '../models/actions/create/CreateRunnerCard';
import { UpdateRunnerCard } from '../models/actions/update/UpdateRunnerCard';
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": [] }] })
export class RunnerCardController {
private cardRepository: Repository<RunnerCard>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.cardRepository = getConnectionManager().get().getRepository(RunnerCard);
}
@Get()
@Authorized("CARD:GET")
@ResponseSchema(ResponseRunnerCard, { isArray: true })
@OpenAPI({ description: 'Lists all card.' })
async getAll() {
let responseCards: ResponseRunnerCard[] = new Array<ResponseRunnerCard>();
const cards = await this.cardRepository.find({ relations: ['runner'] });
cards.forEach(card => {
responseCards.push(new ResponseRunnerCard(card));
});
return responseCards;
}
@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")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: "Create a new card. <br> 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")
@ResponseSchema(ResponseRunnerCard)
@ResponseSchema(RunnerCardNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerCardIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the card whose id you provided. <br> Scans created via this card will still be associated with the old runner. <br> 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 (!oldCard) {
throw new RunnerCardNotFoundError();
}
if (oldCard.id != card.id) {
throw new RunnerCardIdsNotMatchingError();
}
await this.cardRepository.save(await card.update(oldCard));
return (await this.cardRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
}
@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. <br> If no card with this id exists it will just return 204(no content). <br> 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 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) {
await scanController.remove(scan.id, force);
}
await this.cardRepository.delete(card);
return card.toResponse();
}
}

View File

@ -3,11 +3,13 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import { CreateRunner } from '../models/actions/CreateRunner';
import { UpdateRunner } from '../models/actions/UpdateRunner';
import { CreateRunner } from '../models/actions/create/CreateRunner';
import { UpdateRunner } from '../models/actions/update/UpdateRunner';
import { Runner } from '../models/entities/Runner';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController';
@JsonController('/runners')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
@ -27,7 +29,7 @@ export class RunnerController {
@OpenAPI({ description: 'Lists all runners from all teams/orgs. <br> This includes the runner\'s group and distance ran.' })
async getAll() {
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
const runners = await this.runnerRepository.find({ relations: ['scans', 'group'] });
const runners = await this.runnerRepository.find({ relations: ['scans', 'group', 'scans.track', 'cards'] });
runners.forEach(runner => {
responseRunners.push(new ResponseRunner(runner));
});
@ -41,7 +43,7 @@ export class RunnerController {
@OnUndefined(RunnerNotFoundError)
@OpenAPI({ description: 'Lists all information about the runner whose id got provided.' })
async getOne(@Param('id') id: number) {
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] })
let runner = await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] })
if (!runner) { throw new RunnerNotFoundError(); }
return new ResponseRunner(runner);
}
@ -55,13 +57,13 @@ export class RunnerController {
async post(@Body({ validate: true }) createRunner: CreateRunner) {
let runner;
try {
runner = await createRunner.toRunner();
runner = await createRunner.toEntity();
} catch (error) {
throw error;
}
runner = await this.runnerRepository.save(runner)
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] }));
return new ResponseRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
}
@Put('/:id')
@ -81,8 +83,8 @@ export class RunnerController {
throw new RunnerIdsNotMatchingError();
}
await this.runnerRepository.save(await runner.updateRunner(oldRunner));
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
await this.runnerRepository.save(await runner.update(oldRunner));
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group', 'scans.track', 'cards'] }));
}
@Delete('/:id')
@ -90,16 +92,28 @@ export class RunnerController {
@ResponseSchema(ResponseRunner)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> If no runner with this id exists it will just return 204(no content).' })
@OpenAPI({ description: 'Delete the runner whose id you provided. <br> This will also delete all scans and cards associated with the runner. <br> 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 runner = await this.runnerRepository.findOne({ id: id });
if (!runner) { return null; }
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group'] });
const responseRunner = await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'scans.track', 'cards'] });
if (!runner) {
throw new RunnerNotFoundError();
}
const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
const cardController = new RunnerCardController;
for (let scan of runnerCards) {
await cardController.remove(scan.id, force);
}
const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
const scanController = new ScanController;
for (let scan of runnerScans) {
await scanController.remove(scan.id, force);
}
await this.runnerRepository.delete(runner);
return new ResponseRunner(responseRunner);
}

View File

@ -2,8 +2,8 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
import { UpdateRunnerOrganisation } from '../models/actions/UpdateRunnerOrganisation';
import { CreateRunnerOrganisation } from '../models/actions/create/CreateRunnerOrganisation';
import { UpdateRunnerOrganisation } from '../models/actions/update/UpdateRunnerOrganisation';
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
@ -55,7 +55,7 @@ export class RunnerOrganisationController {
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
let runnerOrganisation;
try {
runnerOrganisation = await createRunnerOrganisation.toRunnerOrganisation();
runnerOrganisation = await createRunnerOrganisation.toEntity();
} catch (error) {
throw error;
}
@ -82,7 +82,7 @@ export class RunnerOrganisationController {
throw new RunnerOrganisationIdsNotMatchingError();
}
await this.runnerOrganisationRepository.save(await updateOrganisation.updateRunnerOrganisation(oldRunnerOrganisation));
await this.runnerOrganisationRepository.save(await updateOrganisation.update(oldRunnerOrganisation));
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['address', 'contact', 'teams'] }));
}

View File

@ -2,8 +2,8 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam';
import { UpdateRunnerTeam } from '../models/actions/UpdateRunnerTeam';
import { CreateRunnerTeam } from '../models/actions/create/CreateRunnerTeam';
import { UpdateRunnerTeam } from '../models/actions/update/UpdateRunnerTeam';
import { RunnerTeam } from '../models/entities/RunnerTeam';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam';
@ -54,7 +54,7 @@ export class RunnerTeamController {
async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
let runnerTeam;
try {
runnerTeam = await createRunnerTeam.toRunnerTeam();
runnerTeam = await createRunnerTeam.toEntity();
} catch (error) {
throw error;
}
@ -82,7 +82,7 @@ export class RunnerTeamController {
throw new RunnerTeamIdsNotMatchingError();
}
await this.runnerTeamRepository.save(await runnerTeam.updateRunnerTeam(oldRunnerTeam));
await this.runnerTeamRepository.save(await runnerTeam.update(oldRunnerTeam));
return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: runnerTeam.id }, { relations: ['parentGroup', 'contact'] }));
}

View File

@ -3,11 +3,14 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { ScanIdsNotMatchingError, ScanNotFoundError } from '../errors/ScanErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
import ScanAuth from '../middlewares/ScanAuth';
import { CreateScan } from '../models/actions/CreateScan';
import { CreateTrackScan } from '../models/actions/CreateTrackScan';
import { UpdateScan } from '../models/actions/UpdateScan';
import { CreateScan } from '../models/actions/create/CreateScan';
import { CreateTrackScan } from '../models/actions/create/CreateTrackScan';
import { UpdateScan } from '../models/actions/update/UpdateScan';
import { UpdateTrackScan } from '../models/actions/update/UpdateTrackScan';
import { Scan } from '../models/entities/Scan';
import { TrackScan } from '../models/entities/TrackScan';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScan } from '../models/responses/ResponseScan';
import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
@ -16,12 +19,14 @@ import { ResponseTrackScan } from '../models/responses/ResponseTrackScan';
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class ScanController {
private scanRepository: Repository<Scan>;
private trackScanRepository: Repository<TrackScan>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.scanRepository = getConnectionManager().get().getRepository(Scan);
this.trackScanRepository = getConnectionManager().get().getRepository(TrackScan);
}
@Get()
@ -31,7 +36,7 @@ export class ScanController {
@OpenAPI({ description: 'Lists all scans (normal or track) from all runners. <br> This includes the scan\'s runner\'s distance ran.' })
async getAll() {
let responseScans: ResponseScan[] = new Array<ResponseScan>();
const scans = await this.scanRepository.find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
const scans = await this.scanRepository.find({ relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
scans.forEach(scan => {
responseScans.push(scan.toResponse());
});
@ -46,7 +51,7 @@ export class ScanController {
@OnUndefined(ScanNotFoundError)
@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', 'runner.scans', 'runner.scans.track'] })
let scan = await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })
if (!scan) { throw new ScanNotFoundError(); }
return scan.toResponse();
}
@ -55,20 +60,22 @@ export class ScanController {
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new scan. <br> Please remeber to provide the scan\'s runner\'s id and distance for normal scans.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
@OpenAPI({ description: 'Create a new scan (not track scan - use /scans/trackscans instead). <br> Please rmemember to provide the scan\'s runner\'s id and distance.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async post(@Body({ validate: true }) createScan: CreateScan) {
let scan = await createScan.toScan();
let scan = await createScan.toEntity();
scan = await this.scanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner'] })).toResponse();
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Post("/trackscans")
@UseBefore(ScanAuth)
@ResponseSchema(ResponseScan)
@ResponseSchema(ResponseTrackScan)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new track scan. <br> This is just a alias for posting /scans', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
@OpenAPI({ description: 'Create a new track scan (for "normal" scans use /scans instead). <br> Please remember that to provide the scan\'s card\'s station\'s id.', security: [{ "ScanApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
async postTrackScans(@Body({ validate: true }) createScan: CreateTrackScan) {
return this.post(createScan);
let scan = await createScan.toEntity();
scan = await this.trackScanRepository.save(scan);
return (await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Put('/:id')
@ -77,7 +84,7 @@ export class ScanController {
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the scan whose id you provided. <br> Please remember that ids can't be changed and distances must be positive." })
@OpenAPI({ description: "Update the scan (not track scan use /scans/trackscans/:id instead) whose id you provided. <br> 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 });
@ -89,8 +96,31 @@ export class ScanController {
throw new ScanIdsNotMatchingError();
}
await this.scanRepository.save(await scan.updateScan(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner'] })).toResponse();
await this.scanRepository.save(await scan.update(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Put('/trackscans/:id')
@Authorized("SCAN:UPDATE")
@ResponseSchema(ResponseTrackScan)
@ResponseSchema(ScanNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
@ResponseSchema(ScanIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: 'Update the track scan (not "normal" scan use /scans/trackscans/:id instead) whose id you provided. <br> Please remember that only the validity, runner and track can be changed.' })
async putTrackScan(@Param('id') id: number, @Body({ validate: true }) scan: UpdateTrackScan) {
let oldScan = await this.trackScanRepository.findOne({ id: id });
if (!oldScan) {
throw new ScanNotFoundError();
}
if (oldScan.id != scan.id) {
throw new ScanIdsNotMatchingError();
}
await this.trackScanRepository.save(await scan.update(oldScan));
return (await this.scanRepository.findOne({ id: id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] })).toResponse();
}
@Delete('/:id')
@ -102,7 +132,7 @@ export class ScanController {
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"] });
const responseScan = await this.scanRepository.findOne({ id: scan.id }, { relations: ['runner', 'track', 'runner.scans', 'runner.scans.track', 'card', 'station'] });
await this.scanRepository.delete(scan);
return responseScan.toResponse();

View File

@ -3,8 +3,8 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
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 { CreateScanStation } from '../models/actions/create/CreateScanStation';
import { UpdateScanStation } from '../models/actions/update/UpdateScanStation';
import { ScanStation } from '../models/entities/ScanStation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
@ -77,7 +77,7 @@ export class ScanStationController {
throw new ScanStationIdsNotMatchingError();
}
await this.stationRepository.save(await station.updateStation(oldStation));
await this.stationRepository.save(await station.update(oldStation));
return (await this.stationRepository.findOne({ id: id }, { relations: ['track'] })).toResponse();
}
@ -98,7 +98,7 @@ export class ScanStationController {
}
const scanController = new ScanController;
for (let scan of stationScans) {
scanController.remove(scan.id, force);
await scanController.remove(scan.id, force);
}
const responseStation = await this.stationRepository.findOne({ id: station.id }, { relations: ["track"] });

View File

@ -1,9 +1,9 @@
import { Authorized, Body, Delete, 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 { StatsClientNotFoundError } from '../errors/StatsClientErrors';
import { TrackNotFoundError } from "../errors/TrackErrors";
import { CreateStatsClient } from '../models/actions/CreateStatsClient';
import { CreateStatsClient } from '../models/actions/create/CreateStatsClient';
import { StatsClient } from '../models/entities/StatsClient';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseStatsClient } from '../models/responses/ResponseStatsClient';
@ -53,7 +53,7 @@ export class StatsClientController {
@Body({ validate: true })
client: CreateStatsClient
) {
let newClient = await this.clientRepository.save(await client.toStatsClient());
let newClient = await this.clientRepository.save(await client.toEntity());
let responseClient = new ResponseStatsClient(newClient);
responseClient.key = newClient.cleartextkey;
return responseClient;
@ -65,7 +65,7 @@ export class StatsClientController {
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: "Delete the stats client whose id you provided. <br> If no client 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 client = await this.clientRepository.findOne({ id: id });
if (!client) { return null; }

View File

@ -2,8 +2,8 @@ import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { TrackHasScanStationsError, TrackIdsNotMatchingError, TrackLapTimeCantBeNegativeError, TrackNotFoundError } from "../errors/TrackErrors";
import { CreateTrack } from '../models/actions/CreateTrack';
import { UpdateTrack } from '../models/actions/UpdateTrack';
import { CreateTrack } from '../models/actions/create/CreateTrack';
import { UpdateTrack } from '../models/actions/update/UpdateTrack';
import { Track } from '../models/entities/Track';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseTrack } from '../models/responses/ResponseTrack';
@ -55,7 +55,7 @@ export class TrackController {
@Body({ validate: true })
track: CreateTrack
) {
return new ResponseTrack(await this.trackRepository.save(track.toTrack()));
return new ResponseTrack(await this.trackRepository.save(await track.toEntity()));
}
@Put('/:id')
@ -75,7 +75,7 @@ export class TrackController {
if (oldTrack.id != updateTrack.id) {
throw new TrackIdsNotMatchingError();
}
await this.trackRepository.save(await updateTrack.updateTrack(oldTrack));
await this.trackRepository.save(await updateTrack.update(oldTrack));
return new ResponseTrack(await this.trackRepository.findOne({ id: id }));
}
@ -94,9 +94,9 @@ export class TrackController {
if (trackStations.length != 0 && !force) {
throw new TrackHasScanStationsError();
}
const scanController = new ScanStationController;
const stationController = new ScanStationController;
for (let station of trackStations) {
scanController.remove(station.id, force);
await stationController.remove(station.id, force);
}
await this.trackRepository.delete(track);

View File

@ -3,8 +3,8 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/CreateUser';
import { UpdateUser } from '../models/actions/UpdateUser';
import { CreateUser } from '../models/actions/create/CreateUser';
import { UpdateUser } from '../models/actions/update/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUser } from '../models/responses/ResponseUser';
@ -56,7 +56,7 @@ export class UserController {
async post(@Body({ validate: true }) createUser: CreateUser) {
let user;
try {
user = await createUser.toUser();
user = await createUser.toEntity();
} catch (error) {
throw error;
}
@ -81,7 +81,7 @@ export class UserController {
if (oldUser.id != updateUser.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.save(await updateUser.updateUser(oldUser));
await this.userRepository.save(await updateUser.update(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] }));
}

View File

@ -3,7 +3,8 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
import { UpdateUserGroup } from '../models/actions/update/UpdateUserGroup';
import { UserGroup } from '../models/entities/UserGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
@ -48,7 +49,7 @@ export class UserGroupController {
async post(@Body({ validate: true }) createUserGroup: CreateUserGroup) {
let userGroup;
try {
userGroup = await createUserGroup.toUserGroup();
userGroup = await createUserGroup.toEntity();
} catch (error) {
throw error;
}
@ -62,19 +63,19 @@ export class UserGroupController {
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update the group whose id you provided. <br> To change the permissions granted to the group please use /api/permissions instead. <br> Please remember that ids can't be changed." })
async put(@Param('id') id: number, @EntityFromBody() userGroup: UserGroup) {
let oldUserGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ["permissions"] });
async put(@Param('id') id: number, @EntityFromBody() updateGroup: UpdateUserGroup) {
let oldGroup = await this.userGroupsRepository.findOne({ id: id });
if (!oldUserGroup) {
throw new UserGroupNotFoundError()
if (!oldGroup) {
throw new UserGroupNotFoundError();
}
if (oldUserGroup.id != userGroup.id) {
if (oldGroup.id != updateGroup.id) {
throw new UserGroupIdsNotMatchingError();
}
await this.userGroupsRepository.save(await updateGroup.update(oldGroup));
await this.userGroupsRepository.save(userGroup);
return userGroup;
return (await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })).toResponse();
}
@Delete('/:id')
@ -88,9 +89,9 @@ export class UserGroupController {
if (!group) { return null; }
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });
const permissionControler = new PermissionController();
const permissionController = new PermissionController();
for (let permission of responseGroup.permissions) {
await permissionControler.remove(permission.id, true);
await permissionController.remove(permission.id, true);
}
await this.userGroupsRepository.delete(group);

View File

@ -9,11 +9,11 @@ export class AddressWrongTypeError extends NotAcceptableError {
name = "AddressWrongTypeError"
@IsString()
message = "The address must be an existing adress's id. \n You provided a object of another type."
message = "The address must be an existing address's id. \n You provided a object of another type."
}
/**
* Error to throw, when a non-existant address get's loaded.
* Error to throw, when a non-existent address get's loaded.
*/
export class AddressNotFoundError extends NotFoundError {
@IsString()

View File

@ -118,7 +118,7 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError {
}
/**
* Error to throw when someone tryes to reset a user's password more than once in 15 minutes.
* Error to throw when someone tries to reset a user's password more than once in 15 minutes.
*/
export class ResetAlreadyRequestedError extends NotAcceptableError {
@IsString()

View File

@ -13,7 +13,7 @@ export class GroupContactWrongTypeError extends NotAcceptableError {
}
/**
* Error to throw, when a non-existant groupContact get's loaded.
* Error to throw, when a non-existent groupContact get's loaded.
*/
export class GroupContactNotFoundError extends NotFoundError {
@IsString()

View File

@ -13,12 +13,12 @@ export class PrincipalNotFoundError extends NotFoundError {
}
/**
* Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types.
* Error to throw, when a provided runner organization doesn't belong to the accepted types.
*/
export class PrincipalWrongTypeError extends NotAcceptableError {
@IsString()
name = "PrincipalWrongTypeError"
@IsString()
message = "The princial must have an existing principal's id. \n You provided a object of another type."
message = "The principal must have an existing principal's id. \n You provided a object of another type."
}

View File

@ -0,0 +1,48 @@
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"
}
/**
* Error to throw when a card 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 disabling 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 end user.
*/
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."
}

View File

@ -32,5 +32,5 @@ export class RunnerGroupNeededError extends NotAcceptableError {
name = "RunnerGroupNeededError"
@IsString()
message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither."
message = "Runner's need to be part of one group (team or organisation)! \n You provided neither."
}

View File

@ -13,7 +13,7 @@ export class RunnerOrganisationNotFoundError extends NotFoundError {
}
/**
* Error to throw when two runner organisations' ids don't match.
* Error to throw when two runner organisation's ids don't match.
* Usually occurs when a user tries to change a runner organisation's id.
*/
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {

View File

@ -2,7 +2,7 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw, when a non-existant scan station get's loaded.
* Error to throw, when a non-existent scan station get's loaded.
*/
export class ScanStationNotFoundError extends NotFoundError {
@IsString()

View File

@ -2,7 +2,7 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw, when a non-existant stats client get's loaded.
* Error to throw, when a non-existent stats client get's loaded.
*/
export class StatsClientNotFoundError extends NotFoundError {
@IsString()

View File

@ -2,7 +2,7 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no groupname is set.
* Error to throw when no group name is set.
*/
export class GroupNameNeededError extends NotFoundError {
@IsString()
@ -13,7 +13,7 @@ export class GroupNameNeededError extends NotFoundError {
}
/**
* Error to throw when a usergroup couldn't be found.
* Error to throw when a user group couldn't be found.
*/
export class UserGroupNotFoundError extends NotFoundError {
@IsString()
@ -24,13 +24,13 @@ export class UserGroupNotFoundError extends NotFoundError {
}
/**
* Error to throw when two usergroups' ids don't match.
* Usually occurs when a user tries to change a usergroups's id.
* Error to throw when two user groups' ids don't match.
* Usually occurs when a user tries to change a user groups's id.
*/
export class UserGroupIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "UserGroupIdsNotMatchingError"
@IsString()
message = "The ids don't match!! \n If you wanted to change a usergroup's id: This isn't allowed!"
message = "The ids don't match!! \n If you wanted to change a user group's id: This isn't allowed!"
}

View File

@ -2,7 +2,7 @@ import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import express, { Application } from "express";
import path from 'path';
import { getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from "routing-controllers-openapi";
import { generateSpec } from '../apispec';
/**
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
@ -15,46 +15,7 @@ export default async (app: Application) => {
});
//Spec creation based on the previously created schemas
const spec = routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
},
"RefreshTokenCookie": {
"type": "apiKey",
"in": "cookie",
"name": "lfk_backend__refresh_token",
description: "A cookie containing a JWT based refreh token. Attention: Doesn't work in swagger-ui. Use /api/auth/login or /api/auth/refresh to get one."
},
"StatsApiToken": {
"type": "http",
"scheme": "bearer",
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."
}
}
},
info: {
description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API",
version: "0.0.8",
},
}
);
const spec = generateSpec(storage, schemas);
app.get(["/api/docs/openapi.json", "/api/docs/swagger.json"], (req, res) => {
res.json(spec);
});

View File

@ -1,8 +1,8 @@
import { Request, Response } from 'express';
/**
* Custom express middleware that appends the raw body to the request obeject.
* Mainly used for parsing csvs from boddies.
* Custom express middleware that appends the raw body to the request object.
* Mainly used for parsing csvs from bodies.
*/
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {

View File

@ -5,8 +5,9 @@ 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.
* This middleware handles the authentication of scan station api tokens.
* The tokens have to be provided via Bearer authorization header.
* You have to manually use this middleware via @UseBefore(ScanAuth) instead of using @Authorized().
* @param req Express request object.
* @param res Express response object.
* @param next Next function to call on success.
@ -31,7 +32,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
}
finally {
if (prefix == "" || prefix == undefined || prefix == null) {
res.status(401).send("Api token non-existant or invalid syntax.");
res.status(401).send("Api token non-existent or invalid syntax.");
return;
}
}
@ -45,7 +46,7 @@ const ScanAuth = async (req: Request, res: Response, next: () => void) => {
}
finally {
if (user_authorized == false) {
res.status(401).send("Api token non-existant or invalid syntax.");
res.status(401).send("Api token non-existent or invalid syntax.");
return;
}
else {

View File

@ -5,8 +5,9 @@ import { StatsClient } from '../models/entities/StatsClient';
import authchecker from './authchecker';
/**
* This middleware handels the authentification of stats client api tokens.
* The tokens have to be provided via Bearer auth header.
* This middleware handles the authentication of stats client api tokens.
* The tokens have to be provided via Bearer authorization header.
* You have to manually use this middleware via @UseBefore(StatsAuth) instead of using @Authorized().
* @param req Express request object.
* @param res Express response object.
* @param next Next function to call on success.

View File

@ -8,7 +8,7 @@ import { JwtCreator, JwtUser } from '../jwtcreator';
import { User } from '../models/entities/User';
/**
* Handels authorisation verification via jwt's for all api endpoints using the @Authorized decorator.
* Handles authentication via jwt's (Bearer authorization header) for all api endpoints using the @Authorized decorator.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
* @param permissions The permissions that the endpoint using @Authorized requires.
*/
@ -43,7 +43,7 @@ const authchecker = async (action: Action, permissions: string[] | string) => {
}
/**
* Handels soft-refreshing of access-tokens.
* Handles soft-refreshing of access-tokens.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
*/
const refresh = async (action: Action) => {

View File

@ -5,7 +5,7 @@ import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisation
import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunner } from './CreateRunner';
import { CreateRunner } from './create/CreateRunner';
/**
* Special class used to import runners from csv files - or json arrays created from csv to be exact.

View File

@ -5,7 +5,7 @@ import { config } from '../../config';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
import { JwtCreator } from "../../jwtcreator";
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
import { ResponseAuth } from '../responses/ResponseAuth';
/**
* This class is used to create refreshed auth credentials.
@ -24,8 +24,8 @@ export class RefreshAuth {
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
public async toAuth(): Promise<ResponseAuth> {
let newAuth: ResponseAuth = new ResponseAuth();
if (!this.token || this.token === undefined) {
throw new JwtNotProvidedError()
}

View File

@ -1,6 +1,6 @@
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
import { config } from '../../config';
import { Address } from '../entities/Address';
import { config } from '../../../config';
import { Address } from '../../entities/Address';
/**
* This classed is used to create a new Address entity from a json body (post request).
@ -56,7 +56,7 @@ export class CreateAddress {
/**
* Creates a new Address entity from this.
*/
public toAddress(): Address {
public async toEntity(): Promise<Address> {
let newAddress: Address = new Address();
newAddress.address1 = this.address1;

View File

@ -1,11 +1,11 @@
import * as argon2 from "argon2";
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { JwtCreator } from '../../jwtcreator';
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { JwtCreator } from '../../../jwtcreator';
import { User } from '../../entities/User';
import { ResponseAuth } from '../../responses/ResponseAuth';
/**
* This class is used to create auth credentials based on user credentials provided in a json body (post request).
@ -42,8 +42,8 @@ export class CreateAuth {
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
public async toAuth(): Promise<ResponseAuth> {
let newAuth: ResponseAuth = new ResponseAuth();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();

View File

@ -1,6 +1,6 @@
import { IsBoolean, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
import { Donor } from '../entities/Donor';
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
import { Donor } from '../../entities/Donor';
import { CreateParticipant } from './CreateParticipant';
/**
@ -18,7 +18,7 @@ export class CreateDonor extends CreateParticipant {
/**
* Creates a new Donor entity from this.
*/
public async toDonor(): Promise<Donor> {
public async toEntity(): Promise<Donor> {
let newDonor: Donor = new Donor();
newDonor.firstname = this.firstname;

View File

@ -1,85 +1,85 @@
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
import { GroupContact } from '../entities/GroupContact';
/**
* This classed is used to create a new Group entity from a json body (post request).
*/
export class CreateGroupContact {
/**
* The new contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The new contact's middle name.
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The new contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The new contact's address.
* Must be the address's id.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* The contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The contact's email address.
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* Gets the new contact's address by it's id.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
/**
* Creates a new Address entity from this.
*/
public async toGroupContact(): Promise<GroupContact> {
let contact: GroupContact = new GroupContact();
contact.firstname = this.firstname;
contact.middlename = this.middlename;
contact.lastname = this.lastname;
contact.email = this.email;
contact.phone = this.phone;
contact.address = await this.getAddress();
return null;
}
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config';
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
import { GroupContact } from '../../entities/GroupContact';
/**
* This classed is used to create a new Group entity from a json body (post request).
*/
export class CreateGroupContact {
/**
* The new contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The new contact's middle name.
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The new contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The new contact's address.
* Must be the address's id.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* The contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The contact's email address.
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* Gets the new contact's address by it's id.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
/**
* Creates a new Address entity from this.
*/
public async toEntity(): Promise<GroupContact> {
let contact: GroupContact = new GroupContact();
contact.firstname = this.firstname;
contact.middlename = this.middlename;
contact.lastname = this.lastname;
contact.email = this.email;
contact.phone = this.phone;
contact.address = await this.getAddress();
return null;
}
}

View File

@ -1,72 +1,72 @@
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
/**
* This classed is used to create a new Participant entity from a json body (post request).
*/
export abstract class CreateParticipant {
/**
* The new participant's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new participant's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new participant's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new participant's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsString()
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The new participant's e-mail address.
*/
@IsString()
@IsOptional()
@IsEmail()
email?: string;
/**
* The new participant's address.
* Must be of type number (address id).
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Gets the new participant's address by it's address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
import { IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../../config';
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
/**
* This classed is used to create a new Participant entity from a json body (post request).
*/
export abstract class CreateParticipant {
/**
* The new participant's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new participant's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new participant's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new participant's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsString()
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
phone?: string;
/**
* The new participant's e-mail address.
*/
@IsString()
@IsOptional()
@IsEmail()
email?: string;
/**
* The new participant's address.
* Must be of type number (address id).
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Gets the new participant's address by it's address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
if (!isNaN(this.address)) {
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address });
if (!address) { throw new AddressNotFoundError; }
return address;
}
throw new AddressWrongTypeError;
}
}

View File

@ -4,11 +4,11 @@ import {
IsNotEmpty
} from "class-validator";
import { getConnectionManager } from 'typeorm';
import { PrincipalNotFoundError } from '../../errors/PrincipalErrors';
import { Permission } from '../entities/Permission';
import { Principal } from '../entities/Principal';
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { PrincipalNotFoundError } from '../../../errors/PrincipalErrors';
import { Permission } from '../../entities/Permission';
import { Principal } from '../../entities/Principal';
import { PermissionAction } from '../../enums/PermissionAction';
import { PermissionTarget } from '../../enums/PermissionTargets';
/**
* This classed is used to create a new Permission entity from a json body (post request).
@ -39,7 +39,7 @@ export class CreatePermission {
/**
* Creates a new Permission entity from this.
*/
public async toPermission(): Promise<Permission> {
public async toEntity(): Promise<Permission> {
let newPermission: Permission = new Permission();
newPermission.principal = await this.getPrincipal();

View File

@ -1,9 +1,9 @@
import { IsEmail, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { JwtCreator } from '../../jwtcreator';
import { User } from '../entities/User';
import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { JwtCreator } from '../../../jwtcreator';
import { User } from '../../entities/User';
/**
* This calss is used to create password reset tokens for users.

View File

@ -1,10 +1,10 @@
import { IsInt } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
import { Runner } from '../entities/Runner';
import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { Runner } from '../../entities/Runner';
import { RunnerGroup } from '../../entities/RunnerGroup';
import { CreateParticipant } from './CreateParticipant';
/**
@ -21,7 +21,7 @@ export class CreateRunner extends CreateParticipant {
/**
* Creates a new Runner entity from this.
*/
public async toRunner(): Promise<Runner> {
public async toEntity(): Promise<Runner> {
let newRunner: Runner = new Runner();
newRunner.firstname = this.firstname;

View File

@ -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<RunnerCard> {
let newCard: RunnerCard = new RunnerCard();
newCard.enabled = this.enabled;
newCard.runner = await this.getRunner();
return newCard;
}
public async getRunner(): Promise<Runner> {
if (!this.runner) { return null; }
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,7 +1,7 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../errors/GroupContactErrors';
import { GroupContact } from '../entities/GroupContact';
import { GroupContactNotFoundError, GroupContactWrongTypeError } from '../../../errors/GroupContactErrors';
import { GroupContact } from '../../entities/GroupContact';
/**
* This classed is used to create a new RunnerGroup entity from a json body (post request).

View File

@ -1,8 +1,8 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { AddressNotFoundError, AddressWrongTypeError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
@ -36,7 +36,7 @@ export class CreateRunnerOrganisation extends CreateRunnerGroup {
/**
* Creates a new RunnerOrganisation entity from this.
*/
public async toRunnerOrganisation(): Promise<RunnerOrganisation> {
public async toEntity(): Promise<RunnerOrganisation> {
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();
newRunnerOrganisation.name = this.name;

View File

@ -1,9 +1,9 @@
import { IsInt, IsNotEmpty } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { RunnerTeam } from '../../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
@ -37,7 +37,7 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
/**
* Creates a new RunnerTeam entity from this.
*/
public async toRunnerTeam(): Promise<RunnerTeam> {
public async toEntity(): Promise<RunnerTeam> {
let newRunnerTeam: RunnerTeam = new RunnerTeam();
newRunnerTeam.name = this.name;

View File

@ -1,8 +1,8 @@
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';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { Runner } from '../../entities/Runner';
import { Scan } from '../../entities/Scan';
/**
* This class is used to create a new Scan entity from a json body (post request).
@ -36,7 +36,7 @@ export abstract class CreateScan {
/**
* Creates a new Scan entity from this.
*/
public async toScan(): Promise<Scan> {
public async toEntity(): Promise<Scan> {
let newScan = new Scan();
newScan.distance = this.distance;

View File

@ -3,9 +3,9 @@ import { IsBoolean, IsInt, IsOptional, IsPositive, IsString } from 'class-valida
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';
import { TrackNotFoundError } from '../../../errors/TrackErrors';
import { ScanStation } from '../../entities/ScanStation';
import { Track } from '../../entities/Track';
/**
* This class is used to create a new StatsClient entity from a json body (post request).

View File

@ -2,7 +2,7 @@ import * as argon2 from "argon2";
import { IsOptional, IsString } from 'class-validator';
import crypto from 'crypto';
import * as uuid from 'uuid';
import { StatsClient } from '../entities/StatsClient';
import { StatsClient } from '../../entities/StatsClient';
/**
* This classed is used to create a new StatsClient entity from a json body (post request).
@ -18,7 +18,7 @@ export class CreateStatsClient {
/**
* Converts this to a StatsClient entity.
*/
public async toStatsClient(): Promise<StatsClient> {
public async toEntity(): Promise<StatsClient> {
let newClient: StatsClient = new StatsClient();
newClient.description = this.description;

View File

@ -1,6 +1,6 @@
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
import { TrackLapTimeCantBeNegativeError } from '../../../errors/TrackErrors';
import { Track } from '../../entities/Track';
/**
* This classed is used to create a new Track entity from a json body (post request).
@ -31,7 +31,7 @@ export class CreateTrack {
/**
* Creates a new Track entity from this.
*/
public toTrack(): Track {
public toEntity(): Track {
let newTrack: Track = new Track();
newTrack.name = this.name;

View File

@ -1,41 +1,36 @@
import { IsNotEmpty } from 'class-validator';
import { IsInt, IsPositive } 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';
import { RunnerCardNotFoundError } from '../../../errors/RunnerCardErrors';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
import { RunnerCard } from '../../entities/RunnerCard';
import { ScanStation } from '../../entities/ScanStation';
import { TrackScan } from '../../entities/TrackScan';
/**
* 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;
export class CreateTrackScan {
/**
* The runnerCard associated with the scan.
* This get's saved for documentation and management purposes.
*/
@IsNotEmpty()
@IsInt()
@IsPositive()
card: number;
/**
* The scanning station that created the scan.
* Mainly used for logging and traceing back scans (or errors)
*/
@IsNotEmpty()
@IsInt()
@IsPositive()
station: number;
/**
* Creates a new Track entity from this.
*/
public async toScan(): Promise<TrackScan> {
public async toEntity(): Promise<TrackScan> {
let newScan: TrackScan = new TrackScan();
newScan.station = await this.getStation();
@ -48,7 +43,7 @@ export class CreateTrackScan extends CreateScan {
throw new RunnerNotFoundError();
}
newScan.timestamp = new Date(Date.now()).toString();
newScan.timestamp = Math.round(new Date().getTime() / 1000);
newScan.valid = await this.validateScan(newScan);
return newScan;
@ -57,25 +52,25 @@ export class CreateTrackScan extends CreateScan {
public async getCard(): Promise<RunnerCard> {
const track = await getConnection().getRepository(RunnerCard).findOne({ id: this.card }, { relations: ["runner"] });
if (!track) {
throw new Error();
throw new RunnerCardNotFoundError();
}
return track;
}
public async getStation(): Promise<ScanStation> {
const track = await getConnection().getRepository(ScanStation).findOne({ id: this.card }, { relations: ["track"] });
if (!track) {
throw new Error();
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station }, { relations: ["track"] });
if (!station) {
throw new ScanStationNotFoundError();
}
return track;
return station;
}
public async validateScan(scan: TrackScan): Promise<boolean> {
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner }, relations: ["track"] });
const scans = await getConnection().getRepository(TrackScan).find({ where: { runner: scan.runner, valid: true }, 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) {
const newestScan = scans[scans.length - 1];
if ((scan.timestamp - newestScan.timestamp) > scan.track.minimumLapTime) {
return true;
}

View File

@ -1,132 +1,132 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { config } from '../../config';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../errors/UserGroupErrors';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
/**
* This classed is used to create a new User entity from a json body (post request).
*/
export class CreateUser {
/**
* The new user's first name.
*/
@IsString()
firstname: string;
/**
* The new user's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new user's last name.
*/
@IsString()
lastname: string;
/**
* The new user's username.
* You have to provide at least one of: {email, username}.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The new user's email address.
* You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
@IsOptional()
email?: string;
/**
* The new user's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsPhoneNumber(config.phone_validation_countrycode)
@IsOptional()
phone?: string;
/**
* The new user's password.
* This will of course not be saved in plaintext :)
*/
@IsString()
password: string;
/**
* Will the new user be enabled from the start?
* Default: true
*/
@IsBoolean()
@IsOptional()
enabled?: boolean = true;
/**
* The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs.
*/
@IsOptional()
groups?: number[] | number
/**
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Converts this to a User entity.
*/
public async toUser(): Promise<User> {
let newUser: User = new User();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
newUser.email = this.email
newUser.username = this.username
newUser.firstname = this.firstname
newUser.middlename = this.middlename
newUser.lastname = this.lastname
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
newUser.enabled = this.enabled;
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { newUser.profilePic = this.profilePic; }
return newUser;
}
/**
* Get's all groups for this user by their id's;
*/
public async getGroups() {
if (!this.groups) { return null; }
let groups = new Array<UserGroup>();
if (!Array.isArray(this.groups)) {
this.groups = [this.groups]
}
for (let group of this.groups) {
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
if (!found) { throw new UserGroupNotFoundError(); }
groups.push(found);
}
return groups;
}
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import * as uuid from 'uuid';
import { config } from '../../../config';
import { UsernameOrEmailNeededError } from '../../../errors/UserErrors';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup';
/**
* This classed is used to create a new User entity from a json body (post request).
*/
export class CreateUser {
/**
* The new user's first name.
*/
@IsString()
firstname: string;
/**
* The new user's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new user's last name.
*/
@IsString()
lastname: string;
/**
* The new user's username.
* You have to provide at least one of: {email, username}.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The new user's email address.
* You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
@IsOptional()
email?: string;
/**
* The new user's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
*/
@IsPhoneNumber(config.phone_validation_countrycode)
@IsOptional()
phone?: string;
/**
* The new user's password.
* This will of course not be saved in plaintext :)
*/
@IsString()
password: string;
/**
* Will the new user be enabled from the start?
* Default: true
*/
@IsBoolean()
@IsOptional()
enabled?: boolean = true;
/**
* The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs.
*/
@IsOptional()
groups?: number[] | number
/**
* The user's profile pic (or rather a url pointing to it).
*/
@IsString()
@IsUrl()
@IsOptional()
profilePic?: string;
/**
* Converts this to a User entity.
*/
public async toEntity(): Promise<User> {
let newUser: User = new User();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
newUser.email = this.email
newUser.username = this.username
newUser.firstname = this.firstname
newUser.middlename = this.middlename
newUser.lastname = this.lastname
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
newUser.enabled = this.enabled;
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; }
else { newUser.profilePic = this.profilePic; }
return newUser;
}
/**
* Get's all groups for this user by their id's;
*/
public async getGroups() {
if (!this.groups) { return null; }
let groups = new Array<UserGroup>();
if (!Array.isArray(this.groups)) {
this.groups = [this.groups]
}
for (let group of this.groups) {
let found = await getConnectionManager().get().getRepository(UserGroup).findOne({ id: group });
if (!found) { throw new UserGroupNotFoundError(); }
groups.push(found);
}
return groups;
}
}

View File

@ -1,5 +1,5 @@
import { IsOptional, IsString } from 'class-validator';
import { UserGroup } from '../entities/UserGroup';
import { UserGroup } from '../../entities/UserGroup';
/**
* This classed is used to create a new UserGroup entity from a json body (post request).
@ -22,7 +22,7 @@ export class CreateUserGroup {
/**
* Creates a new UserGroup entity from this.
*/
public async toUserGroup(): Promise<UserGroup> {
public async toEntity(): Promise<UserGroup> {
let newUserGroup: UserGroup = new UserGroup();
newUserGroup.name = this.name;

View File

@ -1,7 +1,7 @@
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import { DonorReceiptAddressNeededError } from '../../errors/DonorErrors';
import { Donor } from '../entities/Donor';
import { CreateParticipant } from './CreateParticipant';
import { DonorReceiptAddressNeededError } from '../../../errors/DonorErrors';
import { Donor } from '../../entities/Donor';
import { CreateParticipant } from '../create/CreateParticipant';
/**
* This class is used to update a Donor entity (via put request).
@ -26,7 +26,7 @@ export class UpdateDonor extends CreateParticipant {
/**
* Updates a provided Donor entity based on this.
*/
public async updateDonor(donor: Donor): Promise<Donor> {
public async update(donor: Donor): Promise<Donor> {
donor.firstname = this.firstname;
donor.middlename = this.middlename;
donor.lastname = this.lastname;

View File

@ -1,11 +1,11 @@
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { PermissionNeedsPrincipalError } from '../../errors/PermissionErrors';
import { PrincipalNotFoundError, PrincipalWrongTypeError } from '../../errors/PrincipalErrors';
import { Permission } from '../entities/Permission';
import { Principal } from '../entities/Principal';
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { PermissionNeedsPrincipalError } from '../../../errors/PermissionErrors';
import { PrincipalNotFoundError, PrincipalWrongTypeError } from '../../../errors/PrincipalErrors';
import { Permission } from '../../entities/Permission';
import { Principal } from '../../entities/Principal';
import { PermissionAction } from '../../enums/PermissionAction';
import { PermissionTarget } from '../../enums/PermissionTargets';
/**
* This class is used to update a Permission entity (via put request).
@ -42,7 +42,7 @@ export class UpdatePermission {
/**
* Updates a provided Permission entity based on this.
*/
public async updatePermission(permission: Permission): Promise<Permission> {
public async update(permission: Permission): Promise<Permission> {
permission.principal = await this.getPrincipal();
permission.target = this.target;
permission.action = this.action;

View File

@ -1,11 +1,11 @@
import { IsInt, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors';
import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
import { Runner } from '../entities/Runner';
import { RunnerGroup } from '../entities/RunnerGroup';
import { CreateParticipant } from './CreateParticipant';
import { RunnerGroupNotFoundError } from '../../../errors/RunnerGroupErrors';
import { RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { Runner } from '../../entities/Runner';
import { RunnerGroup } from '../../entities/RunnerGroup';
import { CreateParticipant } from '../create/CreateParticipant';
/**
* This class is used to update a Runner entity (via put request).
@ -29,7 +29,7 @@ export class UpdateRunner extends CreateParticipant {
/**
* Updates a provided Runner entity based on this.
*/
public async updateRunner(runner: Runner): Promise<Runner> {
public async update(runner: Runner): Promise<Runner> {
runner.firstname = this.firstname;
runner.middlename = this.middlename;
runner.lastname = this.lastname;

View File

@ -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<RunnerCard> {
card.enabled = this.enabled;
card.runner = await this.getRunner();
return card;
}
public async getRunner(): Promise<Runner> {
if (!this.runner) { return null; }
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
}

View File

@ -1,9 +1,9 @@
import { IsInt, IsOptional } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { AddressNotFoundError } from '../../errors/AddressErrors';
import { Address } from '../entities/Address';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { CreateRunnerGroup } from './CreateRunnerGroup';
import { AddressNotFoundError } from '../../../errors/AddressErrors';
import { Address } from '../../entities/Address';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
/**
* This class is used to update a RunnerOrganisation entity (via put request).
@ -41,7 +41,7 @@ export class UpdateRunnerOrganisation extends CreateRunnerGroup {
/**
* Updates a provided RunnerOrganisation entity based on this.
*/
public async updateRunnerOrganisation(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
public async update(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
organisation.name = this.name;
organisation.contact = await this.getContact();

View File

@ -1,10 +1,10 @@
import { IsInt, IsNotEmpty, IsObject } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup';
import { RunnerOrganisationNotFoundError, RunnerOrganisationWrongTypeError } from '../../../errors/RunnerOrganisationErrors';
import { RunnerTeamNeedsParentError } from '../../../errors/RunnerTeamErrors';
import { RunnerOrganisation } from '../../entities/RunnerOrganisation';
import { RunnerTeam } from '../../entities/RunnerTeam';
import { CreateRunnerGroup } from '../create/CreateRunnerGroup';
/**
* This class is used to update a RunnerTeam entity (via put request).
@ -45,7 +45,7 @@ export class UpdateRunnerTeam extends CreateRunnerGroup {
/**
* Updates a provided RunnerTeam entity based on this.
*/
public async updateRunnerTeam(team: RunnerTeam): Promise<RunnerTeam> {
public async update(team: RunnerTeam): Promise<RunnerTeam> {
team.name = this.name;
team.parentGroup = await this.getParent();

View File

@ -1,8 +1,8 @@
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';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { Runner } from '../../entities/Runner';
import { Scan } from '../../entities/Scan';
/**
* This class is used to update a Scan entity (via put request)
@ -41,7 +41,7 @@ export abstract class UpdateScan {
* Update a Scan entity based on this.
* @param scan The scan that shall be updated.
*/
public async updateScan(scan: Scan): Promise<Scan> {
public async update(scan: Scan): Promise<Scan> {
scan.distance = this.distance;
scan.valid = this.valid;
scan.runner = await this.getRunner();

View File

@ -1,5 +1,5 @@
import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator';
import { ScanStation } from '../entities/ScanStation';
import { ScanStation } from '../../entities/ScanStation';
/**
* This class is used to update a ScanStation entity (via put request)
@ -30,7 +30,7 @@ export class UpdateScanStation {
* Update a ScanStation entity based on this.
* @param station The station that shall be updated.
*/
public async updateStation(station: ScanStation): Promise<ScanStation> {
public async update(station: ScanStation): Promise<ScanStation> {
station.description = this.description;
station.enabled = this.enabled;

View File

@ -1,6 +1,6 @@
import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
import { TrackLapTimeCantBeNegativeError } from '../../errors/TrackErrors';
import { Track } from '../entities/Track';
import { TrackLapTimeCantBeNegativeError } from '../../../errors/TrackErrors';
import { Track } from '../../entities/Track';
/**
* This class is used to update a Track entity (via put request).
@ -37,7 +37,7 @@ export class UpdateTrack {
* Update a Track entity based on this.
* @param track The track that shall be updated.
*/
public updateTrack(track: Track): Track {
public async update(track: Track): Promise<Track> {
track.name = this.name;
track.distance = this.distance;
track.minimumLapTime = this.minimumLapTime;

View File

@ -0,0 +1,81 @@
import { IsBoolean, IsInt, IsOptional } from 'class-validator';
import { getConnection } from 'typeorm';
import { RunnerNotFoundError } from '../../../errors/RunnerErrors';
import { ScanStationNotFoundError } from '../../../errors/ScanStationErrors';
import { Runner } from '../../entities/Runner';
import { ScanStation } from '../../entities/ScanStation';
import { TrackScan } from '../../entities/TrackScan';
/**
* This class is used to update a TrackScan entity (via put request)
*/
export abstract class UpdateTrackScan {
/**
* 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()
@IsOptional()
runner?: number;
/**
* Is the updated scan valid (for fraud reasons).
*/
@IsBoolean()
@IsOptional()
valid?: boolean = true;
/**
* The updated scan's associated station.
* This is important to link ran distances to runners.
*/
@IsInt()
@IsOptional()
public station?: number;
/**
* Update a TrackScan entity based on this.
* @param scan The scan that shall be updated.
*/
public async update(scan: TrackScan): Promise<TrackScan> {
scan.valid = this.valid;
if (this.runner) {
scan.runner = await this.getRunner();
}
if (this.station) {
scan.station = await this.getStation();
}
scan.track = scan.station.track;
return scan;
}
/**
* Gets a runner based on the runner id provided via this.runner.
*/
public async getRunner(): Promise<Runner> {
const runner = await getConnection().getRepository(Runner).findOne({ id: this.runner });
if (!runner) {
throw new RunnerNotFoundError();
}
return runner;
}
/**
* Gets a runner based on the runner id provided via this.runner.
*/
public async getStation(): Promise<ScanStation> {
const station = await getConnection().getRepository(ScanStation).findOne({ id: this.station });
if (!station) {
throw new ScanStationNotFoundError();
}
return station;
}
}

View File

@ -1,11 +1,11 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString, IsUrl } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { UsernameOrEmailNeededError } from '../../errors/AuthError';
import { UserGroupNotFoundError } from '../../errors/UserGroupErrors';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
import { config } from '../../../config';
import { UsernameOrEmailNeededError } from '../../../errors/AuthError';
import { UserGroupNotFoundError } from '../../../errors/UserGroupErrors';
import { User } from '../../entities/User';
import { UserGroup } from '../../entities/UserGroup';
/**
* This class is used to update a User entity (via put request).
@ -98,7 +98,7 @@ export class UpdateUser {
* Updates a user entity based on this.
* @param user The user that shall be updated.
*/
public async updateUser(user: User): Promise<User> {
public async update(user: User): Promise<User> {
user.email = this.email;
user.username = this.username;
if ((user.email === undefined || user.email === null) && (user.username === undefined || user.username === null)) {

View File

@ -0,0 +1,39 @@
import { IsInt, IsOptional, IsString } from 'class-validator';
import { UserGroup } from '../../entities/UserGroup';
/**
* This class is used to update a UserGroup entity (via put request).
*/
export class UpdateUserGroup {
/**
* The updated group'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 group's name.
*/
@IsString()
name: string;
/**
* The updated groups's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* Updates a group entity based on this.
* @param group The group that shall be updated.
*/
public async update(group: UserGroup): Promise<UserGroup> {
group.name = this.name;
group.description = this.description;
return group;
}
}

View File

@ -1,12 +1,13 @@
import {
IsBoolean,
IsEAN,
IsInt,
IsNotEmpty,
IsOptional,
IsString
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";
@ -32,17 +33,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
@ -58,10 +48,37 @@ 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 {
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;
}
/**
* Turns this entity into it's response class.
*/
public toResponse() {
return new Error("NotImplemented");
return new ResponseRunnerCard(this);
}
}

View File

@ -1,5 +1,4 @@
import {
IsDateString,
IsInt,
IsNotEmpty,
@ -57,9 +56,8 @@ export class TrackScan extends Scan {
* Will be used to implement fraud detection.
*/
@Column()
@IsDateString()
@IsNotEmpty()
timestamp: string;
@IsInt()
timestamp: number;
/**
* Turns this entity into it's response class.

View File

@ -12,5 +12,6 @@ export enum PermissionTarget {
STATSCLIENT = 'STATSCLIENT',
DONOR = 'DONOR',
SCAN = 'SCAN',
STATION = 'STATION'
STATION = 'STATION',
CARD = 'CARD'
}

View File

@ -3,7 +3,7 @@ import { IsInt, IsString } from 'class-validator';
/**
* Defines the repsonse auth.
*/
export class Auth {
export class ResponseAuth {
/**
* The access_token - JWT shortterm access token.
*/

View File

@ -0,0 +1,53 @@
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 | null;
/**
* 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;
if (!card.runner) { this.runner = null }
else { this.runner = card.runner.toResponse(); }
try {
this.code = card.code;
} catch (error) {
this.code = "0000000000000"
}
this.enabled = card.enabled;
}
}

View File

@ -1,8 +1,8 @@
import { IsDateString, IsNotEmpty } from "class-validator";
import { RunnerCard } from '../entities/RunnerCard';
import { ScanStation } from '../entities/ScanStation';
import { TrackScan } from '../entities/TrackScan';
import { ResponseRunnerCard } from './ResponseRunnerCard';
import { ResponseScan } from './ResponseScan';
import { ResponseScanStation } from './ResponseScanStation';
import { ResponseTrack } from './ResponseTrack';
/**
@ -19,20 +19,20 @@ export class ResponseTrackScan extends ResponseScan {
* The runnerCard associated with the scan.
*/
@IsNotEmpty()
card: RunnerCard;
card: ResponseRunnerCard;
/**
* The scanning station that created the scan.
*/
@IsNotEmpty()
station: ScanStation;
station: ResponseScanStation;
/**
* The scan's creation timestamp.
*/
@IsDateString()
@IsNotEmpty()
timestamp: string;
timestamp: number;
/**
* Creates a ResponseTrackScan object from a scan.
@ -41,8 +41,9 @@ export class ResponseTrackScan extends ResponseScan {
public constructor(scan: TrackScan) {
super(scan);
this.track = new ResponseTrack(scan.track);
this.card = scan.card;
this.station = scan.station;
this.card = scan.card.toResponse();
this.station = scan.station.toResponse();
this.timestamp = scan.timestamp;
this.distance = scan.distance;
}
}

View File

@ -1,8 +1,8 @@
import { Connection } from 'typeorm';
import { Factory, Seeder } from 'typeorm-seeding';
import { CreatePermission } from '../models/actions/CreatePermission';
import { CreateUser } from '../models/actions/CreateUser';
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
import { CreatePermission } from '../models/actions/create/CreatePermission';
import { CreateUser } from '../models/actions/create/CreateUser';
import { CreateUserGroup } from '../models/actions/create/CreateUserGroup';
import { Permission } from '../models/entities/Permission';
import { User } from '../models/entities/User';
import { UserGroup } from '../models/entities/UserGroup';
@ -24,7 +24,7 @@ export default class SeedUsers implements Seeder {
let adminGroup = new CreateUserGroup();
adminGroup.name = "ADMINS";
adminGroup.description = "Have all possible permissions";
return await connection.getRepository(UserGroup).save(await adminGroup.toUserGroup());
return await connection.getRepository(UserGroup).save(await adminGroup.toEntity());
}
public async createUser(connection: Connection, group: number) {
@ -34,7 +34,7 @@ export default class SeedUsers implements Seeder {
initialUser.username = "demo";
initialUser.password = "demo";
initialUser.groups = group;
return await connection.getRepository(User).save(await initialUser.toUser());
return await connection.getRepository(User).save(await initialUser.toEntity());
}
public async createPermissions(connection: Connection, principal: number) {
@ -45,7 +45,7 @@ export default class SeedUsers implements Seeder {
permission.target = <PermissionTarget>target;
permission.action = <PermissionAction>action;
permission.principal = principal;
await repo.save(await permission.toPermission());
await repo.save(await permission.toEntity());
}
}
}

View File

@ -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
});
});
});

View File

@ -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);
});
});

View File

@ -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");
});
});

View File

@ -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
});
});
});

View File

@ -84,6 +84,7 @@ describe('POST /api/scans successfully', () => {
"group": added_org.id
}, axios_config);
delete res2.data.group;
delete res2.data.distance;
added_runner = res2.data;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
@ -96,6 +97,7 @@ describe('POST /api/scans successfully', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,
@ -111,6 +113,7 @@ describe('POST /api/scans successfully', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,
@ -126,6 +129,7 @@ describe('POST /api/scans successfully', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,
@ -154,6 +158,7 @@ describe('POST /api/scans successfully via scan station', () => {
"group": added_org.id
}, axios_config);
delete res2.data.group;
delete res2.data.distance;
added_runner = res2.data;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
@ -186,6 +191,7 @@ describe('POST /api/scans successfully via scan station', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,
@ -204,6 +210,7 @@ describe('POST /api/scans successfully via scan station', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,
@ -222,6 +229,7 @@ describe('POST /api/scans successfully via scan station', () => {
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
delete res.data.id;
delete res.data.runner.distance;
expect(res.data).toEqual({
"runner": added_runner,
"distance": 200,

View File

@ -44,6 +44,7 @@ describe('DELETE scan', () => {
"distance": 1000
}, axios_config);
added_scan = res.data;
delete res.data.runner.distance;
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
@ -51,6 +52,7 @@ describe('DELETE scan', () => {
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")
delete res2.data.runner.distance;
expect(res2.data).toEqual(added_scan);
});
it('check if scan really was deleted', async () => {

View File

@ -100,6 +100,7 @@ describe('adding + updating successfilly', () => {
"group": added_org.id
}, axios_config);
delete res2.data.group;
delete res2.data.distance;
added_runner = res2.data;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
@ -121,6 +122,7 @@ describe('adding + updating successfilly', () => {
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
delete res2.data.runner.distance;
expect(res2.data).toEqual({
"id": added_scan.id,
"runner": added_runner,
@ -137,7 +139,8 @@ describe('adding + updating successfilly', () => {
"valid": false
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
expect(res2.headers['content-type']).toContain("application/json");
delete res2.data.runner.distance;
expect(res2.data).toEqual({
"id": added_scan.id,
"runner": added_runner,
@ -152,6 +155,7 @@ describe('adding + updating successfilly', () => {
"group": added_org.id
}, axios_config);
delete res2.data.group;
delete res2.data.distance;
added_runner2 = res2.data;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
@ -164,6 +168,7 @@ describe('adding + updating successfilly', () => {
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json");
delete res2.data.runner.distance;
expect(res2.data).toEqual({
"id": added_scan.id,
"runner": added_runner2,

View File

@ -0,0 +1,249 @@
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;
let added_card;
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);
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");
added_card = res.data;
});
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('no input should return 400', async () => {
const res = await axios.post(base + '/api/scans/trackscans', null, axios_config);
expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json")
});
it('no station should return 400', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id
}, axios_config);
expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json")
});
it('no card should return 400', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"station": added_station.id,
}, axios_config);
expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json")
});
it('invalid card input should return 404', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": 999999999999999999999999,
"station": added_station.id
}, axios_config);
expect(res.status).toEqual(404);
expect(res.headers['content-type']).toContain("application/json")
});
it('invalid station input should return 404', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": 999999999999999999999999
}, 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;
let added_card;
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);
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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, 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('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
});
it('creating a invalid scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
expect(res.data.valid).toEqual(false);
});
});
// ---------------
describe('POST /api/scans successfully via scan station', () => {
let added_org;
let added_runner;
let added_card;
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);
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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, 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('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
});
it('creating a invalid scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
expect(res.data.valid).toEqual(false);
});
});

View File

@ -0,0 +1,101 @@
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_card;
let added_track;
let added_station;
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('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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, 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('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
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);
});
});

View File

@ -0,0 +1,102 @@
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/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);
expect(res.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('adding + getting scans', () => {
let added_org;
let added_runner;
let added_card;
let added_track;
let added_station;
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('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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, 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('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
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");
});
});

View File

@ -0,0 +1,239 @@
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_card;
let added_track;
let added_station;
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('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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, 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('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
added_scan = 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/scans/trackscans/' + 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/trackscans/' + added_scan.id, {
"id": added_scan.id + 1,
"station": added_station.id,
"runner": added_runner.id
}, axios_config);
expect(res2.status).toEqual(406);
expect(res2.headers['content-type']).toContain("application/json")
});
it('updating with invalid station should return 404', async () => {
const res2 = await axios.put(base + '/api/scans/trackscans/' + added_scan.id, {
"id": added_scan.id,
"station": 9999999999999999,
"runner": added_runner.id
}, axios_config);
expect(res2.status).toEqual(404);
expect(res2.headers['content-type']).toContain("application/json")
});
it('updating with invalid runner should return 404', async () => {
const res2 = await axios.put(base + '/api/scans/trackscans/' + added_scan.id, {
"id": added_scan.id,
"station": added_station.id,
"runner": 9999999999999999999
}, 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_card;
let added_track;
let added_track2;
let added_station;
let added_station2;
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('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_runner2 = 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");
added_card = res.data;
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, axios_config);
added_track = res1.data
expect(res1.status).toEqual(200);
expect(res1.headers['content-type']).toContain("application/json")
});
it('creating a track should return 200', async () => {
const res1 = await axios.post(base + '/api/tracks', {
"name": "test123",
"distance": 123,
"minimumLapTime": 30
}, axios_config);
added_track2 = 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('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_station2 = res.data;
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
it('creating a scan should return 200', async () => {
const res = await axios.post(base + '/api/scans/trackscans', {
"card": added_card.id,
"station": added_station.id
}, {
headers: { "authorization": "Bearer " + added_station.key },
validateStatus: undefined
});
added_scan = res.data;
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json");
});
it('updating with new runner should return 200', async () => {
const res2 = await axios.put(base + '/api/scans/trackscans/' + added_scan.id, {
"id": added_scan.id,
"station": added_station.id,
"runner": added_runner2.id
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
});
it('updating with new station should return 200', async () => {
const res2 = await axios.put(base + '/api/scans/trackscans/' + added_scan.id, {
"id": added_scan.id,
"station": added_station2.id,
"runner": added_runner.id
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
});
it('updating with valid=false should return 200', async () => {
const res2 = await axios.put(base + '/api/scans/trackscans/' + added_scan.id, {
"id": added_scan.id,
"station": added_station2.id,
"runner": added_runner.id,
"valid": false
}, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
});
});