import { Request } from "express"; import * as jwt from "jsonwebtoken"; import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getConnectionManager, Repository } from 'typeorm'; import { config } from '../config'; import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError'; import { MailSendingError } from '../errors/MailErrors'; import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors'; import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors'; import { ScanStationNotFoundError } from '../errors/ScanStationErrors'; import { JwtCreator } from '../jwtcreator'; import { Mailer } from '../mailer'; import ScanAuth from '../middlewares/ScanAuth'; import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner'; import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner'; import { Runner } from '../models/entities/Runner'; import { RunnerGroup } from '../models/entities/RunnerGroup'; import { RunnerOrganization } from '../models/entities/RunnerOrganization'; import { ScanStation } from '../models/entities/ScanStation'; import { ResponseEmpty } from '../models/responses/ResponseEmpty'; import { ResponseScanStation } from '../models/responses/ResponseScanStation'; import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation'; import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner'; import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan'; import { DonationController } from './DonationController'; import { RunnerCardController } from './RunnerCardController'; import { ScanController } from './ScanController'; @JsonController() export class RunnerSelfServiceController { private runnerRepository: Repository; private orgRepository: Repository; private stationRepository: Repository; /** * Gets the repository of this controller's model/entity. */ constructor() { this.runnerRepository = getConnectionManager().get().getRepository(Runner); this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization); this.stationRepository = getConnectionManager().get().getRepository(ScanStation); } @Get('/runners/me/:jwt') @ResponseSchema(ResponseSelfServiceRunner) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @OnUndefined(RunnerNotFoundError) @OpenAPI({ description: 'Lists all information about yourself.
Please provide your runner jwt(that code we gave you during registration) for auth.
If you lost your jwt/personalized link please use the forgot endpoint.' }) async get(@Param('jwt') token: string) { return (new ResponseSelfServiceRunner(await this.getRunner(token))); } @Delete('/runners/me/:jwt') @ResponseSchema(ResponseSelfServiceRunner) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @OnUndefined(RunnerNotFoundError) @OpenAPI({ description: 'Deletes all information about yourself.
Please provide your runner jwt(that code we gave you during registration) for auth.
If you lost your jwt/personalized link please use the forgot endpoint.' }) async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) { const responseRunner = await this.getRunner(token); let runner = await this.runnerRepository.findOne({ id: responseRunner.id }); if (!runner) { return null; } if (!runner) { throw new RunnerNotFoundError(); } const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations; if (runnerDonations.length > 0 && !force) { throw new RunnerHasDistanceDonationsError(); } const donationController = new DonationController(); for (let donation of runnerDonations) { await donationController.remove(donation.id, force); } const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards; const cardController = new RunnerCardController; for (let card of runnerCards) { await cardController.remove(card.id, force); } const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans; const scanController = new ScanController; for (let scan of runnerScans) { await scanController.remove(scan.id, force); } await this.runnerRepository.delete(runner); return new ResponseSelfServiceRunner(responseRunner); } @Get('/runners/me/:jwt/scans') @ResponseSchema(ResponseSelfServiceScan, { isArray: true }) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @OnUndefined(RunnerNotFoundError) @OpenAPI({ description: 'Lists all your (runner) scans.
Please provide your runner jwt(that code we gave you during registration) for auth.
If you lost your jwt/personalized link please contact support.' }) async getScans(@Param('jwt') token: string) { const scans = (await this.getRunner(token)).scans; let responseScans = new Array() for (let scan of scans) { responseScans.push(new ResponseSelfServiceScan(scan)); } return responseScans; } @Get('/stations/me') @UseBefore(ScanAuth) @ResponseSchema(ResponseScanStation) @ResponseSchema(ScanStationNotFoundError, { statusCode: 404 }) @OnUndefined(ScanStationNotFoundError) @OpenAPI({ description: 'Lists basic information about the station whose token got provided.
This includes it\'s associated track.', security: [{ "StationApiToken": [] }] }) async getStationMe(@Req() req: Request) { let scan = await this.stationRepository.findOne({ id: parseInt(req.headers["station_id"].toString()) }, { relations: ['track'] }) if (!scan) { throw new ScanStationNotFoundError(); } return scan.toResponse(); } @Post('/runners/login') @ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @OnUndefined(ResponseEmpty) @OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice token/link to be sent to your mail address (rate limited to one mail every 24hrs).' }) async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") { if (!mail) { throw new RunnerNotFoundError(); } const runner = await this.runnerRepository.findOne({ email: mail }); if (!runner) { throw new RunnerNotFoundError(); } if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 60 * 60 * 24)) { throw new RunnerSelfserviceTimeoutError(); } const token = JwtCreator.createSelfService(runner); try { await Mailer.sendSelfserviceForgottenMail(runner.email, token, locale) } catch (error) { throw new MailSendingError(); } runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000); await this.runnerRepository.save(runner); return { token }; } @Post('/runners/register') @ResponseSchema(ResponseSelfServiceRunner) @ResponseSchema(RunnerEmailNeededError, { statusCode: 406 }) @OpenAPI({ description: 'Create a new selfservice runner in the citizen org.
This endpoint shoud be used to allow "everyday citizen" to register themselves.
You have to provide a mail address, b/c the future we\'ll implement email verification.' }) async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") { let runner = await createRunner.toEntity(); if (await this.getRunnerExistsByMail(runner.email)) { throw new BadRequestError("E-Mail already registered") } runner = await this.runnerRepository.save(runner); let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] })); response.token = JwtCreator.createSelfService(runner); try { await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale) } catch (error) { throw new MailSendingError(); } return response; } @Post('/runners/register/:token') @ResponseSchema(ResponseSelfServiceRunner) @ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 }) @OpenAPI({ description: 'Create a new selfservice runner in a provided org.
The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' }) async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") { const org = await this.getOrgansisation(token); let runner = await createRunner.toEntity(org); if (await this.getRunnerExistsByMail(runner.email)) { throw new BadRequestError("E-Mail already registered") } runner = await this.runnerRepository.save(runner); let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] })); response.token = JwtCreator.createSelfService(runner); try { await Mailer.sendSelfserviceWelcomeMail(runner.email, response.token, locale) } catch (error) { throw new MailSendingError(); } return response; } @Get('/organizations/selfservice/:token') @ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false }) @ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 }) @OpenAPI({ description: 'Get the basic info and teams for a org.' }) async getSelfserviceOrg(@Param('token') token: string) { const orgid = (await this.getOrgansisation(token)).id; const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] }) return new ResponseSelfServiceOrganisation(org); } /** * Get's a runner by a provided jwt token. * @param token The runner jwt provided by the runner to identitfy themselves. */ private async getRunner(token: string): Promise { if (token == "") { throw new JwtNotProvidedError(); } let jwtPayload = undefined try { jwtPayload = jwt.verify(token, config.jwt_secret); } catch (error) { throw new InvalidCredentialsError(); } const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }); if (!runner) { throw new RunnerNotFoundError() } return runner; } /** * Get's a runner org by a provided registration api key. * @param token The organization's registration api token. */ private async getOrgansisation(token: string): Promise { token = Buffer.from(token, 'base64').toString('utf8'); const organization = await this.orgRepository.findOne({ key: token }); if (!organization) { throw new RunnerOrganizationNotFoundError; } return organization; } /** * Checks if a runner already exists * @param email The runner's email address * @returns Boolean (true if exists, false if not) */ private async getRunnerExistsByMail(email: string): Promise { const runner = await this.runnerRepository.findOne({ email }); return runner != undefined } }