Merge pull request 'feature/197-duplicate_runner_mail' (#198) from feature/197-duplicate_runner_mail into dev
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #198
This commit is contained in:
Nicolai Ort 2023-02-02 11:51:23 +00:00
commit 4b676bc853
Signed by: git.odit.services
GPG Key ID: 76E155504123332E
8 changed files with 172 additions and 132 deletions

View File

@ -1,108 +1,109 @@
{ {
"name": "@odit/lfk-backend", "name": "@odit/lfk-backend",
"version": "0.12.0", "version": "0.12.0",
"main": "src/app.ts", "main": "src/app.ts",
"repository": "https://git.odit.services/lfk/backend", "repository": "https://git.odit.services/lfk/backend",
"author": { "author": {
"name": "ODIT.Services", "name": "ODIT.Services",
"email": "info@odit.services", "email": "info@odit.services",
"url": "https://odit.services" "url": "https://odit.services"
}, },
"contributors": [ "contributors": [
{ {
"name": "Philipp Dormann", "name": "Philipp Dormann",
"email": "philipp@philippdormann.de", "email": "philipp@philippdormann.de",
"url": "https://philippdormann.de" "url": "https://philippdormann.de"
}, },
{ {
"name": "Nicolai Ort", "name": "Nicolai Ort",
"email": "info@nicolai-ort.com", "email": "info@nicolai-ort.com",
"url": "https://nicolai-ort.com" "url": "https://nicolai-ort.com"
} }
], ],
"license": "CC-BY-NC-SA-4.0", "license": "CC-BY-NC-SA-4.0",
"dependencies": { "dependencies": {
"@odit/class-validator-jsonschema": "2.1.1", "@odit/class-validator-jsonschema": "2.1.1",
"argon2": "0.27.1", "argon2": "0.27.1",
"axios": "0.21.1", "axios": "0.21.1",
"body-parser": "1.19.0", "body-parser": "1.19.0",
"check-password-strength": "2.0.2", "check-password-strength": "2.0.2",
"class-transformer": "0.3.1", "class-transformer": "0.3.1",
"class-validator": "0.13.1", "class-validator": "0.13.1",
"consola": "2.15.0", "consola": "2.15.0",
"cookie": "0.4.1", "cookie": "0.4.1",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.5",
"cors": "2.8.5", "cors": "2.8.5",
"csvtojson": "2.0.10", "csvtojson": "2.0.10",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"express": "4.17.1", "express": "4.17.1",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"libphonenumber-js": "1.9.9", "libphonenumber-js": "1.9.9",
"mysql": "2.18.1", "mysql": "2.18.1",
"pg": "8.5.1", "pg": "8.5.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"routing-controllers": "0.9.0-alpha.6", "routing-controllers": "0.9.0-alpha.6",
"routing-controllers-openapi": "2.2.0", "routing-controllers-openapi": "2.2.0",
"sqlite3": "5.0.0", "sqlite3": "5.0.0",
"typeorm": "0.2.30", "typeorm": "0.2.30",
"typeorm-routing-controllers-extensions": "0.2.0", "typeorm-routing-controllers-extensions": "0.2.0",
"typeorm-seeding": "1.6.1", "typeorm-seeding": "1.6.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"validator": "13.5.2" "validator": "13.5.2"
}, },
"devDependencies": { "devDependencies": {
"@odit/license-exporter": "0.0.9", "@faker-js/faker": "^7.6.0",
"@types/cors": "2.8.9", "@odit/license-exporter": "0.0.9",
"@types/csvtojson": "1.1.5", "@types/cors": "2.8.9",
"@types/express": "4.17.11", "@types/csvtojson": "1.1.5",
"@types/jest": "26.0.20", "@types/express": "4.17.11",
"@types/jsonwebtoken": "8.5.0", "@types/jest": "26.0.20",
"@types/node": "14.14.22", "@types/jsonwebtoken": "8.5.0",
"@types/uuid": "8.3.0", "@types/node": "14.14.22",
"cp-cli": "2.0.0", "@types/uuid": "8.3.0",
"jest": "26.6.3", "cp-cli": "2.0.0",
"nodemon": "2.0.7", "jest": "26.6.3",
"release-it": "14.2.2", "nodemon": "2.0.7",
"rimraf": "3.0.2", "release-it": "14.2.2",
"start-server-and-test": "1.11.7", "rimraf": "3.0.2",
"ts-jest": "26.5.0", "start-server-and-test": "1.11.7",
"ts-node": "9.1.1", "ts-jest": "26.5.0",
"typedoc": "0.20.19", "ts-node": "9.1.1",
"typescript": "4.1.3" "typedoc": "0.20.19",
}, "typescript": "4.1.3"
"scripts": { },
"dev": "nodemon src/app.ts", "scripts": {
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static", "dev": "nodemon src/app.ts",
"docs": "typedoc --out docs src", "build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"test": "jest", "docs": "typedoc --out docs src",
"test:watch": "jest --watchAll", "test": "jest",
"test:ci:generate_env": "ts-node scripts/create_testenv.ts", "test:watch": "jest --watchAll",
"test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test", "test:ci:generate_env": "ts-node scripts/create_testenv.ts",
"test:ci": "npm run test:ci:generate_env && npm run test:ci:run", "test:ci:run": "start-server-and-test dev http://localhost:4010/api/docs/openapi.json test",
"seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed", "test:ci": "npm run test:ci:generate_env && npm run test:ci:run",
"openapi:export": "ts-node scripts/openapi_export.ts", "seed": "ts-node ./node_modules/typeorm/cli.js schema:sync && ts-node ./node_modules/typeorm-seeding/dist/cli.js seed",
"licenses:export": "license-exporter --markdown", "openapi:export": "ts-node scripts/openapi_export.ts",
"release": "release-it --only-version" "licenses:export": "license-exporter --markdown",
}, "release": "release-it --only-version"
"release-it": { },
"git": { "release-it": {
"commit": true, "git": {
"requireCleanWorkingDir": false, "commit": true,
"commitMessage": "🚀Bumped version to v${version}", "requireCleanWorkingDir": false,
"requireBranch": "dev", "commitMessage": "🚀Bumped version to v${version}",
"push": true, "requireBranch": "dev",
"tag": true, "push": true,
"tagName": "v${version}", "tag": true,
"tagAnnotation": "v${version}" "tagName": "v${version}",
}, "tagAnnotation": "v${version}"
"npm": { },
"publish": false "npm": {
} "publish": false
}, }
"nodemonConfig": { },
"ignore": [ "nodemonConfig": {
"src/tests/*", "ignore": [
"docs/*" "src/tests/*",
] "docs/*"
} ]
} }
}

View File

@ -1,6 +1,6 @@
import { Request } from "express"; import { Request } from "express";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import { Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers'; import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm'; import { getConnectionManager, Repository } from 'typeorm';
import { config } from '../config'; import { config } from '../config';
@ -116,7 +116,7 @@ export class RunnerSelfServiceController {
return scan.toResponse(); return scan.toResponse();
} }
@Post('/runners/forgot') @Post('/runners/login')
@ResponseSchema(RunnerNotFoundError, { statusCode: 404 }) @ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
@OnUndefined(ResponseEmpty) @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).' }) @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).' })
@ -148,8 +148,11 @@ export class RunnerSelfServiceController {
@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' }) @OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> 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") { async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
let runner = await createRunner.toEntity(); let runner = await createRunner.toEntity();
if (await this.getRunnerExistsByMail(runner.email)) {
throw new BadRequestError("E-Mail already registered")
}
runner = await this.runnerRepository.save(runner); 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'] })); 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); response.token = JwtCreator.createSelfService(runner);
@ -170,6 +173,9 @@ export class RunnerSelfServiceController {
const org = await this.getOrgansisation(token); const org = await this.getOrgansisation(token);
let runner = await createRunner.toEntity(org); 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); 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'] })); 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'] }));
@ -225,4 +231,14 @@ export class RunnerSelfServiceController {
return organization; 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<boolean> {
const runner = await this.runnerRepository.findOne({ email });
return runner != undefined
}
} }

