Compare commits

..

No commits in common. "1bf6d3d56422f6cb8b9fb0e0c39a8b8f4f30563b" and "721af329893f6c18c378491b61bae053722f41e6" have entirely different histories.

112 changed files with 1490 additions and 4210 deletions

View File

@ -1,111 +0,0 @@
---
kind: pipeline
name: tests:node_latest
clone:
disable: true
steps:
- name: checkout pr
image: alpine/git
commands:
- git clone $DRONE_REMOTE_URL .
- git checkout $DRONE_SOURCE_BRANCH
- mv .env.ci .env
- name: run tests
image: node:alpine
commands:
- yarn
- yarn test:ci
trigger:
event:
- pull_request
---
kind: pipeline
type: docker
name: build:dev
steps:
- name: build dev
image: plugins/docker
depends_on: [clone]
settings:
username:
from_secret: DOCKER_REGISTRY_USER
password:
from_secret: DOCKER_REGISTRY_PASSWORD
repo: registry.odit.services/lfk/backend
tags:
- dev
registry: registry.odit.services
when:
branch:
- dev
event:
- push
trigger:
branch:
- dev
event:
- push
---
kind: pipeline
type: docker
name: build:latest
steps:
- name: build latest
image: plugins/docker
depends_on: [clone]
settings:
username:
from_secret: DOCKER_REGISTRY_USER
password:
from_secret: DOCKER_REGISTRY_PASSWORD
repo: registry.odit.services/lfk/backend
tags:
- latest
registry: registry.odit.services
trigger:
branch:
- main
event:
- push
---
kind: pipeline
type: docker
name: build:tags
steps:
- name: build $DRONE_TAG
image: plugins/docker
depends_on: [clone]
settings:
username:
from_secret: DOCKER_REGISTRY_USER
password:
from_secret: DOCKER_REGISTRY_PASSWORD
repo: registry.odit.services/lfk/backend
tags:
- $DRONE_TAG
registry: registry.odit.services
- name: trigger lib build
depends_on: [clone]
image: plugins/downstream
settings:
server: https://ci.odit.services/
token:
from_secret: BOT_DRONE_KEY
fork: false
repositories:
- lfk/lib
params:
- SOURCE_TAG: $DRONE_TAG
trigger:
branch:
- main
event:
- tag

View File

@ -1,8 +0,0 @@
APP_PORT=4010
DB_TYPE=sqlite
DB_HOST=unused
DB_PORT=unused
DB_USER=unused
DB_PASSWORD=bla
DB_NAME=./test.sqlite
NODE_ENV=dev

1
.gitignore vendored
View File

@ -133,4 +133,3 @@ build
*.sqlite
*.sqlite-jurnal
docs
lib

View File

@ -1,16 +1,6 @@
# Typescript Build
FROM node:14.15.1-alpine3.12
FROM node:alpine
WORKDIR /app
COPY package.json ./
RUN npm i -g pnpm
RUN pnpm i
COPY tsconfig.json ormconfig.js ./
COPY src ./src
RUN pnpm run build
# final image
FROM node:14.15.1-alpine3.12
COPY package.json ormconfig.js ./
RUN npm i -g pnpm
RUN pnpm i --prod
COPY --from=0 /app/dist dist
ENTRYPOINT ["node", "dist/app.js"]
COPY ./package.json ./
RUN yarn
COPY ./ ./
ENTRYPOINT [ "yarn","dev" ]

View File

@ -6,26 +6,18 @@ services:
- 4010:4010
environment:
APP_PORT: 4010
DB_TYPE: sqlite
DB_HOST: bla
DB_PORT: bla
DB_USER: bla
DB_PASSWORD: bla
DB_NAME: dev.sqlite
DB_TYPE: postgres
DB_HOST: backend_db
DB_PORT: 5432
DB_USER: lfk
DB_PASSWORD: changeme
DB_NAME: lfk
NODE_ENV: production
# APP_PORT: 4010
# DB_TYPE: postgres
# DB_HOST: backend_db
# DB_PORT: 5432
# DB_USER: lfk
# DB_PASSWORD: changeme
# DB_NAME: lfk
# NODE_ENV: production
# backend_db:
# image: postgres:11-alpine
# environment:
# POSTGRES_DB: lfk
# POSTGRES_PASSWORD: changeme
# POSTGRES_USER: lfk
# ports:
# - 5432:5432
backend_db:
image: postgres:11-alpine
environment:
POSTGRES_DB: lfk
POSTGRES_PASSWORD: changeme
POSTGRES_USER: lfk
ports:
- 5432:5432

View File

@ -1,4 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View File

@ -1,16 +0,0 @@
const dotenv = require('dotenv');
dotenv.config();
//
const SOURCE_PATH = process.env.NODE_ENV === 'production' ? 'dist' : 'src';
module.exports = {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// entities: ["src/**/entities/*.ts"],
entities: [ `${SOURCE_PATH}/**/entities/*{.ts,.js}` ],
seeds: [ `${SOURCE_PATH}/**/seeds/*{.ts,.js}` ]
// seeds: ['src/seeds/*.ts'],
};

12
ormconfig.ts Normal file
View File

@ -0,0 +1,12 @@
import { config } from 'dotenv';
config();
export default {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ["src/models/entities/*.ts"]
};

View File

@ -28,40 +28,33 @@
"class-validator": "^0.12.2",
"class-validator-jsonschema": "^2.0.3",
"consola": "^2.15.0",
"cookie": "^0.4.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"csvtojson": "^2.0.10",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"helmet": "^4.2.0",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql": "^2.18.1",
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0-alpha.6",
"routing-controllers-openapi": "^2.1.0",
"sqlite3": "^5.0.0",
"swagger-ui-express": "^4.1.5",
"typeorm": "^0.2.29",
"typeorm-routing-controllers-extensions": "^0.2.0",
"typeorm-seeding": "^1.6.1",
"uuid": "^8.3.1"
},
"devDependencies": {
"@types/cors": "^2.8.8",
"@types/csvtojson": "^1.1.5",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.9",
"@types/jest": "^26.0.16",
"@types/jsonwebtoken": "^8.5.0",
"@types/multer": "^1.4.4",
"@types/node": "^14.14.9",
"@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"axios": "^0.21.0",
"jest": "^26.6.3",
"nodemon": "^2.0.6",
"rimraf": "^2.7.1",
"start-server-and-test": "^1.11.6",
"ts-jest": "^26.4.4",
"sqlite3": "^5.0.0",
"ts-node": "^9.0.0",
"typedoc": "^0.19.2",
"typescript": "^4.1.2"
@ -69,17 +62,6 @@
"scripts": {
"dev": "nodemon src/app.ts",
"build": "tsc",
"docs": "typedoc --out docs src",
"test": "jest",
"test:watch": "jest --watchAll",
"test:ci": "start-server-and-test dev http://localhost:4010/api/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 src/openapi_export.ts"
},
"nodemonConfig": {
"ignore": [
"src/tests/*",
"docs/*"
]
"docs": "typedoc --out docs src"
}
}

View File

