Merge branch 'dev' into feature/13-runner_controllers
This commit is contained in:
		| @@ -53,9 +53,11 @@ docker-compose up --build | ||||
|  | ||||
| ## File Structure | ||||
|  | ||||
| - src/models/\* - database models (typeorm entities) | ||||
| - src/models/entities\* - database models (typeorm entities) | ||||
| - src/models/actions\* - actions models | ||||
| - src/models/responses\* - response models | ||||
| - src/controllers/\* - routing-controllers | ||||
| - src/loaders/\* - loaders for the different init steps of the api server | ||||
| - src/routes/\* - express routes for everything we don't do via routing-controllers (shouldn't be much) | ||||
| - src/middlewares/\* - express middlewares (mainly auth r/n) | ||||
| - src/errors/* - our custom (http) errors | ||||
| - src/routes/\* - express routes for everything we don't do via routing-controllers (depreciated) | ||||
							
								
								
									
										17
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/app.ts
									
									
									
									
									
								
							| @@ -1,18 +1,15 @@ | ||||
| import "reflect-metadata"; | ||||
| import * as dotenvSafe from "dotenv-safe"; | ||||
| import { createExpressServer } from "routing-controllers"; | ||||
| import consola from "consola"; | ||||
| import loaders from "./loaders/index"; | ||||
| import "reflect-metadata"; | ||||
| import { createExpressServer } from "routing-controllers"; | ||||
| import authchecker from "./authchecker"; | ||||
| import { config } from './config'; | ||||
| import loaders from "./loaders/index"; | ||||
| import { ErrorHandler } from './middlewares/ErrorHandler'; | ||||
|  | ||||
| dotenvSafe.config(); | ||||
| const PORT = process.env.APP_PORT || 4010; | ||||
|  | ||||
| const app = createExpressServer({ | ||||
|   authorizationChecker: authchecker, | ||||
|   middlewares: [ErrorHandler], | ||||
|   development: process.env.NODE_ENV === "production", | ||||
|   development: config.development, | ||||
|   cors: true, | ||||
|   routePrefix: "/api", | ||||
|   controllers: [__dirname + "/controllers/*.ts"], | ||||
| @@ -20,9 +17,9 @@ const app = createExpressServer({ | ||||
|  | ||||
| async function main() { | ||||
|   await loaders(app); | ||||
|   app.listen(PORT, () => { | ||||
|   app.listen(config.internal_port, () => { | ||||
|     consola.success( | ||||
|       `⚡️[server]: Server is running at http://localhost:${PORT}` | ||||
|       `⚡️[server]: Server is running at http://localhost:${config.internal_port}` | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| import * as jwt from "jsonwebtoken"; | ||||
| import { Action, HttpError } from "routing-controllers"; | ||||
| // ----------- | ||||
| const sampletoken = jwt.sign({ | ||||
|     "permissions": { | ||||
|         "TRACKS": ["read", "update", "delete", "add"] | ||||
|         // "TRACKS": [] | ||||
|     } | ||||
| }, process.env.JWT_SECRET || "secretjwtsecret") | ||||
| console.log(`sampletoken: ${sampletoken}`); | ||||
| import { Action } from "routing-controllers"; | ||||
| import { getConnectionManager } from 'typeorm'; | ||||
| import { config } from './config'; | ||||
| import { IllegalJWTError, NoPermissionError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError'; | ||||
| import { User } from './models/entities/User'; | ||||
| // ----------- | ||||
| const authchecker = async (action: Action, permissions: string | string[]) => { | ||||
|     let required_permissions = undefined | ||||
| @@ -20,9 +16,14 @@ const authchecker = async (action: Action, permissions: string | string[]) => { | ||||
|     const provided_token = action.request.query["auth"]; | ||||
|     let jwtPayload = undefined | ||||
|     try { | ||||
|         jwtPayload = <any>jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); | ||||
|         jwtPayload = <any>jwt.verify(provided_token, config.jwt_secret); | ||||
|     } catch (error) { | ||||
|         throw new HttpError(401, "jwt_illegal") | ||||
|         console.log(error); | ||||
|         throw new IllegalJWTError() | ||||
|     } | ||||
|     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 = {} | ||||
| @@ -34,15 +35,15 @@ const authchecker = async (action: Action, permissions: string | string[]) => { | ||||
|             if (actual_accesslevel_for_permission.includes(permission_access_level)) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 throw new HttpError(403, "no") | ||||
|                 throw new NoPermissionError() | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         throw new HttpError(403, "no") | ||||
|         throw new NoPermissionError() | ||||
|     } | ||||
|     //  | ||||
|     try { | ||||
|         jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); | ||||
|         jwt.verify(provided_token, config.jwt_secret); | ||||
|         return true | ||||
|     } catch (error) { | ||||
|         return false | ||||
|   | ||||
							
								
								
									
										7
									
								
								src/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import * as dotenvSafe from "dotenv-safe"; | ||||
| dotenvSafe.config(); | ||||
| export const config = { | ||||
|     internal_port: process.env.APP_PORT || 4010, | ||||
|     development: process.env.NODE_ENV === "production", | ||||
|     jwt_secret: process.env.JWT_SECRET || "secretjwtsecret" | ||||
| } | ||||
							
								
								
									
										68
									
								
								src/controllers/AuthController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/controllers/AuthController.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| 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'; | ||||
| import { CreateAuth } from '../models/actions/CreateAuth'; | ||||
| import { HandleLogout } from '../models/actions/HandleLogout'; | ||||
| import { RefreshAuth } from '../models/actions/RefreshAuth'; | ||||
| import { Auth } from '../models/responses/ResponseAuth'; | ||||
| import { Logout } from '../models/responses/ResponseLogout'; | ||||
|  | ||||
| @JsonController('/auth') | ||||
| export class AuthController { | ||||
| 	constructor() { | ||||
| 	} | ||||
|  | ||||
| 	@Post("/login") | ||||
| 	@ResponseSchema(Auth) | ||||
| 	@ResponseSchema(InvalidCredentialsError) | ||||
| 	@ResponseSchema(UserNotFoundError) | ||||
| 	@ResponseSchema(UsernameOrEmailNeededError) | ||||
| 	@ResponseSchema(PasswordNeededError) | ||||
| 	@ResponseSchema(InvalidCredentialsError) | ||||
| 	@OpenAPI({ description: 'Create a new access token object' }) | ||||
| 	async login(@Body({ validate: true }) createAuth: CreateAuth) { | ||||
| 		let auth; | ||||
| 		try { | ||||
| 			auth = await createAuth.toAuth(); | ||||
| 		} catch (error) { | ||||
| 			return error; | ||||
| 		} | ||||
| 		return auth | ||||
| 	} | ||||
|  | ||||
| 	@Post("/logout") | ||||
| 	@ResponseSchema(Logout) | ||||
| 	@ResponseSchema(InvalidCredentialsError) | ||||
| 	@ResponseSchema(UserNotFoundError) | ||||
| 	@ResponseSchema(UsernameOrEmailNeededError) | ||||
| 	@ResponseSchema(PasswordNeededError) | ||||
| 	@ResponseSchema(InvalidCredentialsError) | ||||
| 	@OpenAPI({ description: 'Create a new access token object' }) | ||||
| 	async logout(@Body({ validate: true }) handleLogout: HandleLogout) { | ||||
| 		let logout; | ||||
| 		try { | ||||
| 			logout = await handleLogout.logout() | ||||
| 		} catch (error) { | ||||
| 			return error; | ||||
| 		} | ||||
| 		return logout | ||||
| 	} | ||||
|  | ||||
| 	@Post("/refresh") | ||||
| 	@ResponseSchema(Auth) | ||||
| 	@ResponseSchema(JwtNotProvidedError) | ||||
| 	@ResponseSchema(IllegalJWTError) | ||||
| 	@ResponseSchema(UserNotFoundError) | ||||
| 	@ResponseSchema(RefreshTokenCountInvalidError) | ||||
| 	@OpenAPI({ description: 'refresh a access token' }) | ||||
| 	async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth) { | ||||
| 		let auth; | ||||
| 		try { | ||||
| 			auth = await refreshAuth.toAuth(); | ||||
| 		} catch (error) { | ||||
| 			return error; | ||||
| 		} | ||||
| 		return auth | ||||
| 	} | ||||
| } | ||||
| @@ -2,8 +2,9 @@ import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, Query | ||||
| import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { RunnerGroupNeededError, RunnerGroupNotFoundError, RunnerIdsNotMatchingError, RunnerNotFoundError, RunnerOnlyOneGroupAllowedError } from '../errors/RunnerErrors'; | ||||
| import { CreateRunner } from '../models/creation/CreateRunner'; | ||||
| import { RunnerGroupNeededError, RunnerIdsNotMatchingError, RunnerNotFoundError } from '../errors/RunnerErrors'; | ||||
| import { RunnerGroupNotFoundError } from '../errors/RunnerGroupErrors'; | ||||
| import { CreateRunner } from '../models/actions/CreateRunner'; | ||||
| import { Runner } from '../models/entities/Runner'; | ||||
| import { ResponseRunner } from '../models/responses/ResponseRunner'; | ||||
|  | ||||
| @@ -43,7 +44,6 @@ export class RunnerController { | ||||
|  | ||||
| 	@Post() | ||||
| 	@ResponseSchema(ResponseRunner) | ||||
| 	@ResponseSchema(RunnerOnlyOneGroupAllowedError) | ||||
| 	@ResponseSchema(RunnerGroupNeededError) | ||||
| 	@ResponseSchema(RunnerGroupNotFoundError) | ||||
| 	@OpenAPI({ description: 'Create a new runner object (id will be generated automagicly).' }) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { RunnerOrganisationHasRunnersError, RunnerOrganisationHasTeamsError, RunnerOrganisationIdsNotMatchingError, RunnerOrganisationNotFoundError } from '../errors/RunnerOrganisationErrors'; | ||||
| import { CreateRunnerOrganisation } from '../models/creation/CreateRunnerOrganisation'; | ||||
| import { CreateRunnerOrganisation } from '../models/actions/CreateRunnerOrganisation'; | ||||
| import { RunnerOrganisation } from '../models/entities/RunnerOrganisation'; | ||||
| import { ResponseRunnerOrganisation } from '../models/responses/ResponseRunnerOrganisation'; | ||||
| import { RunnerController } from './RunnerController'; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { RunnerTeamHasRunnersError, RunnerTeamIdsNotMatchingError, RunnerTeamNotFoundError } from '../errors/RunnerTeamErrors'; | ||||
| import { CreateRunnerTeam } from '../models/creation/CreateRunnerTeam'; | ||||
| import { CreateRunnerTeam } from '../models/actions/CreateRunnerTeam'; | ||||
| import { RunnerTeam } from '../models/entities/RunnerTeam'; | ||||
| import { ResponseRunnerTeam } from '../models/responses/ResponseRunnerTeam'; | ||||
| import { RunnerController } from './RunnerController'; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { TrackIdsNotMatchingError, TrackNotFoundError } from "../errors/TrackErrors"; | ||||
| import { CreateTrack } from '../models/creation/CreateTrack'; | ||||
| import { CreateTrack } from '../models/actions/CreateTrack'; | ||||
| import { Track } from '../models/entities/Track'; | ||||
| import { ResponseTrack } from '../models/responses/ResponseTrack'; | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,9 @@ import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put } from | ||||
| import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { UserGroupNotFoundError, UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors'; | ||||
| import { CreateUser } from '../models/creation/CreateUser'; | ||||
| import { UserIdsNotMatchingError, UserNotFoundError } from '../errors/UserErrors'; | ||||
| import { UserGroupNotFoundError } from '../errors/UserGroupErrors'; | ||||
| import { CreateUser } from '../models/actions/CreateUser'; | ||||
| import { User } from '../models/entities/User'; | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; | ||||
| import { getConnectionManager, Repository } from 'typeorm'; | ||||
| import { EntityFromBody, EntityFromParam } from 'typeorm-routing-controllers-extensions'; | ||||
| import { UserGroupIdsNotMatchingError, UserGroupNotFoundError } from '../errors/UserGroupErrors'; | ||||
| import { CreateUserGroup } from '../models/creation/CreateUserGroup'; | ||||
| import { CreateUserGroup } from '../models/actions/CreateUserGroup'; | ||||
| import { UserGroup } from '../models/entities/UserGroup'; | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
| import { NotAcceptableError, NotFoundError } from 'routing-controllers'; | ||||
|  | ||||
| /** | ||||
|  * Error to throw, when to provided address doesn't belong to the accepted types. | ||||
|  */ | ||||
| export class AddressWrongTypeError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "AddressWrongTypeError" | ||||
| @@ -9,6 +12,9 @@ export class AddressWrongTypeError extends NotAcceptableError { | ||||
| 	message = "The address must be an existing adress's id. \n You provided a object of another type." | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw, when a non-existant address get's loaded. | ||||
|  */ | ||||
| export class AddressNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "AddressNotFoundError" | ||||
|   | ||||
							
								
								
									
										123
									
								
								src/errors/AuthError.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/errors/AuthError.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| 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. | ||||
|  */ | ||||
| export class IllegalJWTError extends UnauthorizedError { | ||||
| 	@IsString() | ||||
| 	name = "IllegalJWTError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "your provided jwt could not be parsed" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when user is nonexistant or refreshtoken is invalid. | ||||
|  */ | ||||
| export class UserNonexistantOrRefreshtokenInvalidError extends UnauthorizedError { | ||||
| 	@IsString() | ||||
| 	name = "UserNonexistantOrRefreshtokenInvalidError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "user is nonexistant or refreshtoken is invalid" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when provided credentials are invalid. | ||||
|  */ | ||||
| export class InvalidCredentialsError extends UnauthorizedError { | ||||
| 	@IsString() | ||||
| 	name = "InvalidCredentialsError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "your provided credentials are invalid" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a jwt does not have permission for this route/action. | ||||
|  */ | ||||
| export class NoPermissionError extends ForbiddenError { | ||||
| 	@IsString() | ||||
| 	name = "NoPermissionError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "your provided jwt does not have permission for this route/ action" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when no username and no email is set. | ||||
|  */ | ||||
| export class UsernameOrEmailNeededError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "UsernameOrEmailNeededError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "Auth needs to have email or username set! \n You provided neither." | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when no user could be found mating the provided credential. | ||||
|  */ | ||||
| export class UserNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "UserNotFoundError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "no user could be found for provided credential" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 token was provided" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 refresh token count was invalid" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when refresh token count was invalid | ||||
|  */ | ||||
| export class RefreshTokenCountInvalidError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "RefreshTokenCountInvalidError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "refresh token count was invalid" | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
| import { NotAcceptableError, NotFoundError } from 'routing-controllers'; | ||||
|  | ||||
| /** | ||||
|  * Error to throw, when a provided groupContact doesn't belong to the accepted types. | ||||
|  */ | ||||
| export class GroupContactWrongTypeError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "GroupContactWrongTypeError" | ||||
| @@ -9,6 +12,9 @@ export class GroupContactWrongTypeError extends NotAcceptableError { | ||||
| 	message = "The groupContact must be an existing groupContact's id. \n You provided a object of another type." | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw, when a non-existant groupContact get's loaded. | ||||
|  */ | ||||
| export class GroupContactNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "GroupContactNotFoundError" | ||||
|   | ||||
| @@ -26,27 +26,13 @@ export class RunnerIdsNotMatchingError extends NotAcceptableError { | ||||
| 	message = "The id's don't match!! \n And if you wanted to change a runner's id: This isn't allowed" | ||||
| } | ||||
|  | ||||
| export class RunnerOnlyOneGroupAllowedError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "RunnerOnlyOneGroupAllowedError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "Runner's can only be part of one group (team or organisiation)! \n You provided an id for both." | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a runner is missing his group association. | ||||
|  */ | ||||
| export class RunnerGroupNeededError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "RunnerGroupNeededError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "Runner's need to be part of one group (team or organisiation)! \n You provided neither." | ||||
| } | ||||
|  | ||||
|  | ||||
| export class RunnerGroupNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "RunnerGroupNotFoundError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "The group you provided couldn't be located in the system. \n Please check your request." | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/errors/RunnerGroupErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/errors/RunnerGroupErrors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
| 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() | ||||
| 	name = "RunnerGroupNotFoundError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "RunnerGroup not found!" | ||||
| } | ||||
| @@ -50,6 +50,9 @@ export class RunnerOrganisationHasTeamsError extends NotAcceptableError { | ||||
| 	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." | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw, when a provided runnerOrganisation doesn't belong to the accepted types. | ||||
|  */ | ||||
| export class RunnerOrganisationWrongTypeError extends NotAcceptableError { | ||||
| 	@IsString() | ||||
| 	name = "RunnerOrganisationWrongTypeError" | ||||
|   | ||||
| @@ -1,16 +1,6 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
| import { NotAcceptableError, NotFoundError } from 'routing-controllers'; | ||||
|  | ||||
| /** | ||||
|  * Error to throw when a usergroup couldn't be found. | ||||
|  */ | ||||
| export class UserGroupNotFoundError extends NotFoundError { | ||||
| 	@IsString() | ||||
| 	name = "UserGroupNotFoundError" | ||||
|  | ||||
| 	@IsString() | ||||
| 	message = "User Group not found!" | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error to throw when no username or email is set | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| import { createConnection } from "typeorm"; | ||||
|  | ||||
| /** | ||||
|  * Loader for the database that creates the database connection and initializes the database tabels. | ||||
|  */ | ||||
| export default async () => { | ||||
|     const connection = await createConnection(); | ||||
|     connection.synchronize(); | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { Application } from "express"; | ||||
| import bodyParser from 'body-parser'; | ||||
| import cors from 'cors'; | ||||
|  | ||||
| /** | ||||
|  * Loader for express related configurations. | ||||
|  * Currently only enables the proxy trust. | ||||
|  */ | ||||
| export default async (app: Application) => { | ||||
| 	app.enable('trust proxy'); | ||||
| 	return app; | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| import { Application } from "express"; | ||||
| import databaseLoader from "./database"; | ||||
| import expressLoader from "./express"; | ||||
| import openapiLoader from "./openapi"; | ||||
| import databaseLoader from "./database"; | ||||
| import { Application } from "express"; | ||||
|  | ||||
| /** | ||||
|  * Index Loader that executes the other loaders in the right order. | ||||
|  */ | ||||
| export default async (app: Application) => { | ||||
|     await databaseLoader(); | ||||
|     await openapiLoader(app); | ||||
|   | ||||
| @@ -1,14 +1,19 @@ | ||||
| import { validationMetadatasToSchemas } from "class-validator-jsonschema"; | ||||
| import { Application } from "express"; | ||||
| import * as swaggerUiExpress from "swagger-ui-express"; | ||||
| import { getMetadataArgsStorage } from "routing-controllers"; | ||||
| import { routingControllersToSpec } from "routing-controllers-openapi"; | ||||
| import { validationMetadatasToSchemas } from "class-validator-jsonschema"; | ||||
| import * as swaggerUiExpress from "swagger-ui-express"; | ||||
|  | ||||
| /** | ||||
|  * Loader for everything openapi related - from creating the schema to serving it via a static route. | ||||
|  */ | ||||
| export default async (app: Application) => { | ||||
|   const storage = getMetadataArgsStorage(); | ||||
|   const schemas = validationMetadatasToSchemas({ | ||||
|     refPointerPrefix: "#/components/schemas/", | ||||
|   }); | ||||
|  | ||||
|   //Spec creation based on the previously created schemas | ||||
|   const spec = routingControllersToSpec( | ||||
|     storage, | ||||
|     { | ||||
| @@ -17,6 +22,13 @@ export default async (app: Application) => { | ||||
|     { | ||||
|       components: { | ||||
|         schemas, | ||||
|         "securitySchemes": { | ||||
|           "AuthToken": { | ||||
|             "type": "http", | ||||
|             "scheme": "bearer", | ||||
|             "bearerFormat": "JWT" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       info: { | ||||
|         description: "The the backend API for the LfK! runner system.", | ||||
| @@ -25,6 +37,8 @@ export default async (app: Application) => { | ||||
|       }, | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   //Options for swaggerUiExpress | ||||
|   const options = { | ||||
|     explorer: true, | ||||
|   }; | ||||
|   | ||||
| @@ -1,20 +1,14 @@ | ||||
| import { | ||||
|     Middleware, | ||||
|     ExpressErrorMiddlewareInterface | ||||
| } from "routing-controllers"; | ||||
| import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers"; | ||||
|  | ||||
| /** | ||||
|  * Our Error handling middlware that returns our custom httperrors to the user | ||||
|  */ | ||||
| @Middleware({ type: "after" }) | ||||
| export class ErrorHandler implements ExpressErrorMiddlewareInterface { | ||||
|     public error( | ||||
|         error: any, | ||||
|         request: any, | ||||
|         response: any, | ||||
|         next: (err: any) => any | ||||
|     ) { | ||||
|     public error(error: any, request: any, response: any, next: (err: any) => any) { | ||||
|         if (response.headersSent) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         response.json(error); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { Request, Response, NextFunction } from "express"; | ||||
| // import bodyParser from 'body-parser'; | ||||
| // import cors from 'cors'; | ||||
| import * as jwt from "jsonwebtoken"; | ||||
|  | ||||
| export default (req: Request, res: Response, next: NextFunction) => { | ||||
|   const token = <string>req.headers["auth"]; | ||||
|   try { | ||||
|     const jwtPayload = <any>jwt.verify(token, "secretjwtsecret"); | ||||
|     // const jwtPayload = <any>jwt.verify(token, process.env.JWT_SECRET); | ||||
|     res.locals.jwtPayload = jwtPayload; | ||||
|   } catch (error) { | ||||
|     console.log(error); | ||||
|     return res.status(401).send(); | ||||
|   } | ||||
|   next(); | ||||
| }; | ||||
							
								
								
									
										58
									
								
								src/models/actions/CreateAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/models/actions/CreateAuth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import * as argon2 from "argon2"; | ||||
| 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 { User } from '../entities/User'; | ||||
| import { Auth } from '../responses/ResponseAuth'; | ||||
|  | ||||
| export class CreateAuth { | ||||
|     @IsOptional() | ||||
|     @IsString() | ||||
|     username?: string; | ||||
|     @IsString() | ||||
|     password: string; | ||||
|     @IsOptional() | ||||
|     @IsEmail() | ||||
|     @IsString() | ||||
|     email?: string; | ||||
|  | ||||
|     public async toAuth(): Promise<Auth> { | ||||
|         let newAuth: Auth = new Auth(); | ||||
|  | ||||
|         if (this.email === undefined && this.username === undefined) { | ||||
|             throw new UsernameOrEmailNeededError(); | ||||
|         } | ||||
|         if (!this.password) { | ||||
|             throw new PasswordNeededError() | ||||
|         } | ||||
|         const found_users = await getConnectionManager().get().getRepository(User).find({ 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 | ||||
|                 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 | ||||
|                 //  | ||||
|                 const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000 | ||||
|                 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { IsInt } from 'class-validator'; | ||||
| import { getConnectionManager } from 'typeorm'; | ||||
| import { RunnerGroupNotFoundError } from '../../errors/RunnerErrors'; | ||||
| import { RunnerGroupNotFoundError } from '../../errors/RunnerGroupErrors'; | ||||
| import { RunnerOrganisationWrongTypeError } from '../../errors/RunnerOrganisationErrors'; | ||||
| import { RunnerTeamNeedsParentError } from '../../errors/RunnerTeamErrors'; | ||||
| import { Runner } from '../entities/Runner'; | ||||
| @@ -2,7 +2,8 @@ import * as argon2 from "argon2"; | ||||
| import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; | ||||
| import { getConnectionManager } from 'typeorm'; | ||||
| import * as uuid from 'uuid'; | ||||
| import { UserGroupNotFoundError, UsernameOrEmailNeededError } from '../../errors/UserErrors'; | ||||
| import { UsernameOrEmailNeededError } from '../../errors/UserErrors'; | ||||
| import { UserGroupNotFoundError } from '../../errors/UserGroupErrors'; | ||||
| import { User } from '../entities/User'; | ||||
| import { UserGroup } from '../entities/UserGroup'; | ||||
| 
 | ||||
							
								
								
									
										36
									
								
								src/models/actions/HandleLogout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/models/actions/HandleLogout.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| 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 { User } from '../entities/User'; | ||||
| import { Logout } from '../responses/ResponseLogout'; | ||||
|  | ||||
| export class HandleLogout { | ||||
|     @IsString() | ||||
|     token: string; | ||||
|  | ||||
|     public async logout(): Promise<Logout> { | ||||
|         let logout: Logout = new Logout(); | ||||
|         if (!this.token || this.token === undefined) { | ||||
|             throw new JwtNotProvidedError() | ||||
|         } | ||||
|         let decoded; | ||||
|         try { | ||||
|             decoded = jsonwebtoken.verify(this.token, config.jwt_secret) | ||||
|         } catch (error) { | ||||
|             throw new IllegalJWTError() | ||||
|         } | ||||
|         logout.timestamp = Math.floor(Date.now() / 1000) | ||||
|         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"]) { | ||||
|             throw new RefreshTokenCountInvalidError() | ||||
|         } | ||||
|         found_user.refreshTokenCount++; | ||||
|         await getConnectionManager().get().getRepository(User).update({ id: found_user.id }, found_user) | ||||
|         return logout; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								src/models/actions/RefreshAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/models/actions/RefreshAuth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| 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 { User } from '../entities/User'; | ||||
| import { Auth } from '../responses/ResponseAuth'; | ||||
|  | ||||
| export class RefreshAuth { | ||||
|     @IsString() | ||||
|     token: string; | ||||
|  | ||||
|     public async toAuth(): Promise<Auth> { | ||||
|         let newAuth: Auth = new Auth(); | ||||
|         if (!this.token || this.token === undefined) { | ||||
|             throw new JwtNotProvidedError() | ||||
|         } | ||||
|         let decoded | ||||
|         try { | ||||
|             decoded = jsonwebtoken.verify(this.token, config.jwt_secret) | ||||
|         } catch (error) { | ||||
|             throw new IllegalJWTError() | ||||
|         } | ||||
|         const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] }); | ||||
|         if (!found_user) { | ||||
|             throw new UserNotFoundError() | ||||
|         } | ||||
|         if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) { | ||||
|             throw new RefreshTokenCountInvalidError() | ||||
|         } | ||||
|         delete found_user.password; | ||||
|         const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60 | ||||
|         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 | ||||
|         //  | ||||
|         const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000 | ||||
|         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; | ||||
|     } | ||||
| } | ||||
| @@ -19,14 +19,14 @@ export class User { | ||||
|   /** | ||||
|   * uuid | ||||
|   */ | ||||
|   @Column() | ||||
|   @Column({ unique: true }) | ||||
|   @IsUUID(4) | ||||
|   uuid: string; | ||||
|  | ||||
|   /** | ||||
|   * user email | ||||
|   */ | ||||
|   @Column({ nullable: true }) | ||||
|   @Column({ nullable: true, unique: true }) | ||||
|   @IsEmail() | ||||
|   email?: string; | ||||
|  | ||||
| @@ -41,7 +41,7 @@ export class User { | ||||
|   /** | ||||
|   * username | ||||
|   */ | ||||
|   @Column({ nullable: true }) | ||||
|   @Column({ nullable: true, unique: true }) | ||||
|   @IsString() | ||||
|   username?: string; | ||||
|  | ||||
| @@ -109,7 +109,7 @@ export class User { | ||||
|   /** | ||||
|   * profilepic | ||||
|   */ | ||||
|   @Column({ nullable: true }) | ||||
|   @Column({ nullable: true, unique: true }) | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   profilePic?: string; | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/models/responses/ResponseAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/models/responses/ResponseAuth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { IsInt, IsString } from 'class-validator'; | ||||
|  | ||||
| /** | ||||
|  * Defines a auth object | ||||
| */ | ||||
| export class Auth { | ||||
|   /** | ||||
|   * access_token - JWT shortterm access token | ||||
|   */ | ||||
|   @IsString() | ||||
|   access_token: string; | ||||
|   /** | ||||
|   * refresh_token - longterm refresh token (used for requesting new access tokens) | ||||
|   */ | ||||
|   @IsString() | ||||
|   refresh_token: string; | ||||
|   /** | ||||
|   * access_token_expires_at - unix timestamp of access token expiry | ||||
|   */ | ||||
|   @IsInt() | ||||
|   access_token_expires_at: number; | ||||
|   /** | ||||
|   * refresh_token_expires_at - unix timestamp of access token expiry | ||||
|   */ | ||||
|   @IsInt() | ||||
|   refresh_token_expires_at: number; | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/models/responses/ResponseLogout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/models/responses/ResponseLogout.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { IsString } from 'class-validator'; | ||||
|  | ||||
| /** | ||||
|  * Defines a Logout object | ||||
| */ | ||||
| export class Logout { | ||||
|   /** | ||||
|   * timestamp of logout | ||||
|   */ | ||||
|   @IsString() | ||||
|   timestamp: number; | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| import { Router } from "express"; | ||||
| import jwtauth from "../../middlewares/jwtauth"; | ||||
|  | ||||
| const router = Router(); | ||||
|  | ||||
| router.use("*", jwtauth, async (req, res, next) => { | ||||
|   return res.send("ok"); | ||||
| }); | ||||
|  | ||||
| export default router; | ||||
		Reference in New Issue
	
	Block a user