View File

@ -114,7 +114,7 @@ export class CreateUser {
newUser.groups = await this.getGroups(); newUser.groups = await this.getGroups();
newUser.enabled = this.enabled; newUser.enabled = this.enabled;
if (!this.profilePic) { newUser.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; } if (!this.profilePic) { newUser.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; }
else { newUser.profilePic = this.profilePic; } else { newUser.profilePic = this.profilePic; }
return newUser; return newUser;

View File

@ -124,7 +124,7 @@ export class UpdateUser {
user.phone = this.phone; user.phone = this.phone;
user.groups = await this.getGroups(); user.groups = await this.getGroups();
if (!this.profilePic) { user.profilePic = `https://dev.lauf-fuer-kaya.de/lfk-logo.png`; } if (!this.profilePic) { user.profilePic = `https://lauf-fuer-kaya.de/lfk-logo.png`; }
else { user.profilePic = this.profilePic; } else { user.profilePic = this.profilePic; }
return user; return user;

View File

@ -1,5 +1,7 @@
import { faker } from '@faker-js/faker';
import axios from 'axios'; import axios from 'axios';
import { config } from '../../config'; import { config } from '../../config';
const base = "http://localhost:" + config.internal_port const base = "http://localhost:" + config.internal_port
let access_token; let access_token;
@ -21,7 +23,7 @@ describe('delete selfservice runner invalid', () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
"firstname": "string", "firstname": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
added_runner = res.data; added_runner = res.data;
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
@ -50,7 +52,7 @@ describe('delete selfservice runner valid', () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
"firstname": "string", "firstname": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
added_runner = res.data; added_runner = res.data;
expect(res.status).toEqual(200); expect(res.status).toEqual(200);

View File

@ -15,20 +15,20 @@ beforeAll(async () => {
}; };
}); });
describe('POST /api/runners/me/forgot invalid syntax/mail should fail', () => { describe('POST /api/runners/me/login invalid syntax/mail should fail', () => {
it('get without mail return 404', async () => { it('get without mail return 404', async () => {
const res = await axios.post(base + '/api/runners/forgot', null, axios_config); const res = await axios.post(base + '/api/runners/login', null, axios_config);
expect(res.status).toEqual(404); expect(res.status).toEqual(404);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
}); });
it('get without bs mail return 404', async () => { it('get without bs mail return 404', async () => {
const res = await axios.post(base + '/api/runners/forgot?mail=asdasdasdasdasd@tester.test.dev.lauf-fuer-kaya.de', null, axios_config); const res = await axios.post(base + '/api/runners/login?mail=asdasdasdasdasd@tester.test.dev.lauf-fuer-kaya.de', null, axios_config);
expect(res.status).toEqual(404); expect(res.status).toEqual(404);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
}); });
}); });
// --------------- // ---------------
describe('POST /api/runners/me/forgot 2 times within timeout should fail', () => { describe('POST /api/runners/me/login 2 times within timeout should fail', () => {
let added_runner; let added_runner;
it('registering as citizen should return 200', async () => { it('registering as citizen should return 200', async () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
@ -42,19 +42,19 @@ describe('POST /api/runners/me/forgot 2 times within timeout should fail', () =>
added_runner = res.data; added_runner = res.data;
}); });
it('post with valid mail should return 200', async () => { it('post with valid mail should return 200', async () => {
const res = await axios.post(base + '/api/runners/forgot?mail=' + added_runner.email, null, axios_config); const res = await axios.post(base + '/api/runners/login?mail=' + added_runner.email, null, axios_config);
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
}); });
it('2nd post with valid mail should return 406', async () => { it('2nd post with valid mail should return 406', async () => {
const res = await axios.post(base + '/api/runners/forgot?mail=' + added_runner.email, null, axios_config); const res = await axios.post(base + '/api/runners/login?mail=' + added_runner.email, null, axios_config);
expect(res.status).toEqual(406); expect(res.status).toEqual(406);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
}); });
}); });
// --------------- // ---------------
describe('POST /api/runners/me/forgot valid should return 200', () => { describe('POST /api/runners/me/login valid should return 200', () => {
let added_runner; let added_runner;
let new_token; let new_token;
it('registering as citizen should return 200', async () => { it('registering as citizen should return 200', async () => {
@ -69,7 +69,7 @@ describe('POST /api/runners/me/forgot valid should return 200', () => {
added_runner = res.data; added_runner = res.data;
}); });
it('post with valid mail should return 200', async () => { it('post with valid mail should return 200', async () => {
const res = await axios.post(base + '/api/runners/forgot?mail=' + added_runner.email, null, axios_config); const res = await axios.post(base + '/api/runners/login?mail=' + added_runner.email, null, axios_config);
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
new_token = res.data.token; new_token = res.data.token;

View File

@ -1,3 +1,4 @@
import { faker } from '@faker-js/faker';
import axios from 'axios'; import axios from 'axios';
import { config } from '../../config'; import { config } from '../../config';
const base = "http://localhost:" + config.internal_port const base = "http://localhost:" + config.internal_port
@ -30,7 +31,7 @@ describe('register + get should return 200', () => {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");

View File

@ -1,3 +1,4 @@
import { faker } from '@faker-js/faker';
import axios from 'axios'; import axios from 'axios';
import { config } from '../../config'; import { config } from '../../config';
const base = "http://localhost:" + config.internal_port const base = "http://localhost:" + config.internal_port
@ -39,7 +40,7 @@ describe('register invalid citizen', () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
expect(res.status).toEqual(400); expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
@ -48,7 +49,7 @@ describe('register invalid citizen', () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
expect(res.status).toEqual(400); expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
@ -59,7 +60,26 @@ describe('register invalid citizen', () => {
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"phone": "peter", "phone": "peter",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config);
expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json");
});
it('registering as citizen with duplicate mail should return 400', async () => {
const mail = faker.internet.exampleEmail();
await axios.post(base + '/api/runners/register', {
"firstname": "string",
"middlename": "string",
"lastname": "string",
"phone": "peter",
"email": mail,
}, axios_config);
const res = await axios.post(base + '/api/runners/register', {
"firstname": "string",
"middlename": "string",
"lastname": "string",
"phone": "peter",
"email": mail,
}, axios_config); }, axios_config);
expect(res.status).toEqual(400); expect(res.status).toEqual(400);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
@ -71,7 +91,7 @@ describe('register citizen valid', () => {
const res = await axios.post(base + '/api/runners/register', { const res = await axios.post(base + '/api/runners/register', {
"firstname": "string", "firstname": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com" "email": faker.internet.exampleEmail(),
}, axios_config); }, axios_config);
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
expect(res.headers['content-type']).toContain("application/json"); expect(res.headers['content-type']).toContain("application/json");
@ -81,7 +101,7 @@ describe('register citizen valid', () => {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com", "email": faker.internet.exampleEmail(),
"phone": "+4909132123456", "phone": "+4909132123456",
"address": { "address": {
address1: "Teststreet 1", address1: "Teststreet 1",
@ -187,7 +207,7 @@ describe('register valid company', () => {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com", "email": faker.internet.exampleEmail(),
"phone": "+4909132123456", "phone": "+4909132123456",
"address": { "address": {
address1: "Teststreet 1", address1: "Teststreet 1",
@ -214,7 +234,7 @@ describe('register valid company', () => {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com", "email": faker.internet.exampleEmail(),
"phone": "+4909132123456", "phone": "+4909132123456",
"address": { "address": {
address1: "Teststreet 1", address1: "Teststreet 1",
@ -232,7 +252,7 @@ describe('register valid company', () => {
"firstname": "string", "firstname": "string",
"middlename": "string", "middlename": "string",
"lastname": "string", "lastname": "string",
"email": "user@example.com", "email": faker.internet.exampleEmail(),
"phone": "+4909132123456", "phone": "+4909132123456",
"address": { "address": {
address1: "Teststreet 1", address1: "Teststreet 1",