diff --git a/README.md b/README.md index 077f888..f974523 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 9e15de1..fd3dc8e 100644 --- a/src/app.ts +++ b/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}` ); }); } diff --git a/src/authchecker.ts b/src/authchecker.ts index 9b62dfa..869e0da 100644 --- a/src/authchecker.ts +++ b/src/authchecker.ts @@ -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 = jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); + jwtPayload = 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 diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..5f54ce3 --- /dev/null +++ b/src/config.ts @@ -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" +} \ No newline at end of file diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts new file mode 100644 index 0000000..00380cb --- /dev/null +++ b/src/controllers/AuthController.ts @@ -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 + } +} diff --git a/src/controllers/RunnerController.ts b/src/controllers/RunnerController.ts index 6508f67..acee2eb 100644 --- a/src/controllers/RunnerController.ts +++ b/src/controllers/RunnerController.ts @@ -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).' }) diff --git a/src/controllers/RunnerOrganisationController.ts b/src/controllers/RunnerOrganisationController.ts index 3302913..1a80809 100644 --- a/src/controllers/RunnerOrganisationController.ts +++ b/src/controllers/RunnerOrganisationController.ts @@ -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'; diff --git a/src/controllers/RunnerTeamController.ts b/src/controllers/RunnerTeamController.ts index 1b7627d..7b0a841 100644 --- a/src/controllers/RunnerTeamController.ts +++ b/src/controllers/RunnerTeamController.ts @@ -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'; diff --git a/src/controllers/TrackController.ts b/src/controllers/TrackController.ts index 02370fc..149b837 100644 --- a/src/controllers/TrackController.ts +++ b/src/controllers/TrackController.ts @@ -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'; diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 01e743e..f833f54 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -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'; diff --git a/src/controllers/UserGroupController.ts b/src/controllers/UserGroupController.ts index c937f36..70e3c5a 100644 --- a/src/controllers/UserGroupController.ts +++ b/src/controllers/UserGroupController.ts @@ -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'; diff --git a/src/errors/AddressErrors.ts b/src/errors/AddressErrors.ts index c7ae8af..18cf624 100644 --- a/src/errors/AddressErrors.ts +++ b/src/errors/AddressErrors.ts @@ -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" diff --git a/src/errors/AuthError.ts b/src/errors/AuthError.ts new file mode 100644 index 0000000..7687371 --- /dev/null +++ b/src/errors/AuthError.ts @@ -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" +} \ No newline at end of file diff --git a/src/errors/GroupContactErrors.ts b/src/errors/GroupContactErrors.ts index c5bf9c7..7dc19e8 100644 --- a/src/errors/GroupContactErrors.ts +++ b/src/errors/GroupContactErrors.ts @@ -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" diff --git a/src/errors/RunnerErrors.ts b/src/errors/RunnerErrors.ts index 8250d17..06dc78f 100644 --- a/src/errors/RunnerErrors.ts +++ b/src/errors/RunnerErrors.ts @@ -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." } \ No newline at end of file diff --git a/src/errors/RunnerGroupErrors.ts b/src/errors/RunnerGroupErrors.ts new file mode 100644 index 0000000..ed9624c --- /dev/null +++ b/src/errors/RunnerGroupErrors.ts @@ -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!" +} \ No newline at end of file diff --git a/src/errors/RunnerOrganisationErrors.ts b/src/errors/RunnerOrganisationErrors.ts index 65736b1..040befb 100644 --- a/src/errors/RunnerOrganisationErrors.ts +++ b/src/errors/RunnerOrganisationErrors.ts @@ -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" diff --git a/src/errors/UserErrors.ts b/src/errors/UserErrors.ts index 4c9e98b..cfceff1 100644 --- a/src/errors/UserErrors.ts +++ b/src/errors/UserErrors.ts @@ -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 diff --git a/src/loaders/database.ts b/src/loaders/database.ts index b8e9607..ee331ae 100644 --- a/src/loaders/database.ts +++ b/src/loaders/database.ts @@ -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(); diff --git a/src/loaders/express.ts b/src/loaders/express.ts index 787497a..32a28cd 100644 --- a/src/loaders/express.ts +++ b/src/loaders/express.ts @@ -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; diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 704ea47..802a732 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -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); diff --git a/src/loaders/openapi.ts b/src/loaders/openapi.ts index 9a0ff85..9c270ed 100644 --- a/src/loaders/openapi.ts +++ b/src/loaders/openapi.ts @@ -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, }; diff --git a/src/middlewares/ErrorHandler.ts b/src/middlewares/ErrorHandler.ts index 7f45073..40c1ff6 100644 --- a/src/middlewares/ErrorHandler.ts +++ b/src/middlewares/ErrorHandler.ts @@ -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); } } diff --git a/src/middlewares/jwtauth.ts b/src/middlewares/jwtauth.ts deleted file mode 100644 index 81c28c3..0000000 --- a/src/middlewares/jwtauth.ts +++ /dev/null @@ -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 = req.headers["auth"]; - try { - const jwtPayload = jwt.verify(token, "secretjwtsecret"); - // const jwtPayload = jwt.verify(token, process.env.JWT_SECRET); - res.locals.jwtPayload = jwtPayload; - } catch (error) { - console.log(error); - return res.status(401).send(); - } - next(); -}; diff --git a/src/models/creation/CreateAddress.ts b/src/models/actions/CreateAddress.ts similarity index 100% rename from src/models/creation/CreateAddress.ts rename to src/models/actions/CreateAddress.ts diff --git a/src/models/actions/CreateAuth.ts b/src/models/actions/CreateAuth.ts new file mode 100644 index 0000000..2832e19 --- /dev/null +++ b/src/models/actions/CreateAuth.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/src/models/creation/CreateGroupContact.ts b/src/models/actions/CreateGroupContact.ts similarity index 100% rename from src/models/creation/CreateGroupContact.ts rename to src/models/actions/CreateGroupContact.ts diff --git a/src/models/creation/CreateParticipant.ts b/src/models/actions/CreateParticipant.ts similarity index 100% rename from src/models/creation/CreateParticipant.ts rename to src/models/actions/CreateParticipant.ts diff --git a/src/models/creation/CreateRunner.ts b/src/models/actions/CreateRunner.ts similarity index 95% rename from src/models/creation/CreateRunner.ts rename to src/models/actions/CreateRunner.ts index 4e5f3e9..f3611ce 100644 --- a/src/models/creation/CreateRunner.ts +++ b/src/models/actions/CreateRunner.ts @@ -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'; diff --git a/src/models/creation/CreateRunnerGroup.ts b/src/models/actions/CreateRunnerGroup.ts similarity index 100% rename from src/models/creation/CreateRunnerGroup.ts rename to src/models/actions/CreateRunnerGroup.ts diff --git a/src/models/creation/CreateRunnerOrganisation.ts b/src/models/actions/CreateRunnerOrganisation.ts similarity index 100% rename from src/models/creation/CreateRunnerOrganisation.ts rename to src/models/actions/CreateRunnerOrganisation.ts diff --git a/src/models/creation/CreateRunnerTeam.ts b/src/models/actions/CreateRunnerTeam.ts similarity index 100% rename from src/models/creation/CreateRunnerTeam.ts rename to src/models/actions/CreateRunnerTeam.ts diff --git a/src/models/creation/CreateTrack.ts b/src/models/actions/CreateTrack.ts similarity index 100% rename from src/models/creation/CreateTrack.ts rename to src/models/actions/CreateTrack.ts diff --git a/src/models/creation/CreateUser.ts b/src/models/actions/CreateUser.ts similarity index 95% rename from src/models/creation/CreateUser.ts rename to src/models/actions/CreateUser.ts index fb7841a..ff6771a 100644 --- a/src/models/creation/CreateUser.ts +++ b/src/models/actions/CreateUser.ts @@ -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'; diff --git a/src/models/creation/CreateUserGroup.ts b/src/models/actions/CreateUserGroup.ts similarity index 100% rename from src/models/creation/CreateUserGroup.ts rename to src/models/actions/CreateUserGroup.ts diff --git a/src/models/actions/HandleLogout.ts b/src/models/actions/HandleLogout.ts new file mode 100644 index 0000000..5ecab38 --- /dev/null +++ b/src/models/actions/HandleLogout.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/src/models/actions/RefreshAuth.ts b/src/models/actions/RefreshAuth.ts new file mode 100644 index 0000000..55c124f --- /dev/null +++ b/src/models/actions/RefreshAuth.ts @@ -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 { + 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; + } +} \ No newline at end of file diff --git a/src/models/entities/User.ts b/src/models/entities/User.ts index 1d2065e..8e29e27 100644 --- a/src/models/entities/User.ts +++ b/src/models/entities/User.ts @@ -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; diff --git a/src/models/responses/ResponseAuth.ts b/src/models/responses/ResponseAuth.ts new file mode 100644 index 0000000..39a16ef --- /dev/null +++ b/src/models/responses/ResponseAuth.ts @@ -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; +} diff --git a/src/models/responses/ResponseLogout.ts b/src/models/responses/ResponseLogout.ts new file mode 100644 index 0000000..f0c109d --- /dev/null +++ b/src/models/responses/ResponseLogout.ts @@ -0,0 +1,12 @@ +import { IsString } from 'class-validator'; + +/** + * Defines a Logout object +*/ +export class Logout { + /** + * timestamp of logout + */ + @IsString() + timestamp: number; +} diff --git a/src/routes/v1/test.ts b/src/routes/v1/test.ts deleted file mode 100644 index 7f000f5..0000000 --- a/src/routes/v1/test.ts +++ /dev/null @@ -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;