@ -6,14 +6,13 @@ import { config, e as errors } from './config';
import loaders from "./loaders/index";
import { ErrorHandler } from './middlewares/ErrorHandler';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
const app = createExpressServer({
authorizationChecker: authchecker,
middlewares: [ErrorHandler],
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
controllers: [__dirname + "/controllers/*.ts"],
});
async function main() {

View File

@ -1,72 +1,51 @@
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, 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.
* @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.
*/
const authchecker = async (action: Action, permissions: string[] | string) => {
let required_permissions = undefined;
// -----------
const authchecker = async (action: Action, permissions: string | string[]) => {
let required_permissions = undefined
if (typeof permissions === "string") {
required_permissions = [permissions]
} else {
required_permissions = permissions
}
// const token = action.request.headers["authorization"];
const provided_token = action.request.query["auth"];
let jwtPayload = undefined
try {
let provided_token = "" + action.request.headers["authorization"].replace("Bearer ", "");
jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret);
jwtPayload = jwtPayload["userdetails"];
} catch (error) {
jwtPayload = await refresh(action);
throw new IllegalJWTError()
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
if (!jwtPayload["permissions"]) { throw new NoPermissionError(); }
const count = await getConnectionManager().get().getRepository(User).count({ id: jwtPayload["userdetails"]["id"], refreshTokenCount: jwtPayload["userdetails"]["refreshTokenCount"] })
if (count !== 1) {
throw new UserNonexistantOrRefreshtokenInvalidError()
}
if (jwtPayload.permissions) {
action.response.local = {}
action.response.local.jwtPayload = jwtPayload;
for (let required_permission of required_permissions) {
if (!(jwtPayload["permissions"].includes(required_permission))) { return false; }
}
action.response.local.jwtPayload = jwtPayload.permissions
required_permissions.forEach(r => {
const permission_key = r.split(":")[0]
const actual_accesslevel_for_permission = jwtPayload.permissions[permission_key]
const permission_access_level = r.split(":")[1]
if (actual_accesslevel_for_permission.includes(permission_access_level)) {
return true;
}
/**
* Handels 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) => {
let refresh_token = undefined;
try {
refresh_token = cookie.parse(action.request.headers["cookie"])["lfk_backend__refresh_token"];
} else {
throw new NoPermissionError()
}
catch {
throw new IllegalJWTError();
});
} else {
throw new NoPermissionError()
}
let jwtPayload = undefined;
//
try {
jwtPayload = <any>jwt.verify(refresh_token, config.jwt_secret);
jwt.verify(provided_token, config.jwt_secret);
return true
} catch (error) {
throw new IllegalJWTError();
return false
}
const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] })
if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() }
let newAccess = JwtCreator.createAccess(user);
action.response.header("authorization", "Bearer " + newAccess);
return await new JwtUser(user);
}
export default authchecker

View File

@ -1,4 +1,4 @@
import { Body, CookieParam, JsonController, Post, Res } from 'routing-controllers';
import { Body, JsonController, Post } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
import { UserNotFoundError } from '../errors/UserErrors';
@ -21,16 +21,14 @@ export class AuthController {
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async login(@Body({ validate: true }) createAuth: CreateAuth, @Res() response: any) {
async login(@Body({ validate: true }) createAuth: CreateAuth) {
let auth;
try {
auth = await createAuth.toAuth();
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
return response.send(auth)
} catch (error) {
throw error;
}
return auth
}
@Post("/logout")
@ -41,20 +39,14 @@ export class AuthController {
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async logout(@Body({ validate: true }) handleLogout: HandleLogout, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
if (refresh_token && refresh_token.length != 0 && handleLogout.token == undefined) {
handleLogout.token = refresh_token;
}
async logout(@Body({ validate: true }) handleLogout: HandleLogout) {
let logout;
try {
logout = await handleLogout.logout()
await response.cookie('lfk_backend__refresh_token', "expired", { expires: new Date(Date.now()), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', "expired", { expires: new Date(Date.now()), httpOnly: true });
} catch (error) {
throw error;
return error;
}
return response.send(logout)
return logout
}
@Post("/refresh")
@ -64,18 +56,13 @@ export class AuthController {
@ResponseSchema(UserNotFoundError)
@ResponseSchema(RefreshTokenCountInvalidError)
@OpenAPI({ description: 'refresh a access token' })
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth, @CookieParam("lfk_backend__refresh_token") refresh_token: string, @Res() response: any) {
if (refresh_token && refresh_token.length != 0 && refreshAuth.token == undefined) {
refreshAuth.token = refresh_token;
}
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth) {
let auth;
try {
auth = await refreshAuth.toAuth();
response.cookie('lfk_backend__refresh_token', auth.refresh_token, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
response.cookie('lfk_backend__refresh_token_expires_at', auth.refresh_token_expires_at, { expires: new Date(auth.refresh_token_expires_at * 1000), httpOnly: true });
} catch (error) {
throw error;
return error;
}
return response.send(auth)
return auth
}
}

View File

@ -1,102 +0,0 @@
import csv from 'csvtojson';
import { Authorized, Body, ContentType, Controller, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { RunnerGroupNeededError } from '../errors/RunnerErrors';
import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors';
import RawBodyMiddleware from '../middlewares/RawBody';
import { ImportRunner } from '../models/actions/ImportRunner';
import { ResponseRunner } from '../models/responses/ResponseRunner';
import { RunnerController } from './RunnerController';
@Controller()
@Authorized(["RUNNER:IMPORT", "TEAM:IMPORT"])
@OpenAPI({ security: [{ "AuthToken": [] }] })
export class ImportController {
private runnerController: RunnerController;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.runnerController = new RunnerController();
}
@Post('/runners/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them (or their teams) into the provided group" })
async postJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @QueryParam("group") groupID: number) {
if (!groupID) { throw new RunnerGroupNeededError(); }
let responseRunners: ResponseRunner[] = new Array<ResponseRunner>();
for await (let runner of importRunners) {
responseRunners.push(await this.runnerController.post(await runner.toCreateRunner(groupID)));
}
return responseRunners;
}
@Post('/organisations/:id/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them (or their teams) into the provided org" })
async postOrgsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
return await this.postJSON(importRunners, id)
}
@Post('/teams/:id/import')
@ContentType("application/json")
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from json and insert them into the provided team" })
async postTeamsJSON(@Body({ validate: true, type: ImportRunner }) importRunners: ImportRunner[], @Param('id') id: number) {
return await this.postJSON(importRunners, id)
}
@Post('/runners/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them (or their teams) into the provided group" })
async postCSV(@Req() request: any, @QueryParam("group") groupID: number) {
let csvParse = await csv({ delimiter: [",", ";"], trim: true }).fromString(request.rawBody.toString());
let importRunners: ImportRunner[] = new Array<ImportRunner>();
for await (let runner of csvParse) {
let newImportRunner = new ImportRunner();
newImportRunner.firstname = runner.firstname;
newImportRunner.middlename = runner.middlename;
newImportRunner.lastname = runner.lastname;
if (runner.class === undefined) { newImportRunner.team = runner.team; }
else { newImportRunner.class = runner.class; }
importRunners.push(newImportRunner);
}
return await this.postJSON(importRunners, groupID);
}
@Post('/organisations/:id/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them (or their teams) into the provided org" })
async postOrgsCSV(@Req() request: any, @Param("id") id: number) {
return await this.postCSV(request, id);
}
@Post('/teams/:id/import/csv')
@ContentType("application/json")
@UseBefore(RawBodyMiddleware)
@ResponseSchema(ResponseRunner, { isArray: true, statusCode: 200 })
@ResponseSchema(RunnerGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerGroupNeededError, { statusCode: 406 })
@OpenAPI({ description: "Create new runners from csv and insert them into the provided team" })
async postTeamsCSV(@Req() request: any, @Param("id") id: number) {
return await this.postCSV(request, id);
}
}

View File

@ -1,118 +0,0 @@
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 { PermissionIdsNotMatchingError, PermissionNeedsPrincipalError, PermissionNotFoundError } from '../errors/PermissionErrors';
import { PrincipalNotFoundError } from '../errors/PrincipalErrors';
import { CreatePermission } from '../models/actions/CreatePermission';
import { UpdatePermission } from '../models/actions/UpdatePermission';
import { Permission } from '../models/entities/Permission';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponsePermission } from '../models/responses/ResponsePermission';
import { ResponsePrincipal } from '../models/responses/ResponsePrincipal';
@JsonController('/permissions')
@OpenAPI({ security: [{ "AuthToken": [] }] })
export class PermissionController {
private permissionRepository: Repository<Permission>;
/**
* Gets the repository of this controller's model/entity.
*/
constructor() {
this.permissionRepository = getConnectionManager().get().getRepository(Permission);
}
@Get()
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission, { isArray: true })
@OpenAPI({ description: 'Lists all permissions.' })
async getAll() {
let responsePermissions: ResponsePermission[] = new Array<ResponsePermission>();
const permissions = await this.permissionRepository.find({ relations: ['principal'] });
permissions.forEach(permission => {
responsePermissions.push(new ResponsePermission(permission));
});
return responsePermissions;
}
@Get('/:id')
@Authorized("PERMISSION:GET")
@ResponseSchema(ResponsePermission)
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
@OnUndefined(PermissionNotFoundError)
@OpenAPI({ description: 'Returns a permissions of a specified id (if it exists)' })
async getOne(@Param('id') id: number) {
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!permission) { throw new PermissionNotFoundError(); }
return new ResponsePermission(permission);
}
@Post()
@Authorized("PERMISSION:CREATE")
@ResponseSchema(ResponsePermission)
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@OpenAPI({ description: 'Create a new runnerTeam object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createPermission: CreatePermission) {
let permission;
try {
permission = await createPermission.toPermission();
} catch (error) {
throw error;
}
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
if (existingPermission) { return new ResponsePermission(existingPermission); }
permission = await this.permissionRepository.save(permission);
permission = await this.permissionRepository.findOne(permission, { relations: ['principal'] });
return new ResponsePermission(permission);
}
@Put('/:id')
@Authorized("PERMISSION:UPDATE")
@ResponseSchema(ResponsePrincipal)
@ResponseSchema(PermissionNotFoundError, { statusCode: 404 })
@ResponseSchema(PrincipalNotFoundError, { statusCode: 404 })
@ResponseSchema(PermissionIdsNotMatchingError, { statusCode: 406 })
@ResponseSchema(PermissionNeedsPrincipalError, { statusCode: 406 })
@OpenAPI({ description: "Update a permission object (id can't be changed)." })
async put(@Param('id') id: number, @Body({ validate: true }) permission: UpdatePermission) {
let oldPermission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!oldPermission) {
throw new PermissionNotFoundError();
}
if (oldPermission.id != permission.id) {
throw new PermissionIdsNotMatchingError();
}
let existingPermission = await this.permissionRepository.findOne({ target: permission.target, action: permission.action, principal: permission.principal }, { relations: ['principal'] });
if (existingPermission) {
await this.remove(permission.id, true);
return new ResponsePermission(existingPermission);
}
await this.permissionRepository.save(await permission.updatePermission(oldPermission));
return new ResponsePermission(await this.permissionRepository.findOne({ id: permission.id }, { relations: ['principal'] }));
}
@Delete('/:id')
@Authorized("PERMISSION:DELETE")
@ResponseSchema(ResponsePermission)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a specified permission (if it exists).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
let permission = await this.permissionRepository.findOne({ id: id }, { relations: ['principal'] });
if (!permission) { return null; }
const responsePermission = new ResponsePermission(permission);
await this.permissionRepository.delete(permission);
return responsePermission;
}
}

View File

@ -1,4 +1,4 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { 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 { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors';
@ -10,7 +10,7 @@ import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunner } from '../models/responses/ResponseRunner';
@JsonController('/runners')
@OpenAPI({ security: [{ "AuthToken": [] }] })
//@Authorized('RUNNERS:read')
export class RunnerController {
private runnerRepository: Repository<Runner>;
@ -22,7 +22,6 @@ export class RunnerController {
}
@Get()
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner, { isArray: true })
@OpenAPI({ description: 'Lists all runners.' })
async getAll() {
@ -35,7 +34,6 @@ export class RunnerController {
}
@Get('/:id')
@Authorized("RUNNER:GET")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerNotFoundError)
@ -47,7 +45,6 @@ export class RunnerController {
}
@Post()
@Authorized("RUNNER:CREATE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerGroupNeededError)
@ResponseSchema(RunnerGroupNotFoundError)
@ -65,7 +62,6 @@ export class RunnerController {
}
@Put('/:id')
@Authorized("RUNNER:UPDATE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerIdsNotMatchingError, { statusCode: 406 })
@ -81,12 +77,11 @@ export class RunnerController {
throw new RunnerIdsNotMatchingError();
}
await this.runnerRepository.save(await runner.updateRunner(oldRunner));
await this.runnerRepository.update(oldRunner, await runner.toRunner());
return new ResponseRunner(await this.runnerRepository.findOne({ id: id }, { relations: ['scans', 'group'] }));
}
@Delete('/:id')
@Authorized("RUNNER:DELETE")
@ResponseSchema(ResponseRunner)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)

View File

@ -1,9 +1,9 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { 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 { EntityFromBody } from 'typeorm-routing-controllers-extensions';
import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors';
import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation';
import { UpdateRunnerOrganisation } from '../models/actions/UpdateRunnerOrganisation';
import { RunnerOrganisation } from '../models/entities/RunnerOrganisation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation';
@ -12,7 +12,7 @@ import { RunnerTeamController } from './RunnerTeamController';
@JsonController('/organisations')
@OpenAPI({ security: [{ "AuthToken": [] }] })
//@Authorized('RUNNERS:read')
export class RunnerOrganisationController {
private runnerOrganisationRepository: Repository<RunnerOrganisation>;
@ -24,7 +24,6 @@ export class RunnerOrganisationController {
}
@Get()
@Authorized("ORGANISATION:GET")
@ResponseSchema(ResponseRunnerOrganisation, { isArray: true })
@OpenAPI({ description: 'Lists all runnerOrganisations.' })
async getAll() {
@ -37,7 +36,6 @@ export class RunnerOrganisationController {
}
@Get('/:id')
@Authorized("ORGANISATION:GET")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerOrganisationNotFoundError)
@ -49,7 +47,6 @@ export class RunnerOrganisationController {
}
@Post()
@Authorized("ORGANISATION:CREATE")
@ResponseSchema(ResponseRunnerOrganisation)
@OpenAPI({ description: 'Create a new runnerOrganisation object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createRunnerOrganisation: CreateRunnerOrganisation) {
@ -66,29 +63,28 @@ export class RunnerOrganisationController {
}
@Put('/:id')
@Authorized("ORGANISATION:UPDATE")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(RunnerOrganisationNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerOrganisationIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a runnerOrganisation object (id can't be changed)." })
async put(@Param('id') id: number, @Body({ validate: true }) updateOrganisation: UpdateRunnerOrganisation) {
async put(@Param('id') id: number, @EntityFromBody() runnerOrganisation: RunnerOrganisation) {
let oldRunnerOrganisation = await this.runnerOrganisationRepository.findOne({ id: id });
if (!oldRunnerOrganisation) {
throw new RunnerOrganisationNotFoundError();
}
if (oldRunnerOrganisation.id != updateOrganisation.id) {
if (oldRunnerOrganisation.id != runnerOrganisation.id) {
throw new RunnerOrganisationIdsNotMatchingError();
}
await this.runnerOrganisationRepository.save(await updateOrganisation.updateRunnerOrganisation(oldRunnerOrganisation));
await this.runnerOrganisationRepository.update(oldRunnerOrganisation, runnerOrganisation);
return new ResponseRunnerOrganisation(await this.runnerOrganisationRepository.findOne(id, { relations: ['address', 'contact', 'teams'] }));
runnerOrganisation = await this.runnerOrganisationRepository.findOne(runnerOrganisation, { relations: ['address', 'contact', 'teams'] });
return new ResponseRunnerOrganisation(runnerOrganisation);
}
@Delete('/:id')
@Authorized("ORGANISATION:DELETE")
@ResponseSchema(ResponseRunnerOrganisation)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerOrganisationHasTeamsError, { statusCode: 406 })

View File

@ -1,4 +1,4 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { 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 { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors';
@ -11,7 +11,7 @@ import { RunnerController } from './RunnerController';
@JsonController('/teams')
@OpenAPI({ security: [{ "AuthToken": [] }] })
//@Authorized('RUNNERS:read')
export class RunnerTeamController {
private runnerTeamRepository: Repository<RunnerTeam>;
@ -23,7 +23,6 @@ export class RunnerTeamController {
}
@Get()
@Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam, { isArray: true })
@OpenAPI({ description: 'Lists all runnerTeams.' })
async getAll() {
@ -36,7 +35,6 @@ export class RunnerTeamController {
}
@Get('/:id')
@Authorized("TEAM:GET")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@OnUndefined(RunnerTeamNotFoundError)
@ -48,7 +46,6 @@ export class RunnerTeamController {
}
@Post()
@Authorized("TEAM:CREATE")
@ResponseSchema(ResponseRunnerTeam)
@OpenAPI({ description: 'Create a new runnerTeam object (id will be generated automagicly).' })
async post(@Body({ validate: true }) createRunnerTeam: CreateRunnerTeam) {
@ -66,7 +63,6 @@ export class RunnerTeamController {
}
@Put('/:id')
@Authorized("TEAM:UPDATE")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(RunnerTeamNotFoundError, { statusCode: 404 })
@ResponseSchema(RunnerTeamIdsNotMatchingError, { statusCode: 406 })
@ -82,13 +78,12 @@ export class RunnerTeamController {
throw new RunnerTeamIdsNotMatchingError();
}
await this.runnerTeamRepository.save(await runnerTeam.updateRunnerTeam(oldRunnerTeam));
await this.runnerTeamRepository.update(oldRunnerTeam, await runnerTeam.toRunnerTeam());
return new ResponseRunnerTeam(await this.runnerTeamRepository.findOne({ id: runnerTeam.id }, { relations: ['parentGroup', 'contact'] }));
}
@Delete('/:id')
@Authorized("TEAM:DELETE")
@ResponseSchema(ResponseRunnerTeam)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@ResponseSchema(RunnerTeamHasRunnersError, { statusCode: 406 })

View File

@ -1,22 +0,0 @@
import { Get, JsonController } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi';
import { getConnection } from 'typeorm';
@JsonController('/status')
export class StatusController {
@Get()
@OpenAPI({ description: "Lists all tracks." })
get() {
let connection;
try {
connection = getConnection();
} catch {
throw new Error("sth is wrong, i can feel it....");
}
return {
"controllers": "✔",
"database connection": "✔"
};
}
}

View File

@ -1,4 +1,4 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
@ -9,7 +9,7 @@ import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseTrack } from '../models/responses/ResponseTrack';
@JsonController('/tracks')
@OpenAPI({ security: [{ "AuthToken": [] }] })
//@Authorized("TRACKS:read")
export class TrackController {
private trackRepository: Repository<Track>;
@ -21,8 +21,8 @@ export class TrackController {
}
@Get()
@Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack, { isArray: true })
@OpenAPI({ description: "Lists all tracks." })
async getAll() {
let responseTracks: ResponseTrack[] = new Array<ResponseTrack>();
const tracks = await this.trackRepository.find();
@ -33,7 +33,6 @@ export class TrackController {
}
@Get('/:id')
@Authorized("TRACK:GET")
@ResponseSchema(ResponseTrack)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@OnUndefined(TrackNotFoundError)
@ -45,7 +44,6 @@ export class TrackController {
}
@Post()
@Authorized("TRACK:CREATE")
@ResponseSchema(ResponseTrack)
@OpenAPI({ description: "Create a new track object (id will be generated automagicly)." })
async post(
@ -56,7 +54,6 @@ export class TrackController {
}
@Put('/:id')
@Authorized("TRACK:UPDATE")
@ResponseSchema(ResponseTrack)
@ResponseSchema(TrackNotFoundError, { statusCode: 404 })
@ResponseSchema(TrackIdsNotMatchingError, { statusCode: 406 })
@ -72,12 +69,11 @@ export class TrackController {
throw new TrackIdsNotMatchingError();
}
await this.trackRepository.save(track);
await this.trackRepository.update(oldTrack, track);
return new ResponseTrack(track);
}
@Delete('/:id')
@Authorized("TRACK:DELETE")
@ResponseSchema(ResponseTrack)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)

View File

@ -1,18 +1,15 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors';
import { UserGroupNotFoundError } from '../errors/UserGroupErrors';
import { CreateUser } from '../models/actions/CreateUser';
import { UpdateUser } from '../models/actions/UpdateUser';
import { User } from '../models/entities/User';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUser } from '../models/responses/ResponseUser';
import { PermissionController } from './PermissionController';
@JsonController('/users')
@OpenAPI({ security: [{ "AuthToken": [] }] })
export class UserController {
private userRepository: Repository<User>;
@ -24,32 +21,22 @@ export class UserController {
}
@Get()
@Authorized("USER:GET")
@ResponseSchema(User, { isArray: true })
@OpenAPI({ description: 'Lists all users.' })
async getAll() {
let responseUsers: ResponseUser[] = new Array<ResponseUser>();
const users = await this.userRepository.find({ relations: ['permissions', 'groups'] });
users.forEach(user => {
responseUsers.push(new ResponseUser(user));
});
return responseUsers;
getAll() {
return this.userRepository.find();
}
@Get('/:id')
@Authorized("USER:GET")
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@OnUndefined(UserNotFoundError)
@OpenAPI({ description: 'Returns a user of a specified id (if it exists)' })
async getOne(@Param('id') id: number) {
let user = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] })
if (!user) { throw new UserNotFoundError(); }
return new ResponseUser(user);
getOne(@Param('id') id: number) {
return this.userRepository.findOne({ id: id });
}
@Post()
@Authorized("USER:CREATE")
@ResponseSchema(User)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new user object (id will be generated automagicly).' })
@ -61,48 +48,41 @@ export class UserController {
throw error;
}
user = await this.userRepository.save(user)
return new ResponseUser(await this.userRepository.findOne(user, { relations: ['permissions', 'groups'] }));
return this.userRepository.save(user);
}
@Put('/:id')
@Authorized("USER:UPDATE")
@ResponseSchema(User)
@ResponseSchema(UserNotFoundError, { statusCode: 404 })
@ResponseSchema(UserIdsNotMatchingError, { statusCode: 406 })
@OpenAPI({ description: "Update a user object (id can't be changed)." })
async put(@Param('id') id: number, @Body({ validate: true }) updateUser: UpdateUser) {
async put(@Param('id') id: number, @EntityFromBody() user: User) {
let oldUser = await this.userRepository.findOne({ id: id });
if (!oldUser) {
throw new UserNotFoundError();
}
if (oldUser.id != updateUser.id) {
if (oldUser.id != user.id) {
throw new UserIdsNotMatchingError();
}
await this.userRepository.save(await updateUser.updateUser(oldUser));
return new ResponseUser(await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] }));
await this.userRepository.update(oldUser, user);
return user;
}
@Delete('/:id')
@Authorized("USER:DELETE")
@ResponseSchema(User)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a user runner (if it exists).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
@OpenAPI({ description: 'Delete a specified runner (if it exists).' })
async remove(@Param("id") id: number) {
let user = await this.userRepository.findOne({ id: id });
if (!user) { return null; }
const responseUser = await this.userRepository.findOne({ id: id }, { relations: ['permissions', 'groups'] });;
const permissionControler = new PermissionController();
for (let permission of responseUser.permissions) {
await permissionControler.remove(permission.id, true);
if (!user) {
return null;
}
await this.userRepository.delete(user);
return new ResponseUser(responseUser);
return user;
}
}

View File

@ -1,4 +1,4 @@
import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { EntityFromBody } from 'typeorm-routing-controllers-extensions';
@ -6,12 +6,9 @@ import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/
import { CreateUserGroup } from '../models/actions/CreateUserGroup';
import { UserGroup } from '../models/entities/UserGroup';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseUserGroup } from '../models/responses/ResponseUserGroup';
import { PermissionController } from './PermissionController';
@JsonController('/usergroups')
@OpenAPI({ security: [{ "AuthToken": [] }] })
export class UserGroupController {
private userGroupsRepository: Repository<UserGroup>;
@ -23,7 +20,6 @@ export class UserGroupController {
}
@Get()
@Authorized("USERGROUP:GET")
@ResponseSchema(UserGroup, { isArray: true })
@OpenAPI({ description: 'Lists all usergroups.' })
getAll() {
@ -31,7 +27,6 @@ export class UserGroupController {
}
@Get('/:id')
@Authorized("USERGROUP:GET")
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@OnUndefined(UserGroupNotFoundError)
@ -41,7 +36,6 @@ export class UserGroupController {
}
@Post()
@Authorized("USERGROUP:CREATE")
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError)
@OpenAPI({ description: 'Create a new usergroup object (id will be generated automagicly).' })
@ -57,7 +51,6 @@ export class UserGroupController {
}
@Put('/:id')
@Authorized("USERGROUP:UPDATE")
@ResponseSchema(UserGroup)
@ResponseSchema(UserGroupNotFoundError, { statusCode: 404 })
@ResponseSchema(UserGroupIdsNotMatchingError, { statusCode: 406 })
@ -73,27 +66,22 @@ export class UserGroupController {
throw new UserGroupIdsNotMatchingError();
}
await this.userGroupsRepository.save(userGroup);
await this.userGroupsRepository.update(oldUserGroup, userGroup);
return userGroup;
}
@Delete('/:id')
@Authorized("USERGROUP:DELETE")
@ResponseSchema(ResponseUserGroup)
@ResponseSchema(UserGroup)
@ResponseSchema(ResponseEmpty, { statusCode: 204 })
@OnUndefined(204)
@OpenAPI({ description: 'Delete a specified usergroup (if it exists).' })
async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
async remove(@Param("id") id: number) {
let group = await this.userGroupsRepository.findOne({ id: id });
if (!group) { return null; }
const responseGroup = await this.userGroupsRepository.findOne({ id: id }, { relations: ['permissions'] });;
const permissionControler = new PermissionController();
for (let permission of responseGroup.permissions) {
await permissionControler.remove(permission.id, true);
if (!group) {
return null;
}
await this.userGroupsRepository.delete(group);
return new ResponseUserGroup(responseGroup);
return group;
}
}

View File

@ -1,57 +1,63 @@
import { IsString } from 'class-validator';
import { ForbiddenError, NotAcceptableError, NotFoundError, UnauthorizedError } from 'routing-controllers';
/**
* Error to throw when a jwt is expired.
*/
export class ExpiredJWTError extends UnauthorizedError {
@IsString()
name = "ExpiredJWTError"
@IsString()
message = "your provided jwt is expired"
}
/**
* Error to throw when a jwt could not be parsed.
* For example: Wrong signature or expired.
*/
export class IllegalJWTError extends UnauthorizedError {
@IsString()
name = "IllegalJWTError"
@IsString()
message = "Your provided jwt could not be parsed."
message = "your provided jwt could not be parsed"
}
/**
* Error to throw when user is nonexistant or refreshtoken is invalid.
* This can happen if someone provides a JWT with a invalid user id or the refreshTokenCount of the user is higher that the provided jwt's is.
*/
export class UserNonexistantOrRefreshtokenInvalidError extends UnauthorizedError {
@IsString()
name = "UserNonexistantOrRefreshtokenInvalidError"
@IsString()
message = "User is nonexistant or refreshtoken is invalid."
message = "user is nonexistant or refreshtoken is invalid"
}
/**
* Error to throw when provided credentials are invalid.
* We don't have seperate errors for username/mail and passwords to protect against guessing attacks.
*/
export class InvalidCredentialsError extends UnauthorizedError {
@IsString()
name = "InvalidCredentialsError"
@IsString()
message = "Your provided credentials are invalid."
message = "your provided credentials are invalid"
}
/**
* Error to throw when a jwt does not have permission for this route/action.
* Mainly used be the @Authorized decorator (via the authchecker).
*/
export class NoPermissionError extends ForbiddenError {
@IsString()
name = "NoPermissionError"
@IsString()
message = "Your provided jwt does not have permission for this route/ action."
message = "your provided jwt does not have permission for this route/ action"
}
/**
* Error to throw when no username and no email is set.
* Because we have to identify users somehow.
*/
export class UsernameOrEmailNeededError extends NotAcceptableError {
@IsString()
@ -62,48 +68,47 @@ export class UsernameOrEmailNeededError extends NotAcceptableError {
}
/**
* Error to throw when no password is provided for a new user.
* Passwords are the minimum we need for user security.
* Error to throw when no password is provided.
*/
export class PasswordNeededError extends NotAcceptableError {
@IsString()
name = "PasswordNeededError"
@IsString()
message = "No password is provided - you need to provide it."
message = "no password is provided - you need to provide it"
}
/**
* Error to throw when no user could be found for a certain query.
* Error to throw when no user could be found mating the provided credential.
*/
export class UserNotFoundError extends NotFoundError {
@IsString()
name = "UserNotFoundError"
@IsString()
message = "The user you provided couldn't be located in the system. \n Please check your request."
message = "no user could be found for provided credential"
}
/**
* Error to throw when no jwt was provided (but one had to be).
* Error to throw when no jwt token was provided (but one had to be).
*/
export class JwtNotProvidedError extends NotAcceptableError {
@IsString()
name = "JwtNotProvidedError"
@IsString()
message = "No jwt was provided."
message = "no jwt token was provided"
}
/**
* Error to throw when user was not found or the jwt's refresh token count was invalid.
* Error to throw when user was not found or refresh token count was invalid.
*/
export class UserNotFoundOrRefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
name = "UserNotFoundOrRefreshTokenCountInvalidError"
@IsString()
message = "User was not found or the refresh token count is invalid."
message = "user was not found or refresh token count was invalid"
}
/**
@ -114,5 +119,5 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError {
name = "RefreshTokenCountInvalidError"
@IsString()
message = "Refresh token count is invalid."
message = "refresh token count was invalid"
}

View File

@ -1,36 +0,0 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a permission couldn't be found.
*/
export class PermissionNotFoundError extends NotFoundError {
@IsString()
name = "PermissionNotFoundError"
@IsString()
message = "Permission not found!"
}
/**
* Error to throw when two permissions' ids don't match.
* Usually occurs when a user tries to change a permission's id.
*/
export class PermissionIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "PermissionIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a permission's id: This isn't allowed!"
}
/**
* Error to throw when a permission gets provided without a principal.
*/
export class PermissionNeedsPrincipalError extends NotAcceptableError {
@IsString()
name = "PermissionNeedsPrincipalError"
@IsString()
message = "You provided no principal for this permission."
}

View File

@ -1,24 +0,0 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a user couldn't be found.
*/
export class PrincipalNotFoundError extends NotFoundError {
@IsString()
name = "PrincipalNotFoundError"
@IsString()
message = "Principal not found!"
}
/**
* Error to throw, when a provided runnerOrganisation 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."
}

View File

@ -3,6 +3,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerNotFoundError extends NotFoundError {
@IsString()
@ -15,13 +16,14 @@ export class RunnerNotFoundError extends NotFoundError {
/**
* Error to throw when two runners' ids don't match.
* Usually occurs when a user tries to change a runner's id.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
}
/**

View File

@ -3,6 +3,7 @@ import { NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner group couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerGroupNotFoundError extends NotFoundError {
@IsString()

View File

@ -3,6 +3,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner organisation couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationNotFoundError extends NotFoundError {
@IsString()
@ -14,36 +15,39 @@ export class RunnerOrganisationNotFoundError extends NotFoundError {
/**
* Error to throw when two runner organisations' ids don't match.
* Usually occurs when a user tries to change a runner organisation's id.
* Usually occurs when a user tries to change a runner's id.
* Implemented this way to work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner organisation's id: This isn't allowed!"
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
}
/**
* Error to throw when a organisation still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasRunnersError"
@IsString()
message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
message = "This organisation still has runners associated with it. \n If you want to delete this organisation with all it's runners and teams ass `?force` to your query."
}
/**
* Error to throw when a organisation still has teams associated.
* Error to throw when a organisation still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerOrganisationHasTeamsError extends NotAcceptableError {
@IsString()
name = "RunnerOrganisationHasTeamsError"
@IsString()
message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams add `?force` to your query."
message = "This organisation still has teams associated with it. \n If you want to delete this organisation with all it's runners and teams ass `?force` to your query."
}
/**

View File

@ -3,6 +3,7 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when a runner team couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class RunnerTeamNotFoundError extends NotFoundError {
@IsString()
@ -14,29 +15,32 @@ export class RunnerTeamNotFoundError extends NotFoundError {
/**
* Error to throw when two runner teams' ids don't match.
* Usually occurs when a user tries to change a runner team's id.
* Usually occurs when a user tries to change a runner's id.
* Implemented this way to work with the json-schema conversion for openapi.
*/
export class RunnerTeamIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "RunnerTeamIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a runner's id: This isn't allowed!"
message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed"
}
/**
* Error to throw when a team still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerTeamHasRunnersError extends NotAcceptableError {
@IsString()
name = "RunnerTeamHasRunnersError"
@IsString()
message = "This team still has runners associated with it. \n If you want to delete this team with all it's runners and teams add `?force` to your query."
message = "This team still has runners associated with it. \n If you want to delete this team with all it's runners and teams ass `?force` to your query."
}
/**
* Error to throw when a team still has runners associated.
* Implemented this waysto work with the json-schema conversion for openapi.
*/
export class RunnerTeamNeedsParentError extends NotAcceptableError {
@IsString()

View File

@ -1,8 +1,9 @@
import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
import { JsonController, Param, Body, Get, Post, Put, Delete, NotFoundError, OnUndefined, NotAcceptableError } from 'routing-controllers';
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
/**
* Error to throw when a track couldn't be found.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class TrackNotFoundError extends NotFoundError {
@IsString()
@ -15,11 +16,12 @@ export class TrackNotFoundError extends NotFoundError {
/**
* Error to throw when two tracks' ids don't match.
* Usually occurs when a user tries to change a track's id.
* Implemented this ways to work with the json-schema conversion for openapi.
*/
export class TrackIdsNotMatchingError extends NotAcceptableError {
@IsString()
name = "TrackIdsNotMatchingError"
@IsString()
message = "The ids don't match! \n And if you wanted to change a track's id: This isn't allowed"
message = "The id's don't match!! \n And if you wanted to change a track's id: This isn't allowed"
}

View File

@ -3,15 +3,14 @@ import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no username or email is set.
* We somehow need to identify you :)
* Error to throw when no username or email is set
*/
export class UsernameOrEmailNeededError extends NotFoundError {
@IsString()
name = "UsernameOrEmailNeededError"
@IsString()
message = "No username or email is set!"
message = "no username or email is set!"
}
/**
@ -34,5 +33,5 @@ export class UserIdsNotMatchingError extends NotAcceptableError {
name = "UserIdsNotMatchingError"
@IsString()
message = "The ids don't match!! \n And if you wanted to change a user's id: This isn't allowed!"
message = "The id's don't match!! \n And if you wanted to change a user's id: This isn't allowed"
}

View File

@ -2,14 +2,14 @@ import { IsString } from 'class-validator';
import { NotAcceptableError, NotFoundError } from 'routing-controllers';
/**
* Error to throw when no groupname is set.
* Error to throw when no groupname is set
*/
export class GroupNameNeededError extends NotFoundError {
@IsString()
name = "GroupNameNeededError"
@IsString()
message = "No name is set for this group!"
message = "no groupname is set!"
}
/**
@ -32,5 +32,5 @@ export class UserGroupIdsNotMatchingError extends NotAcceptableError {
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 id's don't match!! \n If you wanted to change a usergroup's id: This isn't allowed"
}

View File

@ -1,114 +0,0 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import * as jsonwebtoken from "jsonwebtoken";
import { config } from './config';
import { User } from './models/entities/User';
/**
* This class is responsible for all things JWT creation.
*/
export class JwtCreator {
/**
* Creates a new refresh token for a given user
* @param user User entity that the refresh token shall be created for
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
*/
public static createRefresh(user: User, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
return jsonwebtoken.sign({
refreshTokenCount: user.refreshTokenCount,
id: user.id,
exp: expiry_timestamp
}, config.jwt_secret)
}
/**
* Creates a new access token for a given user
* @param user User entity that the access token shall be created for
* @param expiry_timestamp Timestamp for the token expiry. Will be generated if not provided.
*/
public static createAccess(user: User, expiry_timestamp?: number) {
if (!expiry_timestamp) { expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; }
return jsonwebtoken.sign({
userdetails: new JwtUser(user),
exp: expiry_timestamp
}, config.jwt_secret)
}
}
/**
* Special variant of the user class that
*/
export class JwtUser {
@IsInt()
id: number;
@IsUUID(4)
uuid: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
username?: string;
@IsString()
@IsNotEmpty()
firstname: string;
@IsString()
@IsOptional()
middlename?: string;
@IsString()
@IsNotEmpty()
lastname: string;
permissions: string[];
@IsBoolean()
enabled: boolean;
@IsInt()
@IsNotEmpty()
refreshTokenCount?: number;
@IsString()
@IsOptional()
profilePic?: string;
/**
* Creates a new instance of this class based on a provided user entity.
* @param user User entity that shall be encapsulated in a jwt.
*/
public constructor(user: User) {
this.id = user.id;
this.firstname = user.firstname;
this.middlename = user.middlename;
this.lastname = user.lastname;
this.username = user.username;
this.email = user.email;
this.refreshTokenCount = user.refreshTokenCount;
this.uuid = user.uuid;
this.profilePic = user.profilePic;
this.permissions = this.getPermissions(user);
}
/**
* Handels getting the permissions granted to this user (direct or indirect).
* @param user User which's permissions shall be gotten.
*/
public getPermissions(user: User): string[] {
let returnPermissions: string[] = new Array<string>();
for (let permission of user.permissions) {
returnPermissions.push(permission.toString());
}
for (let group of user.groups) {
for (let permission of group.permissions) {
returnPermissions.push(permission.toString());
}
}
return Array.from(new Set(returnPermissions));
}
}

View File

@ -1,16 +1,10 @@
import { createConnection } from "typeorm";
import { runSeeder } from 'typeorm-seeding';
import { User } from '../models/entities/User';
import SeedUsers from '../seeds/SeedUsers';
/**
* Loader for the database that creates the database connection and initializes the database tabels.
* It also triggers the seeding process if no users got detected in the database.
*/
export default async () => {
const connection = await createConnection();
await connection.synchronize();
if (await connection.getRepository(User).count() === 0) {
await runSeeder(SeedUsers);
}
connection.synchronize();
return connection;
};

View File

@ -1,11 +1,10 @@
import cookieParser from "cookie-parser";
import { Application } from "express";
/**
* Loader for express related configurations.
* Configures proxy trusts, globally used middlewares and other express features.
* Currently only enables the proxy trust.
*/
export default async (app: Application) => {
app.enable('trust proxy');
app.use(cookieParser());
return app;
};

View File

@ -5,7 +5,6 @@ import openapiLoader from "./openapi";
/**
* Index Loader that executes the other loaders in the right order.
* This basicly exists for abstraction and a overall better dev experience.
*/
export default async (app: Application) => {
await databaseLoader();

View File

@ -5,8 +5,7 @@ import { routingControllersToSpec } from "routing-controllers-openapi";
import * as swaggerUiExpress from "swagger-ui-express";
/**
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
* All auth schema related stuff also has to be configured here
* Loader for everything openapi related - from creating the schema to serving it via a static route.
*/
export default async (app: Application) => {
const storage = getMetadataArgsStorage();
@ -27,8 +26,7 @@ export default async (app: Application) => {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
description: "A JWT based access token. Use /api/auth/login or /api/auth/refresh to get one."
"bearerFormat": "JWT"
}
}
},

View File

@ -1,7 +1,7 @@
import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers";
/**
* Our Error handling middlware that returns our custom httperrors to the user.
* Our Error handling middlware that returns our custom httperrors to the user
*/
@Middleware({ type: "after" })
export class ErrorHandler implements ExpressErrorMiddlewareInterface {

View File

@ -1,23 +0,0 @@
import { Request, Response } from 'express';
/**
* Custom express middleware that appends the raw body to the request obeject.
* Mainly used for parsing csvs from boddies.
*/
const RawBodyMiddleware = (req: Request, res: Response, next: () => void) => {
const body = []
req.on('data', chunk => {
body.push(chunk)
})
req.on('end', () => {
const rawBody = Buffer.concat(body)
req['rawBody'] = rawBody
next()
})
req.on('error', () => {
res.sendStatus(400)
})
}
export default RawBodyMiddleware

View File

@ -1,19 +1,16 @@
import { IsNotEmpty, IsOptional, IsPostalCode, IsString } from 'class-validator';
import { Address } from '../entities/Address';
/**
* This classed is used to create a new Address entity from a json body (post request).
*/
export class CreateAddress {
/**
* The newaddress's description.
* The address's description.
*/
@IsString()
@IsOptional()
description?: string;
/**
* The new address's first line.
* The address's first line.
* Containing the street and house number.
*/
@IsString()
@ -21,7 +18,7 @@ export class CreateAddress {
address1: string;
/**
* The new address's second line.
* The address's second line.
* Containing optional information.
*/
@IsString()
@ -29,9 +26,7 @@ export class CreateAddress {
address2?: string;
/**
* The new address's postal code.
* This will get checked against the postal code syntax for the configured country.
* TODO: Implement the config option.
* The address's postal code.
*/
@IsString()
@IsNotEmpty()
@ -39,21 +34,21 @@ export class CreateAddress {
postalcode: string;
/**
* The new address's city.
* The address's city.
*/
@IsString()
@IsNotEmpty()
city: string;
/**
* The new address's country.
* The address's country.
*/
@IsString()
@IsNotEmpty()
country: string;
/**
* Creates a new Address entity from this.
* Creates a Address object based on this.
*/
public toAddress(): Address {
let newAddress: Address = new Address();

View File

@ -1,47 +1,24 @@
import * as argon2 from "argon2";
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsEmail, IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { JwtCreator } from '../../jwtcreator';
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
/**
* This class is used to create auth credentials based on user credentials provided in a json body (post request).
* To be a little bit more exact: Is takes in a username/email + password and creates a new access and refresh token for the user.
* It of course checks for user existance, password validity and so on.
*/
export class CreateAuth {
/**
* The username of the user that want's to login.
* Either username or email have to be provided.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The email address of the user that want's to login.
* Either username or email have to be provided.
*/
@IsString()
password: string;
@IsOptional()
@IsEmail()
@IsString()
email?: string;
/**
* The user's password.
* Will be checked against an argon2 hash.
*/
@IsNotEmpty()
@IsString()
password: string;
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
@ -49,24 +26,34 @@ export class CreateAuth {
throw new UsernameOrEmailNeededError();
}
if (!this.password) {
throw new PasswordNeededError();
throw new PasswordNeededError()
}
const found_user = await getConnectionManager().get().getRepository(User).findOne({ relations: ['groups', 'permissions', 'groups.permissions'], where: [{ username: this.username }, { email: this.email }] });
if (!found_user) {
throw new UserNotFoundError();
}
if (!(await argon2.verify(found_user.password, this.password + found_user.uuid))) {
throw new InvalidCredentialsError();
}
//Create the access token
const found_users = await getConnectionManager().get().getRepository(User).find({ relations: ['groups', 'permissions'], where: [{ username: this.username }, { email: this.email }] });
if (found_users.length === 0) {
throw new UserNotFoundError()
} else {
const found_user = found_users[0]
if (await argon2.verify(found_user.password, this.password + found_user.uuid)) {
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
found_user.permissions = found_user.permissions || []
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, config.jwt_secret)
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//Create the refresh token
//
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, config.jwt_secret)
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
} else {
throw new InvalidCredentialsError()
}
}
return newAuth;
}
}

View File

@ -5,34 +5,32 @@ import { AddressNotFoundError, AddressWrongTypeError } from '../../errors/Addres
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.
* The contact's first name.
*/
@IsNotEmpty()
@IsString()
firstname: string;
/**
* The new contact's middle name.
* The contact's middle name.
* Optional
*/
@IsOptional()
@IsString()
middlename?: string;
/**
* The new contact's last name.
* The contact's last name.
*/
@IsNotEmpty()
@IsString()
lastname: string;
/**
* The new contact's address.
* Must be the address's id.
* The contact's address.
* Optional
*/
@IsInt()
@IsOptional()
@ -40,7 +38,7 @@ export class CreateGroupContact {
/**
* The contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
* Optional
*/
@IsOptional()
@IsPhoneNumber(config.phone_validation_countrycode)
@ -48,16 +46,17 @@ export class CreateGroupContact {
/**
* The contact's email address.
* Optional
*/
@IsOptional()
@IsEmail()
email?: string;
/**
* Gets the new contact's address by it's id.
* Get's this participant's address from this.address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {
@ -70,7 +69,7 @@ export class CreateGroupContact {
}
/**
* Creates a new Address entity from this.
* Creates a Address object based on this.
*/
public async toGroupContact(): Promise<GroupContact> {
let contact: GroupContact = new GroupContact();

View File

@ -4,9 +4,6 @@ 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.
@ -17,6 +14,7 @@ export abstract class CreateParticipant {
/**
* The new participant's middle name.
* Optional.
*/
@IsString()
@IsOptional()
@ -31,7 +29,7 @@ export abstract class CreateParticipant {
/**
* The new participant's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
* Optional.
*/
@IsString()
@IsOptional()
@ -40,6 +38,7 @@ export abstract class CreateParticipant {
/**
* The new participant's e-mail address.
* Optional.
*/
@IsString()
@IsOptional()
@ -49,16 +48,17 @@ export abstract class CreateParticipant {
/**
* The new participant's address.
* Must be of type number (address id).
* Optional.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Gets the new participant's address by it's address.
* Get's this participant's address from this.address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {

View File

@ -1,60 +0,0 @@
import {
IsEnum,
IsInt,
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';
/**
* This classed is used to create a new Permission entity from a json body (post request).
*/
export class CreatePermission {
/**
* The new permissions's principal's id.
*/
@IsInt()
@IsNotEmpty()
principal: number;
/**
* The new permissions's target.
*/
@IsNotEmpty()
@IsEnum(PermissionTarget)
target: PermissionTarget;
/**
* The new permissions's action.
*/
@IsNotEmpty()
@IsEnum(PermissionAction)
action: PermissionAction;
/**
* Creates a new Permission entity from this.
*/
public async toPermission(): Promise<Permission> {
let newPermission: Permission = new Permission();
newPermission.principal = await this.getPrincipal();
newPermission.target = this.target;
newPermission.action = this.action;
return newPermission;
}
/**
* Gets the new permission's principal by it's id.
*/
public async getPrincipal(): Promise<Principal> {
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal })
if (!principal) { throw new PrincipalNotFoundError(); }
return principal;
}
}

View File

@ -7,9 +7,6 @@ import { Runner } from '../entities/Runner';
import { RunnerGroup } from '../entities/RunnerGroup';
import { CreateParticipant } from './CreateParticipant';
/**
* This classed is used to create a new Runner entity from a json body (post request).
*/
export class CreateRunner extends CreateParticipant {
/**
@ -19,7 +16,7 @@ export class CreateRunner extends CreateParticipant {
group: number;
/**
* Creates a new Runner entity from this.
* Creates a Runner entity from this.
*/
public async toRunner(): Promise<Runner> {
let newRunner: Runner = new Runner();
@ -36,10 +33,10 @@ export class CreateRunner extends CreateParticipant {
}
/**
* Gets the new runner's group by it's id.
* Manages all the different ways a group can be provided.
*/
public async getGroup(): Promise<RunnerGroup> {
if (this.group === undefined || this.group === null) {
if (this.group === undefined) {
throw new RunnerTeamNeedsParentError();
}
if (!isNaN(this.group)) {

View File

@ -3,19 +3,16 @@ import { getConnectionManager } from 'typeorm';
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).
*/
export abstract class CreateRunnerGroup {
/**
* The new group's name.
* The group's name.
*/
@IsNotEmpty()
@IsString()
name: string;
/**
* The new group's contact.
* The group's contact.
* Optional
*/
@IsInt()
@ -23,7 +20,7 @@ export abstract class CreateRunnerGroup {
contact?: number;
/**
* Gets the new group's contact by it's id.
* Get's this group's contact from this.address.
*/
public async getContact(): Promise<GroupContact> {
if (this.contact === undefined || this.contact === null) {

View File

@ -5,23 +5,21 @@ import { Address } from '../entities/Address';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
* This classed is used to create a new RunnerOrganisation entity from a json body (post request).
*/
export class CreateRunnerOrganisation extends CreateRunnerGroup {
/**
* The new organisation's address.
* Must be of type number (address id).
* Optional.
*/
@IsInt()
@IsOptional()
address?: number;
/**
* Gets the org's address by it's id.
* Get's this org's address from this.address.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
if (this.address === undefined) {
return null;
}
if (!isNaN(this.address)) {
@ -34,7 +32,7 @@ export class CreateRunnerOrganisation extends CreateRunnerGroup {
}
/**
* Creates a new RunnerOrganisation entity from this.
* Creates a RunnerOrganisation entity from this.
*/
public async toRunnerOrganisation(): Promise<RunnerOrganisation> {
let newRunnerOrganisation: RunnerOrganisation = new RunnerOrganisation();

View File

@ -6,23 +6,17 @@ import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
* This classed is used to create a new RunnerTeam entity from a json body (post request).
*/
export class CreateRunnerTeam extends CreateRunnerGroup {
/**
* The new team's parent group (organisation).
* The team's parent group (organisation).
*/
@IsInt()
@IsNotEmpty()
parentGroup: number;
/**
* Gets the new team's parent org based on it's id.
*/
public async getParent(): Promise<RunnerOrganisation> {
if (this.parentGroup === undefined || this.parentGroup === null) {
if (this.parentGroup === undefined) {
throw new RunnerTeamNeedsParentError();
}
if (!isNaN(this.parentGroup)) {
@ -35,7 +29,7 @@ export class CreateRunnerTeam extends CreateRunnerGroup {
}
/**
* Creates a new RunnerTeam entity from this.
* Creates a RunnerTeam entity from this.
*/
public async toRunnerTeam(): Promise<RunnerTeam> {
let newRunnerTeam: RunnerTeam = new RunnerTeam();

View File

@ -1,26 +1,23 @@
import { IsInt, IsNotEmpty, IsPositive, IsString } from 'class-validator';
import { Track } from '../entities/Track';
/**
* This classed is used to create a new Track entity from a json body (post request).
*/
export class CreateTrack {
/**
* The new track's name.
* The track's name.
*/
@IsString()
@IsNotEmpty()
name: string;
/**
* The new track's distance in meters (must be greater than 0).
* The track's distance in meters (must be greater than 0).
*/
@IsInt()
@IsPositive()
distance: number;
/**
* Creates a new Track entity from this.
* Converts a Track object based on this.
*/
public toTrack(): Track {
let newTrack: Track = new Track();

View File

@ -8,9 +8,6 @@ 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.
@ -20,6 +17,7 @@ export class CreateUser {
/**
* The new user's middle name.
* Optinal.
*/
@IsString()
@IsOptional()
@ -50,7 +48,7 @@ export class CreateUser {
/**
* The new user's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
* Optional
*/
@IsPhoneNumber(config.phone_validation_countrycode)
@IsOptional()
@ -66,14 +64,15 @@ export class CreateUser {
/**
* The new user's groups' id(s).
* You can provide either one groupId or an array of groupIDs.
* Optional.
*/
@IsOptional()
groups?: number[] | number
groupId?: number[] | number
//TODO: ProfilePics
/**
* Converts this to a User entity.
* Converts this to a User Entity.
*/
public async toUser(): Promise<User> {
let newUser: User = new User();
@ -82,6 +81,30 @@ export class CreateUser {
throw new UsernameOrEmailNeededError();
}
if (this.groupId) {
if (!Array.isArray(this.groupId)) {
this.groupId = [this.groupId]
}
const groupIDs: number[] = this.groupId
let errors = 0
const validateusergroups = async () => {
let foundgroups = []
for (const g of groupIDs) {
const found = await getConnectionManager().get().getRepository(UserGroup).find({ id: g });
if (found.length === 0) {
errors++
} else {
foundgroups.push(found[0])
}
}
newUser.groups = foundgroups
}
await validateusergroups()
if (errors !== 0) {
throw new UserGroupNotFoundError();
}
}
newUser.email = this.email
newUser.username = this.username
newUser.firstname = this.firstname
@ -90,26 +113,8 @@ export class CreateUser {
newUser.uuid = uuid.v4()
newUser.phone = this.phone
newUser.password = await argon2.hash(this.password + newUser.uuid);
newUser.groups = await this.getGroups();
//TODO: ProfilePics
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,9 +1,6 @@
import { IsOptional, IsString } from 'class-validator';
import { UserGroup } from '../entities/UserGroup';
/**
* This classed is used to create a new UserGroup entity from a json body (post request).
*/
export class CreateUserGroup {
/**
* The new group's name.
@ -20,7 +17,7 @@ export class CreateUserGroup {
description?: string;
/**
* Creates a new UserGroup entity from this.
* Converts this to a UserGroup entity.
*/
public async toUserGroup(): Promise<UserGroup> {
let newUserGroup: UserGroup = new UserGroup();

View File

@ -1,4 +1,4 @@
import { IsOptional, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
@ -6,23 +6,10 @@ import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, Us
import { User } from '../entities/User';
import { Logout } from '../responses/ResponseLogout';
/**
* This class handels a user logging out of the system.
* Of course it check's the user's provided credential (token) before logging him out.
*/
export class HandleLogout {
/**
* A stringyfied jwt access token.
* Will get checked for validity.
*/
@IsString()
@IsOptional()
token?: string;
token: string;
/**
* Logs the user out.
* This gets achived by increasing the user's refresh token count, thereby invalidateing all currently existing jwts for that user.
*/
public async logout(): Promise<Logout> {
let logout: Logout = new Logout();
if (!this.token || this.token === undefined) {
@ -35,11 +22,11 @@ export class HandleLogout {
throw new IllegalJWTError()
}
logout.timestamp = Math.floor(Date.now() / 1000)
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] });
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
throw new RefreshTokenCountInvalidError()
}
found_user.refreshTokenCount++;

View File

@ -1,97 +0,0 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { getConnectionManager } from 'typeorm';
import { RunnerGroupNeededError } from '../../errors/RunnerErrors';
import { RunnerOrganisationNotFoundError } from '../../errors/RunnerOrganisationErrors';
import { RunnerGroup } from '../entities/RunnerGroup';
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunner } from './CreateRunner';
/**
* Special class used to import runners from csv files - or json arrays created from csv to be exact.
* Why you ask? Because the past has shown us that a non excel/csv based workflow is too much for most schools.
*/
export class ImportRunner {
/**
* The new runner's first name.
*/
@IsString()
@IsNotEmpty()
firstname: string;
/**
* The new runner's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The new runner's last name.
*/
@IsString()
@IsNotEmpty()
lastname: string;
/**
* The new runner's team's name (if not provided otherwise).
* The team will automaticly get generated if it doesn't exist in this org yet.
*/
@IsString()
@IsOptional()
team?: string;
/**
* Just an alias for team, because this is usually only used for importing data from schools.
*/
@IsOptional()
@IsString()
public set class(value: string) {
this.team = value;
}
/**
* Creates a CreateRunner object based on this.
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
*/
public async toCreateRunner(groupID: number): Promise<CreateRunner> {
let newRunner: CreateRunner = new CreateRunner();
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;
newRunner.group = (await this.getGroup(groupID)).id;
return newRunner;
}
/**
* Get's the new runners group.
* @param groupID Either the id of the new runner's group or the id of the org that the new runner's team is a part of.
*/
public async getGroup(groupID: number): Promise<RunnerGroup> {
if (this.team === undefined && groupID === undefined) {
throw new RunnerGroupNeededError();
}
let team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ id: groupID });
if (team) { return team; }
let org = await getConnectionManager().get().getRepository(RunnerOrganisation).findOne({ id: groupID });
if (!org) {
throw new RunnerOrganisationNotFoundError();
}
if (this.team === undefined) { return org; }
team = await getConnectionManager().get().getRepository(RunnerTeam).findOne({ name: this.team, parentGroup: org });
if (!team) {
let newRunnerTeam: RunnerTeam = new RunnerTeam();
newRunnerTeam.name = this.team;
newRunnerTeam.parentGroup = org;
team = await getConnectionManager().get().getRepository(RunnerTeam).save(newRunnerTeam);
}
return team;
}
}

View File

@ -1,29 +1,15 @@
import { IsOptional, IsString } from 'class-validator';
import { IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { config } from '../../config';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
import { JwtCreator } from "../../jwtcreator";
import { User } from '../entities/User';
import { Auth } from '../responses/ResponseAuth';
/**
* This class is used to create refreshed auth credentials.
* To be a little bit more exact: Is takes in a refresh token and creates a new access and refresh token for it's user.
* It of course checks for user existance, jwt validity and so on.
*/
export class RefreshAuth {
/**
* A stringyfied jwt refresh token.
* Will get checked for validity.
*/
@IsString()
@IsOptional()
token?: string;
token: string;
/**
* Creates a new auth object based on this.
*/
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
if (!this.token || this.token === undefined) {
@ -35,21 +21,31 @@ export class RefreshAuth {
} catch (error) {
throw new IllegalJWTError()
}
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }, { relations: ['groups', 'permissions', 'groups.permissions'] });
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] }, { relations: ['groups', 'permissions'] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) {
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
throw new RefreshTokenCountInvalidError()
}
//Create the auth token
found_user.permissions = found_user.permissions || []
delete found_user.password;
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
newAuth.access_token = JwtCreator.createAccess(found_user, timestamp_accesstoken_expiry);
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, config.jwt_secret)
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//Create the refresh token
//
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = JwtCreator.createRefresh(found_user, timestamp_refresh_expiry);
newAuth.refresh_token_expires_at = timestamp_refresh_expiry;
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, config.jwt_secret)
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
return newAuth;
}
}

View File

@ -1,68 +0,0 @@
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';
/**
* This class is used to update a Permission entity (via put request).
*/
export class UpdatePermission {
/**
* The updated permission'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 permissions's principal.
* Just has to contain the principal's id -everything else won't be checked or changed.
*/
@IsObject()
@IsNotEmpty()
principal: Principal;
/**
* The permissions's target.
*/
@IsNotEmpty()
target: PermissionTarget;
/**
* The permissions's action.
*/
@IsNotEmpty()
action: PermissionAction;
/**
* Updates a provided Permission entity based on this.
*/
public async updatePermission(permission: Permission): Promise<Permission> {
permission.principal = await this.getPrincipal();
permission.target = this.target;
permission.action = this.action;
return permission;
}
/**
* Loads the updated permission's principal based on it's id.
*/
public async getPrincipal(): Promise<Principal> {
if (this.principal === undefined || this.principal === null) {
throw new PermissionNeedsPrincipalError();
}
if (!isNaN(this.principal.id)) {
let principal = await getConnectionManager().get().getRepository(Principal).findOne({ id: this.principal.id });
if (!principal) { throw new PrincipalNotFoundError(); }
return principal;
}
throw new PrincipalWrongTypeError();
}
}

View File

@ -7,45 +7,43 @@ import { Runner } from '../entities/Runner';
import { RunnerGroup } from '../entities/RunnerGroup';
import { CreateParticipant } from './CreateParticipant';
/**
* This class is used to update a Runner entity (via put request).
*/
export class UpdateRunner extends CreateParticipant {
/**
* The updated runner'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 runner's new team/org.
* Just has to contain the group's id -everything else won't be checked or changed.
*/
@IsObject()
group: RunnerGroup;
/**
* Updates a provided Runner entity based on this.
* Creates a Runner entity from this.
*/
public async updateRunner(runner: Runner): Promise<Runner> {
runner.firstname = this.firstname;
runner.middlename = this.middlename;
runner.lastname = this.lastname;
runner.phone = this.phone;
runner.email = this.email;
runner.group = await this.getGroup();
runner.address = await this.getAddress();
public async toRunner(): Promise<Runner> {
let newRunner: Runner = new Runner();
return runner;
newRunner.id = this.id;
newRunner.firstname = this.firstname;
newRunner.middlename = this.middlename;
newRunner.lastname = this.lastname;
newRunner.phone = this.phone;
newRunner.email = this.email;
newRunner.group = await this.getGroup();
newRunner.address = await this.getAddress();
return newRunner;
}
/**
* Loads the updated runner's group based on it's id.
* Manages all the different ways a group can be provided.
*/
public async getGroup(): Promise<RunnerGroup> {
if (this.group === undefined || this.group === null) {
if (this.group === undefined) {
throw new RunnerTeamNeedsParentError();
}
if (!isNaN(this.group.id)) {

View File

@ -1,52 +0,0 @@
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';
/**
* This class is used to update a RunnerOrganisation entity (via put request).
*/
export class UpdateRunnerOrganisation extends CreateRunnerGroup {
/**
* The updated orgs'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 organisation's address.
* Just has to contain the address's id - everything else won't be checked or changed.
* Optional.
*/
@IsInt()
@IsOptional()
address?: Address;
/**
* Loads the organisation's address based on it's id.
*/
public async getAddress(): Promise<Address> {
if (this.address === undefined || this.address === null) {
return null;
}
let address = await getConnectionManager().get().getRepository(Address).findOne({ id: this.address.id });
if (!address) { throw new AddressNotFoundError; }
return address;
}
/**
* Updates a provided RunnerOrganisation entity based on this.
*/
public async updateRunnerOrganisation(organisation: RunnerOrganisation): Promise<RunnerOrganisation> {
organisation.name = this.name;
organisation.contact = await this.getContact();
organisation.address = await this.getAddress();
return organisation;
}
}

View File

@ -6,31 +6,23 @@ import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { CreateRunnerGroup } from './CreateRunnerGroup';
/**
* This class is used to update a RunnerTeam entity (via put request).
*/
export class UpdateRunnerTeam extends CreateRunnerGroup {
/**
* The updated team'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 team's parentGroup.
* Just has to contain the organisation's id - everything else won't be checked or changed.
* The team's parent group (organisation).
*/
@IsObject()
@IsNotEmpty()
parentGroup: RunnerOrganisation;
/**
* Loads the updated teams's parentGroup based on it's id.
*/
public async getParent(): Promise<RunnerOrganisation> {
if (this.parentGroup === undefined || this.parentGroup === null) {
if (this.parentGroup === undefined) {
throw new RunnerTeamNeedsParentError();
}
if (!isNaN(this.parentGroup.id)) {
@ -43,14 +35,16 @@ export class UpdateRunnerTeam extends CreateRunnerGroup {
}
/**
* Updates a provided RunnerTeam entity based on this.
* Creates a RunnerTeam entity from this.
*/
public async updateRunnerTeam(team: RunnerTeam): Promise<RunnerTeam> {
public async toRunnerTeam(): Promise<RunnerTeam> {
let newRunnerTeam: RunnerTeam = new RunnerTeam();
team.name = this.name;
team.parentGroup = await this.getParent();
team.contact = await this.getContact()
newRunnerTeam.id = this.id;
newRunnerTeam.name = this.name;
newRunnerTeam.parentGroup = await this.getParent();
newRunnerTeam.contact = await this.getContact()
return team;
return newRunnerTeam;
}
}

View File

@ -1,130 +0,0 @@
import * as argon2 from "argon2";
import { IsBoolean, IsEmail, IsInt, IsOptional, IsPhoneNumber, IsString } 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';
/**
* This class is used to update a User entity (via put request).
*/
export class UpdateUser {
/**
* The updated user'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 user's first name.
*/
@IsString()
firstname: string;
/**
* The updated user's middle name.
*/
@IsString()
@IsOptional()
middlename?: string;
/**
* The updated user's last name.
*/
@IsString()
lastname: string;
/**
* The updated user's username.
* You have to provide at least one of: {email, username}.
*/
@IsOptional()
@IsString()
username?: string;
/**
* The updated user's email address.
* You have to provide at least one of: {email, username}.
*/
@IsEmail()
@IsString()
@IsOptional()
email?: string;
/**
* The updated 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 updated's password.
* Only provide it if you want it updated.
* Changeing the password will invalidate all of the user's jwts.
* This will of course not be saved in plaintext :)
*/
@IsString()
@IsOptional()
password?: string;
/**
* Should the user be enabled?
*/
@IsBoolean()
enabled: boolean = true;
/**
* The updated user's groups.
* This just has to contain the group's id - everything else won't be changed.
*/
@IsOptional()
groups?: UserGroup[]
/**
* Updates a provided User entity based on this.
*/
public async updateUser(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)) {
throw new UsernameOrEmailNeededError();
}
if (this.password) {
user.password = await argon2.hash(this.password + user.uuid);
user.refreshTokenCount = user.refreshTokenCount + 1;
}
user.enabled = this.enabled;
user.firstname = this.firstname
user.middlename = this.middlename
user.lastname = this.lastname
user.phone = this.phone;
user.groups = await this.getGroups();
//TODO: ProfilePics
return user;
}
/**
* Loads the updated user's groups based on their ids.
*/
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.id });
if (!found) { throw new UserGroupNotFoundError(); }
groups.push(found);
}
return groups;
}
}

View File

@ -10,8 +10,7 @@ import { Participant } from "./Participant";
import { RunnerOrganisation } from "./RunnerOrganisation";
/**
* Defines the Address entity.
* Implemented this way to prevent any formatting differences.
* Defines a address (to be used for contact information).
*/
@Entity()
export class Address {
@ -24,7 +23,6 @@ export class Address {
/**
* The address's description.
* Optional and mostly for UX.
*/
@Column({ nullable: true })
@IsString()
@ -51,8 +49,6 @@ export class Address {
/**
* The address's postal code.
* This will get checked against the postal code syntax for the configured country.
* TODO: Implement the config option.
*/
@Column()
@IsString()

View File

@ -4,21 +4,19 @@ import { Donation } from "./Donation";
import { Runner } from "./Runner";
/**
* Defines the DistanceDonation entity.
* For distanceDonations a donor pledges to donate a certain amount for each kilometer ran by a runner.
* Defines a distance based donation.
* Here people donate a certain amout per kilometer
*/
@ChildEntity()
export class DistanceDonation extends Donation {
/**
* The donation's associated runner.
* Used as the source of the donation's distance.
* The runner associated.
*/
@IsNotEmpty()
@ManyToOne(() => Runner, runner => runner.distanceDonations)
runner: Runner;
/**
* The donation's amount donated per distance.
* The amount the donor set to be donated per kilometer that the runner ran.
*/
@Column()
@ -28,12 +26,12 @@ export class DistanceDonation extends Donation {
/**
* The donation's amount in cents (or whatever your currency's smallest unit is.).
* Get's calculated from the runner's distance ran and the amount donated per kilometer.
* The exact implementation may differ for each type of donation.
*/
public get amount(): number {
let calculatedAmount = -1;
try {
calculatedAmount = this.amountPerDistance * (this.runner.distance / 1000);
calculatedAmount = this.amountPerDistance * this.runner.distance;
} catch (error) {
throw error;
}

View File

@ -6,9 +6,7 @@ import { Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typ
import { Participant } from "./Participant";
/**
* Defines the Donation entity.
* A donation just associates a donor with a donation amount.
* The specifics of the amoun's determination has to be implemented in child classes.
* Defines the donation interface.
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })

View File

@ -3,13 +3,13 @@ import { ChildEntity, Column } from "typeorm";
import { Participant } from "./Participant";
/**
* Defines the Donor entity.
* Defines a donor.
*/
@ChildEntity()
export class Donor extends Participant {
/**
* Does this donor need a receipt?
* Will later be used to automaticly generate donation receipts.
* Does this donor need a receipt?.
* Default: false
*/
@Column()
@IsBoolean()

View File

@ -3,8 +3,7 @@ import { ChildEntity, Column } from "typeorm";
import { Donation } from "./Donation";
/**
* Defines the FixedDonation entity.
* In the past there was no easy way to track fixed donations (eg. for creating donation receipts).
* Defines a fixed donation.
*/
@ChildEntity()
export class FixedDonation extends Donation {

View File

@ -13,8 +13,7 @@ import { Address } from "./Address";
import { RunnerGroup } from "./RunnerGroup";
/**
* Defines the GroupContact entity.
* Mainly it's own class to reduce duplicate code and enable contact's to be associated with multiple groups.
* Defines a group's contact.
*/
@Entity()
export class GroupContact {
@ -35,6 +34,7 @@ export class GroupContact {
/**
* The contact's middle name.
* Optional
*/
@Column({ nullable: true })
@IsOptional()
@ -51,7 +51,7 @@ export class GroupContact {
/**
* The contact's address.
* This is a address object to prevent any formatting differences.
* Optional
*/
@IsOptional()
@ManyToOne(() => Address, address => address.participants, { nullable: true })
@ -59,7 +59,7 @@ export class GroupContact {
/**
* The contact's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
* Optional
*/
@Column({ nullable: true })
@IsOptional()
@ -68,7 +68,7 @@ export class GroupContact {
/**
* The contact's email address.
* Could later be used to automaticly send mails concerning the contact's associated groups.
* Optional
*/
@Column({ nullable: true })
@IsOptional()

View File

@ -13,8 +13,7 @@ import { Address } from "./Address";
import { Donation } from "./Donation";
/**
* Defines the Participant entity.
* Participans can donate and therefor be associated with donation entities.
* Defines the participant interface.
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
@ -36,6 +35,7 @@ export abstract class Participant {
/**
* The participant's middle name.
* Optional
*/
@Column({ nullable: true })
@IsOptional()
@ -52,14 +52,14 @@ export abstract class Participant {
/**
* The participant's address.
* This is a address object to prevent any formatting differences.
* Optional
*/
@ManyToOne(() => Address, address => address.participants, { nullable: true })
address?: Address;
/**
* The participant's phone number.
* This will be validated against the configured country phone numer syntax (default: international).
* Optional
*/
@Column({ nullable: true })
@IsOptional()
@ -68,7 +68,7 @@ export abstract class Participant {
/**
* The participant's email address.
* Can be used to contact the participant.
* Optional
*/
@Column({ nullable: true })
@IsOptional()
@ -77,7 +77,6 @@ export abstract class Participant {
/**
* Used to link the participant as the donor of a donation.
* Attention: Only runner's can be associated as a distanceDonations distance source.
*/
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
donations: Donation[];

View File

@ -1,19 +1,17 @@
import {
IsEnum,
IsInt,
IsNotEmpty
IsNotEmpty,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { Principal } from './Principal';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { User } from './User';
import { UserGroup } from './UserGroup';
/**
* Defines the Permission entity.
* Permissions can be granted to principals.
* The permissions possible targets and actions are defined in enums.
* Defines the Permission interface.
*/
@Entity()
export class Permission {
export abstract class Permission {
/**
* Autogenerated unique id (primary key).
*/
@ -22,33 +20,30 @@ export class Permission {
id: number;
/**
* The permission's principal.
* users
*/
@ManyToOne(() => Principal, principal => principal.permissions)
principal: Principal;
@OneToMany(() => User, user => user.permissions, { nullable: true })
users: User[]
/**
* The permission's target.
* This get's stored as the enum value's string representation for compatability reasons.
* groups
*/
@Column({ type: 'varchar' })
@OneToMany(() => UserGroup, group => group.permissions, { nullable: true })
groups: UserGroup[]
/**
* The target
*/
@Column()
@IsNotEmpty()
@IsEnum(PermissionTarget)
target: PermissionTarget;
@IsString()
target: string;
/**
* The permission's action.
* This get's stored as the enum value's string representation for compatability reasons.
* The action type
*/
@Column({ type: 'varchar' })
@IsEnum(PermissionAction)
action: PermissionAction;
/**
* Turn this into a string for exporting and jwts.
* Mainly used to shrink the size of jwts (otherwise the would contain entire objects).
*/
public toString(): string {
return this.target + ":" + this.action;
}
@Column()
@IsNotEmpty()
@IsString()
action: string;
}

View File

@ -1,30 +0,0 @@
import { IsInt } from 'class-validator';
import { Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { Permission } from './Permission';
/**
* Defines the principal entity.
* A principal basicly is any entity that can receive permissions for the api (users and their groups).
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
export abstract class Principal {
/**
* Autogenerated unique id (primary key).
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;
/**
* The participant's permissions.
*/
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
permissions: Permission[];
/**
* Turns this entity into it's response class.
*/
public abstract toResponse(): ResponsePrincipal;
}

View File

@ -7,52 +7,44 @@ import { RunnerGroup } from "./RunnerGroup";
import { Scan } from "./Scan";
/**
* Defines the runner entity.
* Runners differ from participants in being able to actually accumulate a ran distance through scans.
* Runner's get organized in groups.
* Defines a runner.
*/
@ChildEntity()
export class Runner extends Participant {
/**
* The runner's associated group.
* Can be a runner team or organisation.
*/
@IsNotEmpty()
@ManyToOne(() => RunnerGroup, group => group.runners, { nullable: false })
group: RunnerGroup;
/**
* The runner's associated distanceDonations.
* Used to link runners to distanceDonations in order to calculate the donation's amount based on the distance the runner ran.
* Used to link runners to donations.
*/
@OneToMany(() => DistanceDonation, distanceDonation => distanceDonation.runner, { nullable: true })
distanceDonations: DistanceDonation[];
/**
* The runner's associated cards.
* Used to link runners to cards - yes a runner be associated with multiple cards this came in handy in the past.
* Used to link runners to cards.
*/
@OneToMany(() => RunnerCard, card => card.runner, { nullable: true })
cards: RunnerCard[];
/**
* The runner's associated scans.
* Used to link runners to scans (valid and fraudulant).
* Used to link runners to a scans
*/
@OneToMany(() => Scan, scan => scan.runner, { nullable: true })
scans: Scan[];
/**
* Returns all valid scans associated with this runner.
* This is implemented here to avoid duplicate code in other files.
*/
public get validScans(): Scan[] {
return this.scans.filter(scan => { scan.valid === true });
}
/**
* Returns the total distance ran by this runner based on all his valid scans.
* This is implemented here to avoid duplicate code in other files.
* Returns the total distance ran by this runner.
*/
@IsInt()
public get distance(): number {

View File

@ -11,9 +11,7 @@ import { Runner } from "./Runner";
import { TrackScan } from "./TrackScan";
/**
* Defines the RunnerCard entity.
* A runnerCard is a physical representation for a runner.
* It can be associated with a runner to create scans via the scan station's.
* Defines a card that can be scanned via a scanner station.
*/
@Entity()
export class RunnerCard {
@ -25,8 +23,7 @@ export class RunnerCard {
id: number;
/**
* The card's currently associated runner.
* To increase reusability a card can be reassigned.
* The runner that is currently associated with this card.
*/
@IsOptional()
@ManyToOne(() => Runner, runner => runner.cards, { nullable: true })
@ -35,7 +32,7 @@ export class RunnerCard {
/**
* The card's code.
* This has to be able to being converted to something barcode compatible.
* Will get automaticlly generated (not implemented yet).
* could theoretically be autogenerated
*/
@Column()
@IsEAN()
@ -52,8 +49,7 @@ export class RunnerCard {
enabled: boolean = true;
/**
* The card's associated scans.
* Used to link cards to track scans.
* Used to link cards to a track scans.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];

View File

@ -9,8 +9,7 @@ import { GroupContact } from "./GroupContact";
import { Runner } from "./Runner";
/**
* Defines the RunnerGroup entity.
* This is used to group runners together (as the name suggests).
* Defines the runnerGroup interface.
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
@ -32,14 +31,13 @@ export abstract class RunnerGroup {
/**
* The group's contact.
* This is mostly a feature for the group managers and public relations.
* Optional
*/
@IsOptional()
@ManyToOne(() => GroupContact, contact => contact.groups, { nullable: true })
contact?: GroupContact;
/**
* The group's associated runners.
* Used to link runners to a runner group.
*/
@OneToMany(() => Runner, runner => runner.group, { nullable: true })

View File

@ -5,22 +5,21 @@ import { RunnerGroup } from "./RunnerGroup";
import { RunnerTeam } from "./RunnerTeam";
/**
* Defines the RunnerOrganisation entity.
* This usually is a school, club or company.
* Defines a runner organisation (business or school for example).
*/
@ChildEntity()
export class RunnerOrganisation extends RunnerGroup {
/**
* The organisations's address.
* Optional
*/
@IsOptional()
@ManyToOne(() => Address, address => address.groups, { nullable: true })
address?: Address;
/**
* The organisation's teams.
* Used to link teams to a organisation.
* Used to link teams to runner groups.
*/
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
teams: RunnerTeam[];

View File

@ -4,15 +4,14 @@ import { RunnerGroup } from "./RunnerGroup";
import { RunnerOrganisation } from "./RunnerOrganisation";
/**
* Defines the RunnerTeam entity.
* This usually is a school class or department in a company.
* Defines a runner team (class or deparment for example).
*/
@ChildEntity()
export class RunnerTeam extends RunnerGroup {
/**
* The team's parent group.
* Every team has to be part of a runnerOrganisation - this get's checked on creation and update.
* Optional
*/
@IsNotEmpty()
@ManyToOne(() => RunnerOrganisation, org => org.teams, { nullable: true })

View File

@ -9,8 +9,7 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance } f
import { Runner } from "./Runner";
/**
* Defines the Scan entity.
* A scan basicly adds a certain distance to a runner's total ran distance.
* Defines the scan interface.
*/
@Entity()
@TableInheritance({ column: { name: "type", type: "varchar" } })
@ -23,8 +22,7 @@ export abstract class Scan {
id: number;
/**
* The scan's associated runner.
* This is important to link ran distances to runners.
* The associated runner.
*/
@IsNotEmpty()
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
@ -32,7 +30,6 @@ export abstract class Scan {
/**
* The scan's distance in meters.
* Can be set manually or derived from another object.
*/
@IsInt()
@IsPositive()
@ -40,7 +37,6 @@ export abstract class Scan {
/**
* Is the scan valid (for fraud reasons).
* The determination of validity will work differently for every child class.
* Default: true
*/
@Column()

View File

@ -10,8 +10,7 @@ import { Track } from "./Track";
import { TrackScan } from "./TrackScan";
/**
* Defines the ScanStation entity.
* ScanStations get used to create TrackScans for runners based on a scan of their runnerCard.
* ScannerStations have the ability to create scans for specific tracks.
*/
@Entity()
export class ScanStation {
@ -24,7 +23,6 @@ export class ScanStation {
/**
* The station's description.
* Mostly for better UX when traceing back stuff.
*/
@Column({ nullable: true })
@IsOptional()
@ -33,7 +31,6 @@ export class ScanStation {
/**
* The track this station is associated with.
* All scans created by this station will also be associated with this track.
*/
@IsNotEmpty()
@ManyToOne(() => Track, track => track.stations, { nullable: false })
@ -41,7 +38,6 @@ export class ScanStation {
/**
* The station's api key.
* This is used to authorize a station against the api (not implemented yet).
*/
@Column()
@IsNotEmpty()
@ -49,7 +45,7 @@ export class ScanStation {
key: string;
/**
* Is the station enabled (for fraud and setup reasons)?
* Is the station enabled (for fraud reasons)?
* Default: true
*/
@Column()

View File

@ -1,6 +1,7 @@
import {
IsInt,
IsNotEmpty,
IsPositive,
IsString
} from "class-validator";
@ -9,7 +10,7 @@ import { ScanStation } from "./ScanStation";
import { TrackScan } from "./TrackScan";
/**
* Defines the Track entity.
* Defines a track of given length.
*/
@Entity()
export class Track {
@ -22,7 +23,6 @@ export class Track {
/**
* The track's name.
* Mainly here for UX.
*/
@Column()
@IsString()
@ -31,7 +31,6 @@ export class Track {
/**
* The track's length/distance in meters.
* Will be used to calculate runner's ran distances.
*/
@Column()
@IsInt()
@ -39,15 +38,13 @@ export class Track {
distance: number;
/**
* Used to link scan stations to a certain track.
* This makes the configuration of the scan stations easier.
* Used to link scan stations to track.
*/
@OneToMany(() => ScanStation, station => station.track, { nullable: true })
stations: ScanStation[];
/**
* Used to link track scans to a track.
* The scan will derive it's distance from the track's distance.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];

View File

@ -12,30 +12,26 @@ import { ScanStation } from "./ScanStation";
import { Track } from "./Track";
/**
* Defines the TrackScan entity.
* A track scan usaually get's generated by a scan station.
* Defines the scan interface.
*/
@ChildEntity()
export class TrackScan extends Scan {
/**
* The scan's associated track.
* This is used to determine the scan's distance.
* The associated track.
*/
@IsNotEmpty()
@ManyToOne(() => Track, track => track.scans, { nullable: true })
track: Track;
/**
* The runnerCard associated with the scan.
* This get's saved for documentation and management purposes.
* The associated card.
*/
@IsNotEmpty()
@ManyToOne(() => RunnerCard, card => card.scans, { nullable: true })
card: RunnerCard;
/**
* The scanning station that created the scan.
* Mainly used for logging and traceing back scans (or errors)
* The scanning station.
*/
@IsNotEmpty()
@ManyToOne(() => ScanStation, station => station.scans, { nullable: true })
@ -43,7 +39,6 @@ export class TrackScan extends Scan {
/**
* The scan's distance in meters.
* This just get's loaded from it's track.
*/
@IsInt()
@IsPositive()
@ -53,7 +48,6 @@ export class TrackScan extends Scan {
/**
* The scan's creation timestamp.
* Will be used to implement fraud detection.
*/
@Column()
@IsDateString()

View File

@ -1,36 +1,38 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUUID } from "class-validator";
import { ChildEntity, Column, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUser } from '../responses/ResponseUser';
import { Principal } from './Principal';
import { Permission } from './Permission';
import { UserAction } from './UserAction';
import { UserGroup } from './UserGroup';
/**
* Defines the User entity.
* Users are the ones that can use the "admin" webui and do stuff in the backend.
* Defines a admin user.
*/
@ChildEntity()
export class User extends Principal {
@Entity()
export class User {
/**
* The user's uuid.
* Mainly gets used as a per-user salt for the password hash.
* autogenerated unique id (primary key).
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;
/**
* uuid
*/
@Column({ unique: true })
@IsUUID(4)
uuid: string;
/**
* The user's e-mail address.
* Either username or email has to be set (otherwise the user couldn't log in).
* user email
*/
@Column({ nullable: true, unique: true })
@IsEmail()
email?: string;
/**
* The user's phone number.
* user phone
*/
@Column({ nullable: true })
@IsOptional()
@ -38,15 +40,14 @@ export class User extends Principal {
phone?: string;
/**
* The user's username.
* Either username or email has to be set (otherwise the user couldn't log in).
* username
*/
@Column({ nullable: true, unique: true })
@IsString()
username?: string;
/**
* The user's first name.
* firstname
*/
@Column()
@IsString()
@ -54,7 +55,7 @@ export class User extends Principal {
firstname: string;
/**
* The user's middle name.
* middlename
*/
@Column({ nullable: true })
@IsString()
@ -62,7 +63,7 @@ export class User extends Principal {
middlename?: string;
/**
* The user's last name.
* lastname
*/
@Column()
@IsString()
@ -70,8 +71,7 @@ export class User extends Principal {
lastname: string;
/**
* The user's password.
* This is a argon2 hash salted with the user's uuid.
* password
*/
@Column()
@IsString()
@ -79,8 +79,14 @@ export class User extends Principal {
password: string;
/**
* The groups this user is a part of.
* The user will inherit the groups permissions (without overwriting his own).
* permissions
*/
@IsOptional()
@ManyToOne(() => Permission, permission => permission.users, { nullable: true })
permissions?: Permission[];
/**
* groups
*/
@IsOptional()
@ManyToMany(() => UserGroup, { nullable: true })
@ -88,23 +94,21 @@ export class User extends Principal {
groups: UserGroup[];
/**
* Is this user enabled?
* is user enabled?
*/
@Column()
@IsBoolean()
enabled: boolean = true;
/**
* The user's jwt refresh token count.
* Used to invalidate jwts.
* jwt refresh count
*/
@IsInt()
@Column({ default: 1 })
refreshTokenCount?: number;
/**
* The user's profile picture.
* We haven't decided yet if this will be a bas64 encoded image or just a link to the profile picture.
* profilepic
*/
@Column({ nullable: true, unique: true })
@IsString()
@ -112,17 +116,27 @@ export class User extends Principal {
profilePic?: string;
/**
* The actions performed by this user.
* For documentation purposes only, will be implemented later.
* actions
*/
@IsOptional()
@OneToMany(() => UserAction, action => action.user, { nullable: true })
actions: UserAction[]
/**
* Turns this entity into it's response class.
* calculate all permissions
*/
public toResponse(): ResponsePrincipal {
return new ResponseUser(this);
public get calc_permissions(): Permission[] {
let final_permissions = []
this.groups.forEach((permission) => {
if (!final_permissions.includes(permission)) {
final_permissions.push(permission)
}
})
this.permissions.forEach((permission) => {
if (!final_permissions.includes(permission)) {
final_permissions.push(permission)
}
})
return final_permissions
}
}

View File

@ -1,17 +1,14 @@
import {
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString
} from "class-validator";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { User } from './User';
/**
* Defines the UserAction entity.
* Will later be used to document a user's actions.
* Defines the UserAction interface.
*/
@Entity()
export class UserAction {
@ -23,7 +20,7 @@ export class UserAction {
id: number;
/**
* The user that performed the action.
* user
*/
@ManyToOne(() => User, user => user.actions)
user: User
@ -37,16 +34,15 @@ export class UserAction {
target: string;
/**
* The actions's action (e.g. UPDATE).
* Directly pulled from the PermissionAction Enum.
* The actions's action (e.g. UPDATE)
*/
@Column({ type: 'varchar' })
@IsEnum(PermissionAction)
action: PermissionAction;
@Column()
@IsNotEmpty()
@IsString()
action: string;
/**
* The description of the change (before-> after; e.g. distance:15->17).
* Will later be defined in more detail.
* The description of change (before-> after; e.g. distance:15->17)
*/
@Column({ nullable: true })
@IsOptional()

View File

@ -1,19 +1,29 @@
import {
IsInt,
IsNotEmpty,
IsOptional,
IsString
} from "class-validator";
import { ChildEntity, Column } from "typeorm";
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUserGroup } from '../responses/ResponseUserGroup';
import { Principal } from './Principal';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Permission } from "./Permission";
/**
* Defines the UserGroup entity.
* This entity describes a group of users with a set of permissions.
* Defines the UserGroup interface.
*/
@ChildEntity()
export class UserGroup extends Principal {
@Entity()
export class UserGroup {
/**
* Autogenerated unique id (primary key).
*/
@PrimaryGeneratedColumn()
@IsInt()
id: number;
/**
* permissions
*/
@ManyToOne(() => Permission, permission => permission.groups, { nullable: true })
permissions: Permission[];
/**
* The group's name
@ -30,11 +40,4 @@ export class UserGroup extends Principal {
@IsOptional()
@IsString()
description?: string;
/**
* Turns this entity into it's response class.
*/
public toResponse(): ResponsePrincipal {
return new ResponseUserGroup(this);
}
}

View File

@ -1,10 +0,0 @@
/**
* This enum contains all posible actions for permissions.
*/
export enum PermissionAction {
GET = 'GET',
CREATE = 'CREATE',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
IMPORT = 'IMPORT'
}

View File

@ -1,12 +0,0 @@
/**
* This enum contains all posible targets for permissions.
*/
export enum PermissionTarget {
RUNNER = 'RUNNER',
ORGANISATION = 'ORGANISATION',
TEAM = 'TEAM',
TRACK = 'TRACK',
USER = 'USER',
USERGROUP = 'USERGROUP',
PERMISSION = 'PERMISSION'
}

View File

@ -1,29 +1,26 @@
import { IsInt, IsString } from 'class-validator';
/**
* Defines the repsonse auth.
* Defines a auth object
*/
export class Auth {
/**
* The access_token - JWT shortterm access token.
* access_token - JWT shortterm access token
*/
@IsString()
access_token: string;
/**
* The refresh_token - longterm refresh token (used for requesting new access tokens).
* refresh_token - longterm refresh token (used for requesting new access tokens)
*/
@IsString()
refresh_token: string;
/**
* The unix timestamp for access the token's expiry.
* access_token_expires_at - unix timestamp of access token expiry
*/
@IsInt()
access_token_expires_at: number;
/**
* The unix unix timestamp for the access token's expiry.
* refresh_token_expires_at - unix timestamp of access token expiry
*/
@IsInt()
refresh_token_expires_at: number;

View File

@ -1,7 +1,7 @@
import { IsString } from 'class-validator';
/**
* Defines a empty response object.
* Defines a empty response object
*/
export class ResponseEmpty {
@IsString()

View File

@ -1,11 +1,11 @@
import { IsString } from 'class-validator';
/**
* Defines the logout response.
* Defines a Logout object
*/
export class Logout {
/**
* The logout's timestamp.
* timestamp of logout
*/
@IsString()
timestamp: number;

View File

@ -1,15 +1,21 @@
import { IsInt, IsString } from "class-validator";
import {
IsInt,
IsString
} from "class-validator";
import { Participant } from '../entities/Participant';
/**
* Defines the participant response.
* Defines a participant response.
*/
export abstract class ResponseParticipant {
/**
* The participant's id.
* Autogenerated unique id (primary key).
*/
@IsInt()
id: number;
id: number;;
/**
* The participant's first name.
@ -19,6 +25,7 @@ export abstract class ResponseParticipant {
/**
* The participant's middle name.
* Optional.
*/
@IsString()
middlename?: string;
@ -31,20 +38,18 @@ export abstract class ResponseParticipant {
/**
* The participant's phone number.
* Optional.
*/
@IsString()
phone?: string;
/**
* The participant's e-mail address.
* Optional.
*/
@IsString()
email?: string;
/**
* Creates a ResponseParticipant object from a participant.
* @param participant The participant the response shall be build for.
*/
public constructor(participant: Participant) {
this.id = participant.id;
this.firstname = participant.firstname;

View File

@ -1,53 +0,0 @@
import {
IsEnum,
IsInt,
IsNotEmpty,
IsObject
} from "class-validator";
import { Permission } from '../entities/Permission';
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { ResponsePrincipal } from './ResponsePrincipal';
/**
* Defines the permission response.
*/
export class ResponsePermission {
/**
* The permission's id.
*/
@IsInt()
id: number;;
/**
* The permissions's principal.
*/
@IsObject()
@IsNotEmpty()
principal: ResponsePrincipal;
/**
* The permissions's target.
*/
@IsNotEmpty()
@IsEnum(PermissionTarget)
target: PermissionTarget;
/**
* The permissions's action.
*/
@IsNotEmpty()
@IsEnum(PermissionAction)
action: PermissionAction;
/**
* Creates a ResponsePermission object from a permission.
* @param permission The permission the response shall be build for.
*/
public constructor(permission: Permission) {
this.id = permission.id;
this.principal = permission.principal.toResponse();
this.target = permission.target;
this.action = permission.action;
}
}

View File

@ -1,24 +0,0 @@
import {
IsInt
} from "class-validator";
import { Principal } from '../entities/Principal';
/**
* Defines the principal response.
*/
export abstract class ResponsePrincipal {
/**
* The principal's id.
*/
@IsInt()
id: number;
/**
* Creates a ResponsePrincipal object from a principal.
* @param principal The principal the response shall be build for.
*/
public constructor(principal: Principal) {
this.id = principal.id;
}
}

View File

@ -7,12 +7,13 @@ import { RunnerGroup } from '../entities/RunnerGroup';
import { ResponseParticipant } from './ResponseParticipant';
/**
* Defines the runner response.
* Defines RunnerTeam's response class.
*/
export class ResponseRunner extends ResponseParticipant {
/**
* The runner's currently ran distance in meters.
* Optional.
*/
@IsInt()
distance: number;
@ -23,10 +24,6 @@ export class ResponseRunner extends ResponseParticipant {
@IsObject()
group: RunnerGroup;
/**
* Creates a ResponseRunner object from a runner.
* @param runner The user the response shall be build for.
*/
public constructor(runner: Runner) {
super(runner);
this.distance = runner.scans.filter(scan => { scan.valid === true }).reduce((sum, current) => sum + current.distance, 0);

View File

@ -1,20 +1,38 @@
import { IsInt, IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
import {
IsInt,
IsNotEmpty,
IsObject,
IsOptional,
IsString
} from "class-validator";
import { GroupContact } from '../entities/GroupContact';
import { RunnerGroup } from '../entities/RunnerGroup';
/**
* Defines the runnerGroup response.
* Defines a track of given length.
*/
export abstract class ResponseRunnerGroup {
/**
* The runnerGroup's id.
* Autogenerated unique id (primary key).
*/
@IsInt()
@IsNotEmpty()
id: number;;
/**
* The runnerGroup's name.
* The groups's name.
*/
@IsString()
@IsNotEmpty()
@ -22,16 +40,13 @@ export abstract class ResponseRunnerGroup {
/**
* The runnerGroup's contact.
* The group's contact.
* Optional.
*/
@IsObject()
@IsOptional()
contact?: GroupContact;
/**
* Creates a ResponseRunnerGroup object from a runnerGroup.
* @param group The runnerGroup the response shall be build for.
*/
public constructor(group: RunnerGroup) {
this.id = group.id;
this.name = group.name;

View File

@ -9,27 +9,25 @@ import { RunnerTeam } from '../entities/RunnerTeam';
import { ResponseRunnerGroup } from './ResponseRunnerGroup';
/**
* Defines the runnerOrganisation response.
* Defines RunnerOrgs's response class.
*/
export class ResponseRunnerOrganisation extends ResponseRunnerGroup {
/**
* The runnerOrganisation's address.
* The orgs's address.
* Optional.
*/
@IsObject()
@IsNotEmpty()
address?: Address;
/**
* The runnerOrganisation associated teams.
* The orgs associated teams.
*/
@IsArray()
teams: RunnerTeam[];
/**
* Creates a ResponseRunnerOrganisation object from a runnerOrganisation.
* @param org The runnerOrganisation the response shall be build for.
*/
public constructor(org: RunnerOrganisation) {
super(org);
this.address = org.address;

View File

@ -1,24 +1,24 @@
import { IsNotEmpty, IsObject } from "class-validator";
import {
IsNotEmpty,
IsObject
} from "class-validator";
import { RunnerOrganisation } from '../entities/RunnerOrganisation';
import { RunnerTeam } from '../entities/RunnerTeam';
import { ResponseRunnerGroup } from './ResponseRunnerGroup';
/**
* Defines the runnerTeam response.
* Defines RunnerTeam's response class.
*/
export class ResponseRunnerTeam extends ResponseRunnerGroup {
/**
* The runnerTeam's parent group (organisation).
* The team's parent group (organisation).
* Optional.
*/
@IsObject()
@IsNotEmpty()
parentGroup: RunnerOrganisation;
/**
* Creates a ResponseRunnerTeam object from a runnerTeam.
* @param team The team the response shall be build for.
*/
public constructor(team: RunnerTeam) {
super(team);
this.parentGroup = team.parentGroup;

View File

@ -1,12 +1,16 @@
import { IsInt, IsString } from "class-validator";
import {
IsInt,
IsString
} from "class-validator";
import { Track } from '../entities/Track';
/**
* Defines the track response.
* Defines a track of given length.
*/
export class ResponseTrack {
/**
* The track's id.
* Autogenerated unique id (primary key).
*/
@IsInt()
id: number;;
@ -23,10 +27,6 @@ export class ResponseTrack {
@IsInt()
distance: number;
/**
* Creates a ResponseTrack object from a track.
* @param track The track the response shall be build for.
*/
public constructor(track: Track) {
this.id = track.id;
this.name = track.name;

View File

@ -1,97 +0,0 @@
import {
IsArray,
IsBoolean,
IsOptional,
IsString
} from "class-validator";
import { Permission } from '../entities/Permission';
import { User } from '../entities/User';
import { UserGroup } from '../entities/UserGroup';
import { ResponsePrincipal } from './ResponsePrincipal';
/**
* Defines the user response.
*/
export class ResponseUser extends ResponsePrincipal {
/**
* The user's first name.
*/
@IsString()
firstname: string;
/**
* The user's middle name.
*/
@IsString()
middlename?: string;
/**
* The user's last name.
*/
@IsString()
lastname: string;
/**
* The user's phone number.
*/
@IsString()
phone?: string;
/**
* The user's e-mail address.
*/
@IsString()
email?: string;
/**
* The user's username.
*/
@IsString()
username?: string;
/**
* Is user enabled?
*/
@IsBoolean()
enabled: boolean = true;
/**
* The user's profile pic.
*/
@IsString()
@IsOptional()
profilePic?: string;
/**
* The groups that the user is a part of.
*/
@IsArray()
@IsOptional()
groups: UserGroup[];
/**
* The user's permissions.
*/
@IsArray()
@IsOptional()
permissions: Permission[];
/**
* Creates a ResponseUser object from a user.
* @param user The user the response shall be build for.
*/
public constructor(user: User) {
super(user);
this.firstname = user.firstname;
this.middlename = user.middlename;
this.lastname = user.lastname;
this.phone = user.phone;
this.email = user.email;
this.username = user.username;
this.enabled = user.enabled;
this.profilePic = user.profilePic;
this.groups = user.groups;
this.permissions = user.permissions;
}
}

View File

@ -1,41 +0,0 @@
import { IsArray, IsNotEmpty, IsOptional, IsString } from "class-validator";
import { Permission } from '../entities/Permission';
import { UserGroup } from '../entities/UserGroup';
import { ResponsePrincipal } from './ResponsePrincipal';
/**
* Defines the userGroup response.
*/
export class ResponseUserGroup extends ResponsePrincipal {
/**
* The userGroup's name.
*/
@IsNotEmpty()
@IsString()
name: string;
/**
* The userGroup's description.
*/
@IsOptional()
@IsString()
description?: string;
/**
* The userGroup's permissions.
*/
@IsArray()
@IsOptional()
permissions: Permission[];
/**
* Creates a ResponseUserGroup object from a userGroup.
* @param group The userGroup the response shall be build for.
*/
public constructor(group: UserGroup) {
super(group);
this.name = group.name;
this.description = group.description;
this.permissions = group.permissions;
}
}

View File

@ -1,56 +0,0 @@
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
import consola from "consola";
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 { ErrorHandler } from './middlewares/ErrorHandler';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
createExpressServer({
authorizationChecker: authchecker,
middlewares: [ErrorHandler],
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
});
const storage = getMetadataArgsStorage();
const schemas = validationMetadatasToSchemas({
refPointerPrefix: "#/components/schemas/",
});
//Spec creation based on the previously created schemas
const spec = routingControllersToSpec(
storage,
{
routePrefix: "/api"
},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
info: {
description: "The the backend API for the LfK! runner system.",
title: "LfK! Backend API",
version: "1.0.0",
},
}
);
try {
fs.writeFileSync("./openapi.json", JSON.stringify(spec), { encoding: "utf-8" });
consola.success("Exported openapi spec to openapi.json");
} catch (error) {
consola.error("Couldn't export the openapi spec");
}

View File

@ -1,52 +0,0 @@
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 { Permission } from '../models/entities/Permission';
import { User } from '../models/entities/User';
import { UserGroup } from '../models/entities/UserGroup';
import { PermissionAction } from '../models/enums/PermissionAction';
import { PermissionTarget } from '../models/enums/PermissionTargets';
/**
* Seeds a admin group with a demo user into the database for initial setup and auto recovery.
* We know that the nameing isn't perfectly fitting. Feel free to change it.
*/
export default class SeedUsers implements Seeder {
public async run(factory: Factory, connection: Connection): Promise<any> {
let adminGroup: UserGroup = await this.createAdminGroup(connection);
await this.createUser(connection, adminGroup.id);
await this.createPermissions(connection, adminGroup.id);
}
public async createAdminGroup(connection: Connection) {
let adminGroup = new CreateUserGroup();
adminGroup.name = "ADMINS";
adminGroup.description = "Have all possible permissions";
return await connection.getRepository(UserGroup).save(await adminGroup.toUserGroup());
}
public async createUser(connection: Connection, group: number) {
let initialUser = new CreateUser();
initialUser.firstname = "demo";
initialUser.lastname = "demo";
initialUser.username = "demo";
initialUser.password = "demo";
initialUser.groups = group;
return await connection.getRepository(User).save(await initialUser.toUser());
}
public async createPermissions(connection: Connection, principal: number) {
let repo = await connection.getRepository(Permission);
for (let target in PermissionTarget) {
for (let action in PermissionAction) {
let permission = new CreatePermission;
permission.target = <PermissionTarget>target;
permission.action = <PermissionAction>action;
permission.principal = principal;
await repo.save(await permission.toPermission());
}
}
}
}

View File

@ -1,35 +0,0 @@
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/openapi.json', () => {
it('is http 200', async () => {
const res = await axios.get(base + '/api/openapi.json');
expect(res.status).toEqual(200);
});
});
describe('GET /', () => {
it('is http 404', async () => {
const res = await axios.get(base + '/', axios_config);
expect(res.status).toEqual(404);
});
});
describe('GET /api/teams', () => {
it('is http 200 && is json', async () => {
const res = await axios.get(base + '/api/teams', axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
});

View File

@ -1,90 +0,0 @@
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/organisations', () => {
it('basic get should return 200', async () => {
const res = await axios.get(base + '/api/organisations', axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('POST /api/organisations', () => {
it('creating a new org with just a name should return 200', async () => {
const res = await axios.post(base + '/api/organisations', {
"name": "test123"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
it('creating a new org with without a name should return 400', async () => {
const res = await axios.post(base + '/api/organisations', {
"name": null
}, axios_config);
expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('adding + getting from all orgs', () => {
it('creating a new org with just a name should return 200', async () => {
const res = await axios.post(base + '/api/organisations', {
"name": "test123"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
it('check if org was added', async () => {
const res = await axios.get(base + '/api/organisations', axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
let added_org = res.data[res.data.length - 1]
delete added_org.id
expect(added_org).toEqual({
"name": "test123",
"contact": null,
"address": null,
"teams": []
})
});
});
// ---------------
describe('adding + getting explicitly', () => {
let added_org_id
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);
let added_org = res1.data
added_org_id = added_org.id;
expect(res1.status).toEqual(200);
expect(res1.headers['content-type']).toContain("application/json")
});
it('check if org was added', async () => {
const res2 = await axios.get(base + '/api/organisations/' + added_org_id, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
let added_org2 = res2.data
added_org_id = added_org2.id;
delete added_org2.id
expect(added_org2).toEqual({
"name": "test123",
"contact": null,
"address": null,
"teams": []
})
});
});

View File

@ -1,132 +0,0 @@
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 + deletion (non-existant)', () => {
it('delete', async () => {
const res2 = await axios.delete(base + '/api/organisations/0', axios_config);
expect(res2.status).toEqual(204);
});
});
// ---------------
describe('adding + deletion (successfull)', () => {
let added_org_id
let added_org
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
added_org_id = added_org.id;
expect(res1.status).toEqual(200);
expect(res1.headers['content-type']).toContain("application/json")
});
it('delete', async () => {
const res2 = await axios.delete(base + '/api/organisations/' + added_org_id, axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
let added_org2 = res2.data
added_org_id = added_org2.id;
delete added_org2.id
expect(added_org2).toEqual({
"name": "test123",
"contact": null,
"address": null,
"teams": []
});
});
it('check if org really was deleted', async () => {
const res3 = await axios.get(base + '/api/organisations/' + added_org_id, axios_config);
expect(res3.status).toEqual(404);
expect(res3.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('adding + deletion with teams still existing (without force)', () => {
let added_org;
let added_org_id;
let added_team;
let added_team_id
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;
added_org_id = added_org.id;
expect(res1.status).toEqual(200);
expect(res1.headers['content-type']).toContain("application/json")
});
it('creating a new team with a valid org should return 200', async () => {
const res2 = await axios.post(base + '/api/teams', {
"name": "test123",
"parentGroup": added_org_id
}, axios_config);
added_team = res2.data;
added_team_id = added_team.id;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
});
it('delete org - this should fail with a 406', async () => {
const res2 = await axios.delete(base + '/api/organisations/' + added_org_id, axios_config);
expect(res2.status).toEqual(406);
expect(res2.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('adding + deletion with teams still existing (with force)', () => {
let added_org;
let added_org_id;
let added_team;
let added_team_id
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;
added_org_id = added_org.id;
expect(res1.status).toEqual(200);
expect(res1.headers['content-type']).toContain("application/json")
});
it('creating a new team with a valid org should return 200', async () => {
const res2 = await axios.post(base + '/api/teams', {
"name": "test123",
"parentGroup": added_org_id
}, axios_config);
added_team = res2.data;
added_team_id = added_team.id;
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
});
it('delete', async () => {
const res2 = await axios.delete(base + '/api/organisations/' + added_org_id + '?force=true', axios_config);
expect(res2.status).toEqual(200);
expect(res2.headers['content-type']).toContain("application/json")
let added_org2 = res2.data
added_org_id = added_org2.id;
delete added_org2.id;
delete added_org2.teams;
expect(added_org2).toEqual({
"name": "test123",
"contact": null,
"address": null
});
});
it('check if org really was deleted', async () => {
const res3 = await axios.get(base + '/api/organisations/' + added_org_id, axios_config);
expect(res3.status).toEqual(404);
expect(res3.headers['content-type']).toContain("application/json")
});
});

View File

@ -1,31 +0,0 @@
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/organisations', () => {
it('basic get should return 200', async () => {
const res = await axios.get(base + '/api/organisations', axios_config);
expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json")
});
});
// ---------------
describe('GET /api/organisations/0', () => {
it('basic get should return 404', async () => {
const res = await axios.get(base + '/api/runners/0', axios_config);
expect(res.status).toEqual(404);
expect(res.headers['content-type']).toContain("application/json")
});
});

Some files were not shown because too many files have changed in this diff Show More