Merge pull request 'Merge for alpha 0.0.6' (#61) from dev into main
Reviewed-on: #61 Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
This commit is contained in:
commit
9cd181c5b8
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@odit/lfk-backend",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"main": "src/app.ts",
|
||||
"repository": "https://git.odit.services/lfk/backend",
|
||||
"author": {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import consola from "consola";
|
||||
import "reflect-metadata";
|
||||
import { createExpressServer } from "routing-controllers";
|
||||
import authchecker from "./authchecker";
|
||||
import { config, e as errors } from './config';
|
||||
import loaders from "./loaders/index";
|
||||
import authchecker from "./middlewares/authchecker";
|
||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||
|
||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post } 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 { StatsClient } from '../models/entities/StatsClient';
|
||||
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
|
||||
import { ResponseStatsClient } from '../models/responses/ResponseStatsClient';
|
||||
|
||||
@JsonController('/statsclients')
|
||||
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
export class StatsClientController {
|
||||
private clientRepository: Repository<StatsClient>;
|
||||
|
||||
/**
|
||||
* Gets the repository of this controller's model/entity.
|
||||
*/
|
||||
constructor() {
|
||||
this.clientRepository = getConnectionManager().get().getRepository(StatsClient);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authorized("STATSCLIENT:GET")
|
||||
@ResponseSchema(ResponseStatsClient, { isArray: true })
|
||||
@OpenAPI({ description: 'Lists all stats clients. Please remember that the key can only be viewed on creation.' })
|
||||
async getAll() {
|
||||
let responseClients: ResponseStatsClient[] = new Array<ResponseStatsClient>();
|
||||
const clients = await this.clientRepository.find();
|
||||
clients.forEach(clients => {
|
||||
responseClients.push(new ResponseStatsClient(clients));
|
||||
});
|
||||
return responseClients;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Authorized("STATSCLIENT:GET")
|
||||
@ResponseSchema(ResponseStatsClient)
|
||||
@ResponseSchema(StatsClientNotFoundError, { statusCode: 404 })
|
||||
@OnUndefined(StatsClientNotFoundError)
|
||||
@OpenAPI({ description: "Lists all information about the stats client whose id got provided. Please remember that the key can only be viewed on creation." })
|
||||
async getOne(@Param('id') id: number) {
|
||||
let client = await this.clientRepository.findOne({ id: id });
|
||||
if (!client) { throw new TrackNotFoundError(); }
|
||||
return new ResponseStatsClient(client);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authorized("STATSCLIENT:CREATE")
|
||||
@ResponseSchema(ResponseStatsClient)
|
||||
@OpenAPI({ description: "Create a new stats client. <br> Please remember that the client\'s key will be generated automaticly and that it can only be viewed on creation." })
|
||||
async post(
|
||||
@Body({ validate: true })
|
||||
client: CreateStatsClient
|
||||
) {
|
||||
let newClient = await this.clientRepository.save(await client.toStatsClient());
|
||||
let responseClient = new ResponseStatsClient(newClient);
|
||||
responseClient.key = newClient.cleartextkey;
|
||||
return responseClient;
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Authorized("STATSCLIENT:DELETE")
|
||||
@ResponseSchema(ResponseStatsClient)
|
||||
@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) {
|
||||
let client = await this.clientRepository.findOne({ id: id });
|
||||
if (!client) { return null; }
|
||||
|
||||
await this.clientRepository.delete(client);
|
||||
return new ResponseStatsClient(client);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { Get, JsonController, UseBefore } from 'routing-controllers';
|
||||
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
|
||||
import { getConnection } from 'typeorm';
|
||||
import StatsAuth from '../middlewares/StatsAuth';
|
||||
import { Donation } from '../models/entities/Donation';
|
||||
import { Runner } from '../models/entities/Runner';
|
||||
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
|
||||
import { RunnerTeam } from '../models/entities/RunnerTeam';
|
||||
import { Scan } from '../models/entities/Scan';
|
||||
import { User } from '../models/entities/User';
|
||||
import { ResponseStats } from '../models/responses/ResponseStats';
|
||||
import { ResponseStatsOrgnisation } from '../models/responses/ResponseStatsOrganisation';
|
||||
import { ResponseStatsRunner } from '../models/responses/ResponseStatsRunner';
|
||||
import { ResponseStatsTeam } from '../models/responses/ResponseStatsTeam';
|
||||
|
||||
@JsonController('/stats')
|
||||
export class StatsController {
|
||||
|
||||
@Get()
|
||||
@ResponseSchema(ResponseStats)
|
||||
@OpenAPI({ description: "A very basic stats endpoint providing basic counters for a dashboard or simmilar" })
|
||||
async get() {
|
||||
let connection = getConnection();
|
||||
let runners = await connection.getRepository(Runner).find({ relations: ['scans', 'scans.track'] });
|
||||
let teams = await connection.getRepository(RunnerTeam).find();
|
||||
let orgs = await connection.getRepository(RunnerOrganisation).find();
|
||||
let users = await connection.getRepository(User).find();
|
||||
let scans = await connection.getRepository(Scan).find();
|
||||
let donations = await connection.getRepository(Donation).find({ relations: ['runner', 'runner.scans', 'runner.scans.track'] });
|
||||
return new ResponseStats(runners, teams, orgs, users, scans, donations)
|
||||
}
|
||||
|
||||
@Get("/runners/distance")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDistance() {
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distance - runner2.distance).slice(0, 9);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
});
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Get("/runners/donations")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten runners by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByDonations() {
|
||||
let runners = await getConnection().getRepository(Runner).find({ relations: ['scans', 'group', 'distanceDonations', 'scans.track'] });
|
||||
let topRunners = runners.sort((runner1, runner2) => runner1.distanceDonationAmount - runner2.distanceDonationAmount).slice(0, 9);
|
||||
let responseRunners: ResponseStatsRunner[] = new Array<ResponseStatsRunner>();
|
||||
topRunners.forEach(runner => {
|
||||
responseRunners.push(new ResponseStatsRunner(runner));
|
||||
});
|
||||
return responseRunners;
|
||||
}
|
||||
|
||||
@Get("/scans")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsRunner, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten fastest track times (with their runner and the runner's group).", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopRunnersByTrackTime() {
|
||||
throw new Error("Not implemented yet.")
|
||||
}
|
||||
|
||||
@Get("/teams/distance")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDistance() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distance - team2.distance).slice(0, 9);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
});
|
||||
return responseTeams;
|
||||
}
|
||||
|
||||
@Get("/teams/donations")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsTeam, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten teams by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopTeamsByDonations() {
|
||||
let teams = await getConnection().getRepository(RunnerTeam).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track'] });
|
||||
let topTeams = teams.sort((team1, team2) => team1.distanceDonationAmount - team2.distanceDonationAmount).slice(0, 9);
|
||||
let responseTeams: ResponseStatsTeam[] = new Array<ResponseStatsTeam>();
|
||||
topTeams.forEach(team => {
|
||||
responseTeams.push(new ResponseStatsTeam(team));
|
||||
});
|
||||
return responseTeams;
|
||||
}
|
||||
|
||||
@Get("/organisations/distance")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organisations by distance.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDistance() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distance - org2.distance).slice(0, 9);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
});
|
||||
return responseOrgs;
|
||||
}
|
||||
|
||||
@Get("/organisations/donations")
|
||||
@UseBefore(StatsAuth)
|
||||
@ResponseSchema(ResponseStatsOrgnisation, { isArray: true })
|
||||
@OpenAPI({ description: "Returns the top ten organisations by donations.", security: [{ "StatsApiToken": [] }, { "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
|
||||
async getTopOrgsByDonations() {
|
||||
let orgs = await getConnection().getRepository(RunnerOrganisation).find({ relations: ['runners', 'runners.scans', 'runners.distanceDonations', 'runners.scans.track', 'teams', 'teams.runners', 'teams.runners.scans', 'teams.runners.distanceDonations', 'teams.runners.scans.track'] });
|
||||
let topOrgs = orgs.sort((org1, org2) => org1.distanceDonationAmount - org2.distanceDonationAmount).slice(0, 9);
|
||||
let responseOrgs: ResponseStatsOrgnisation[] = new Array<ResponseStatsOrgnisation>();
|
||||
topOrgs.forEach(org => {
|
||||
responseOrgs.push(new ResponseStatsOrgnisation(org));
|
||||
});
|
||||
return responseOrgs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { IsString } from 'class-validator';
|
||||
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
|
||||
|
||||
/**
|
||||
* Error to throw, when a non-existant stats client get's loaded.
|
||||
*/
|
||||
export class StatsClientNotFoundError extends NotFoundError {
|
||||
@IsString()
|
||||
name = "StatsClientNotFoundError"
|
||||
|
||||
@IsString()
|
||||
message = "The stats client you provided couldn't be located in the system. \n Please check your request."
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to throw when two stats clients' ids don't match.
|
||||
* Usually occurs when a user tries to change a stats client's id.
|
||||
*/
|
||||
export class StatsClientIdsNotMatchingError extends NotAcceptableError {
|
||||
@IsString()
|
||||
name = "StatsClientIdsNotMatchingError"
|
||||
|
||||
@IsString()
|
||||
message = "The ids don't match! \n And if you wanted to change a stats client's id: This isn't allowed!"
|
||||
}
|
|
@ -35,6 +35,11 @@ export default async (app: Application) => {
|
|||
"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)."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import * as argon2 from "argon2";
|
||||
import { Request, Response } from 'express';
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
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.
|
||||
* @param req Express request object.
|
||||
* @param res Express response object.
|
||||
* @param next Next function to call on success.
|
||||
*/
|
||||
const StatsAuth = async (req: Request, res: Response, next: () => void) => {
|
||||
let provided_token: string = req.headers["authorization"];
|
||||
if (provided_token == "" || provided_token === undefined || provided_token === null) {
|
||||
res.status(401).send("No api token provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
provided_token = provided_token.replace("Bearer ", "");
|
||||
} catch (error) {
|
||||
res.status(401).send("No valid jwt or api token provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = "";
|
||||
try {
|
||||
prefix = provided_token.split(".")[0];
|
||||
}
|
||||
finally {
|
||||
if (prefix == "" || prefix == undefined || prefix == null) {
|
||||
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const client = await getConnectionManager().get().getRepository(StatsClient).findOne({ prefix: prefix });
|
||||
if (!client) {
|
||||
let user_authorized = false;
|
||||
try {
|
||||
let action = { request: req, response: res, context: null, next: next }
|
||||
user_authorized = await authchecker(action, ["RUNNER:GET", "TEAM:GET", "ORGANISATION:GET"]);
|
||||
}
|
||||
finally {
|
||||
if (user_authorized == false) {
|
||||
res.status(401).send("Api token non-existant or invalid syntax.");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!(await argon2.verify(client.key, provided_token))) {
|
||||
res.status(401).send("Api token invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
export default StatsAuth;
|
|
@ -2,10 +2,10 @@ import cookie from "cookie";
|
|||
import * as jwt from "jsonwebtoken";
|
||||
import { Action } from "routing-controllers";
|
||||
import { getConnectionManager } from 'typeorm';
|
||||
import { config } from './config';
|
||||
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
|
||||
import { JwtCreator, JwtUser } from './jwtcreator';
|
||||
import { User } from './models/entities/User';
|
||||
import { config } from '../config';
|
||||
import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from '../errors/AuthError';
|
||||
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.
|
|
@ -0,0 +1,33 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* This classed is used to create a new StatsClient entity from a json body (post request).
|
||||
*/
|
||||
export class CreateStatsClient {
|
||||
/**
|
||||
* The new client's description.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Converts this to a StatsClient entity.
|
||||
*/
|
||||
public async toStatsClient(): Promise<StatsClient> {
|
||||
let newClient: StatsClient = new StatsClient();
|
||||
|
||||
newClient.description = this.description;
|
||||
|
||||
let newUUID = uuid.v4().toUpperCase();
|
||||
newClient.prefix = crypto.createHash("sha3-512").update(newUUID).digest('hex').substring(0, 7).toUpperCase();
|
||||
newClient.key = await argon2.hash(newClient.prefix + "." + newUUID);
|
||||
newClient.cleartextkey = newClient.prefix + "." + newUUID;
|
||||
|
||||
return newClient;
|
||||
}
|
||||
}
|
|
@ -31,5 +31,5 @@ export abstract class Donation {
|
|||
* The donation's amount in cents (or whatever your currency's smallest unit is.).
|
||||
* The exact implementation may differ for each type of donation.
|
||||
*/
|
||||
abstract amount: number | Promise<number>;
|
||||
abstract amount: number;
|
||||
}
|
|
@ -58,4 +58,12 @@ export class Runner extends Participant {
|
|||
public get distance(): number {
|
||||
return this.validScans.reduce((sum, current) => sum + current.distance, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distanceDonationAmount(): number {
|
||||
return this.distanceDonations.reduce((sum, current) => sum + current.amountPerDistance, 0) * this.distance;
|
||||
}
|
||||
}
|
|
@ -44,4 +44,20 @@ export abstract class RunnerGroup {
|
|||
*/
|
||||
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
|
||||
runners: Runner[];
|
||||
|
||||
/**
|
||||
* Returns the total distance ran by this group's runners based on all their valid scans.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distance(): number {
|
||||
return this.runners.reduce((sum, current) => sum + current.distance, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distanceDonationAmount(): number {
|
||||
return this.runners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { IsOptional } from "class-validator";
|
||||
import { IsInt, IsOptional } from "class-validator";
|
||||
import { ChildEntity, ManyToOne, OneToMany } from "typeorm";
|
||||
import { Address } from "./Address";
|
||||
import { Runner } from './Runner';
|
||||
import { RunnerGroup } from "./RunnerGroup";
|
||||
import { RunnerTeam } from "./RunnerTeam";
|
||||
|
||||
|
@ -24,4 +25,32 @@ export class RunnerOrganisation extends RunnerGroup {
|
|||
*/
|
||||
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
|
||||
teams: RunnerTeam[];
|
||||
|
||||
/**
|
||||
* Returns all runners associated with this organisation (directly or indirectly via teams).
|
||||
*/
|
||||
public get allRunners(): Runner[] {
|
||||
let returnRunners: Runner[] = new Array<Runner>();
|
||||
returnRunners.push(...this.runners);
|
||||
for (let team of this.teams) {
|
||||
returnRunners.push(...team.runners)
|
||||
}
|
||||
return returnRunners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total distance ran by this group's runners based on all their valid scans.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distance(): number {
|
||||
return this.allRunners.reduce((sum, current) => sum + current.distance, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total donations a runner has collected based on his linked donations and distance ran.
|
||||
*/
|
||||
@IsInt()
|
||||
public get distanceDonationAmount(): number {
|
||||
return this.allRunners.reduce((sum, current) => sum + current.distanceDonationAmount, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
/**
|
||||
* Defines the StatsClient entity.
|
||||
* StatsClients can be used to access the protected parts of the stats api (top runners, donators and so on).
|
||||
*/
|
||||
@Entity()
|
||||
export class StatsClient {
|
||||
/**
|
||||
* Autogenerated unique id (primary key).
|
||||
*/
|
||||
@PrimaryGeneratedColumn()
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The clients's description.
|
||||
* Mostly for better UX when traceing back stuff.
|
||||
*/
|
||||
@Column({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The client's api key prefix.
|
||||
* This is used identitfy a client by it's api key.
|
||||
*/
|
||||
@Column({ unique: true })
|
||||
@IsString()
|
||||
prefix: string;
|
||||
|
||||
/**
|
||||
* The client's api key hash.
|
||||
* The api key can be used to authenticate against the /stats/** routes.
|
||||
*/
|
||||
@Column()
|
||||
@IsString()
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The client's api key in plain text.
|
||||
* This will only be used to display the full key on creation and updates.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
cleartextkey?: string;
|
||||
}
|
|
@ -8,5 +8,6 @@ export enum PermissionTarget {
|
|||
TRACK = 'TRACK',
|
||||
USER = 'USER',
|
||||
USERGROUP = 'USERGROUP',
|
||||
PERMISSION = 'PERMISSION'
|
||||
PERMISSION = 'PERMISSION',
|
||||
STATSCLIENT = 'STATSCLIENT'
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
IsInt
|
||||
} from "class-validator";
|
||||
import { Donation } from '../entities/Donation';
|
||||
import { Runner } from '../entities/Runner';
|
||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||
import { Scan } from '../entities/Scan';
|
||||
import { User } from '../entities/User';
|
||||
|
||||
/**
|
||||
* Defines the stats response.
|
||||
* The stats response calculates some basic stats for a dashboard or public display.
|
||||
*/
|
||||
export class ResponseStats {
|
||||
/**
|
||||
* The amount of runners registered in the system.
|
||||
*/
|
||||
@IsInt()
|
||||
total_runners: number;
|
||||
|
||||
/**
|
||||
* The amount of teams registered in the system.
|
||||
*/
|
||||
@IsInt()
|
||||
total_teams: number;
|
||||
|
||||
/**
|
||||
* The amount of organisations registered in the system.
|
||||
*/
|
||||
@IsInt()
|
||||
total_orgs: number;
|
||||
|
||||
/**
|
||||
* The amount of users registered in the system.
|
||||
*/
|
||||
@IsInt()
|
||||
total_users: number;
|
||||
|
||||
/**
|
||||
* The amount of valid scans registered in the system.
|
||||
*/
|
||||
@IsInt()
|
||||
total_scans: number;
|
||||
|
||||
/**
|
||||
* The total distance that all runners ran combined.
|
||||
*/
|
||||
@IsInt()
|
||||
total_distance: number;
|
||||
|
||||
/**
|
||||
* The total donation amount.
|
||||
*/
|
||||
@IsInt()
|
||||
total_donation: number;
|
||||
|
||||
/**
|
||||
* The average distance ran per runner.
|
||||
*/
|
||||
@IsInt()
|
||||
average_distance: number;
|
||||
|
||||
/**
|
||||
* Creates a new stats response containing some basic statistics for a dashboard or public display.
|
||||
* @param runners Array containing all runners - the following relations have to be resolved: scans, scans.track
|
||||
* @param teams Array containing all teams - no relations have to be resolved.
|
||||
* @param orgs Array containing all orgs - no relations have to be resolved.
|
||||
* @param users Array containing all users - no relations have to be resolved.
|
||||
* @param scans Array containing all scans - no relations have to be resolved.
|
||||
* @param donations Array containing all donations - the following relations have to be resolved: runner, runner.scans, runner.scans.track
|
||||
*/
|
||||
public constructor(runners: Runner[], teams: RunnerTeam[], orgs: RunnerOrganisation[], users: User[], scans: Scan[], donations: Donation[]) {
|
||||
this.total_runners = runners.length;
|
||||
this.total_teams = teams.length;
|
||||
this.total_orgs = orgs.length;
|
||||
this.total_users = users.length;
|
||||
this.total_scans = scans.filter(scan => { scan.valid === true }).length;
|
||||
this.total_distance = runners.reduce((sum, current) => sum + current.distance, 0);
|
||||
this.total_donation = donations.reduce((sum, current) => sum + current.amount, 0);
|
||||
this.average_distance = this.total_distance / this.total_runners;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
|
||||
IsInt,
|
||||
|
||||
IsNotEmpty,
|
||||
|
||||
IsOptional,
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { StatsClient } from '../entities/StatsClient';
|
||||
|
||||
/**
|
||||
* Defines the statsClient response.
|
||||
*/
|
||||
export class ResponseStatsClient {
|
||||
/**
|
||||
* The client's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The client's description.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The client's api key.
|
||||
* Only visible on creation or regeneration.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
key: string;
|
||||
|
||||
/**
|
||||
* The client's api key prefix.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
prefix: string;
|
||||
|
||||
/**
|
||||
* Creates a ResponseStatsClient object from a statsClient.
|
||||
* @param client The statsClient the response shall be build for.
|
||||
*/
|
||||
public constructor(client: StatsClient) {
|
||||
this.id = client.id;
|
||||
this.description = client.description;
|
||||
this.prefix = client.prefix;
|
||||
this.key = "Only visible on creation.";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
IsInt,
|
||||
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
|
||||
|
||||
/**
|
||||
* Defines the org stats response.
|
||||
* This differs from the normal org responce.
|
||||
*/
|
||||
export class ResponseStatsOrgnisation {
|
||||
/**
|
||||
* The orgs's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The orgs's name.
|
||||
*/
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The orgs's runner's currently ran distance in meters.
|
||||
*/
|
||||
@IsInt()
|
||||
distance: number;
|
||||
|
||||
/**
|
||||
* The orgs's currently collected donations.
|
||||
*/
|
||||
@IsInt()
|
||||
donationAmount: number;
|
||||
|
||||
/**
|
||||
* Creates a new organisation stats response from a organisation
|
||||
* @param org The organisation whoes response shall be generated - the following relations have to be resolved: runners, runners.scans, runners.distanceDonations, runners.scans.track, teams, teams.runners, teams.runners.scans, teams.runners.distanceDonations, teams.runners.scans.track
|
||||
*/
|
||||
public constructor(org: RunnerOrganisation) {
|
||||
this.name = org.name;
|
||||
this.id = org.id;
|
||||
this.distance = org.distance;
|
||||
this.donationAmount = org.distanceDonationAmount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import {
|
||||
IsInt,
|
||||
IsObject,
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { Runner } from '../entities/Runner';
|
||||
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||
|
||||
/**
|
||||
* Defines the runner stats response.
|
||||
* This differs from the normal runner responce.
|
||||
*/
|
||||
export class ResponseStatsRunner {
|
||||
/**
|
||||
* The runner's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The runner's first name.
|
||||
*/
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
/**
|
||||
* The runner's middle name.
|
||||
*/
|
||||
@IsString()
|
||||
middlename?: string;
|
||||
|
||||
/**
|
||||
* The runner's last name.
|
||||
*/
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
/**
|
||||
* The runner's currently ran distance in meters.
|
||||
*/
|
||||
@IsInt()
|
||||
distance: number;
|
||||
|
||||
/**
|
||||
* The runner's currently collected donations.
|
||||
*/
|
||||
@IsInt()
|
||||
donationAmount: number;
|
||||
|
||||
/**
|
||||
* The runner's group.
|
||||
*/
|
||||
@IsObject()
|
||||
group: RunnerGroup;
|
||||
|
||||
/**
|
||||
* Creates a new runner stats response from a runner
|
||||
* @param runner The runner whoes response shall be generated - the following relations have to be resolved: scans, group, distanceDonations, scans.track
|
||||
*/
|
||||
public constructor(runner: Runner) {
|
||||
this.id = runner.id;
|
||||
this.firstname = runner.firstname;
|
||||
this.middlename = runner.middlename;
|
||||
this.lastname = runner.lastname;
|
||||
this.distance = runner.distance;
|
||||
this.donationAmount = runner.distanceDonationAmount;
|
||||
this.group = runner.group;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
IsInt,
|
||||
IsObject,
|
||||
IsString
|
||||
} from "class-validator";
|
||||
import { RunnerGroup } from '../entities/RunnerGroup';
|
||||
import { RunnerTeam } from '../entities/RunnerTeam';
|
||||
|
||||
/**
|
||||
* Defines the team stats response.
|
||||
* This differs from the normal team responce.
|
||||
*/
|
||||
export class ResponseStatsTeam {
|
||||
/**
|
||||
* The team's id.
|
||||
*/
|
||||
@IsInt()
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The team's name.
|
||||
*/
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The teams's currently ran distance in meters.
|
||||
*/
|
||||
@IsInt()
|
||||
distance: number;
|
||||
|
||||
/**
|
||||
* The teams's currently collected donations.
|
||||
*/
|
||||
@IsInt()
|
||||
donationAmount: number;
|
||||
|
||||
/**
|
||||
* The teams's parent group.
|
||||
*/
|
||||
@IsObject()
|
||||
parent: RunnerGroup;
|
||||
|
||||
/**
|
||||
* Creates a new team stats response from a team
|
||||
* @param team The team whoes response shall be generated - the following relations have to be resolved: runners, runners.scans, runners.distanceDonations, runners.scans.track
|
||||
*/
|
||||
public constructor(team: RunnerTeam) {
|
||||
this.name = team.name;
|
||||
this.id = team.id;
|
||||
this.parent = team.parentGroup;
|
||||
this.distance = team.distance;
|
||||
this.donationAmount = team.distanceDonationAmount;
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@ import fs from "fs";
|
|||
import "reflect-metadata";
|
||||
import { createExpressServer, getMetadataArgsStorage } from "routing-controllers";
|
||||
import { routingControllersToSpec } from 'routing-controllers-openapi';
|
||||
import authchecker from "./authchecker";
|
||||
import { config } from './config';
|
||||
import authchecker from "./middlewares/authchecker";
|
||||
import { ErrorHandler } from './middlewares/ErrorHandler';
|
||||
|
||||
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
|
||||
|
@ -44,6 +44,11 @@ const spec = routingControllersToSpec(
|
|||
"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)."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue