API-10: Adding PartyPlaner register endpoint (#3)
All checks were successful
Jenkins Production Deployment

- Also various other improvements

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Reviewed-on: #3
Co-authored-by: Patrick Müller <patrick@plutodev.de>
Co-committed-by: Patrick Müller <patrick@plutodev.de>
This commit is contained in:
Patrick Müller 2021-08-20 11:26:38 +00:00
parent 7df8b5ad8e
commit 7265f92486
12 changed files with 651 additions and 1201 deletions

12
app.ts
View File

@ -1,7 +1,10 @@
import express from 'express';
import * as http from 'http';
import * as bodyparser from 'body-parser';
import * as dotenv from 'dotenv';
// Router imports
import {partyPlanerRouter} from './src/models/partyplaner/PartyPlaner.router';
import {highlightMarkerRouter} from './src/models/twitch-highlight-marker/HighlightMarker.router';
import {dhbwServiceRouter} from './src/models/dhbw-service/DHBWService.router';
dotenv.config();
@ -12,16 +15,11 @@ if (!process.env.PORT) {
const port: number = parseInt(process.env.PORT, 10);
// Router imports
import {partyPlanerRouter} from "./src/models/partyplaner/PartyPlaner.router";
import {highlightMarkerRouter} from "./src/models/twitch-highlight-marker/HighlightMarker.router";
import { dhbwServiceRouter } from './src/models/dhbw-service/DHBWServiceRouter';
const app: express.Application = express();
const server: http.Server = http.createServer(app);
// here we are adding middleware to parse all incoming requests as JSON
app.use(bodyparser.json());
app.use(express.json());
// Add routers
app.use('/dhbw-service', dhbwServiceRouter);

1513
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,15 @@
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.1",
"debug": "^4.3.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"guid-typescript": "^1.0.9",
"mariadb": "^2.5.3"
},
"devDependencies": {
"@types/bcrypt": "^3.0.1",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.11",
"source-map-support": "^0.5.19",

View File

@ -2,7 +2,7 @@
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {generalInfoRouter} from "./generalInfo/GeneralInfo.router";
import {generalInfoRouter} from './generalInfo/GeneralInfo.router';
/**
* Router Definition
@ -12,11 +12,11 @@ export const dhbwServiceRouter = express.Router();
// Sub-Endpoints
dhbwServiceRouter.use('/generalInfo', generalInfoRouter);
generalInfoRouter.get('/', async (req: Request, res: Response) => {
dhbwServiceRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`Pluto Development DHBW Service App API Endpoint`);
res.status(200).send('Pluto Development DHBW Service App API Endpoint');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -10,18 +10,18 @@ export const generalInfoRouter = express.Router();
generalInfoRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`GET generalInfo v2.1`);
res.status(200).send('GET generalInfo v2.1');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})
generalInfoRouter.post('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`POST generalInfo v2.1`);
res.status(200).send('POST generalInfo v2.1');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -2,7 +2,8 @@
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {dataRouter} from "./data/Data.router";
import {dataRouter} from './data/Data.router';
import {registerRouter} from './register/Register.router';
/**
* Router Definition
@ -11,12 +12,13 @@ export const partyPlanerRouter = express.Router();
// Sub-Endpoints
partyPlanerRouter.use('/data', dataRouter);
partyPlanerRouter.use('/register', registerRouter);
partyPlanerRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`Pluto Development PartyPlaner API Endpoint V2`);
res.status(200).send('Pluto Development PartyPlaner API Endpoint V2');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -10,18 +10,18 @@ export const dataRouter = express.Router();
dataRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`GET data`);
res.status(200).send('GET data');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})
dataRouter.post('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`POST data`);
res.status(200).send('POST data');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -0,0 +1,67 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as UserService from '../userService/user.service';
/**
* Router Definition
*/
export const registerRouter = express.Router();
registerRouter.post('/:isDevCall', async (req: Request, res: Response) => {
try {
let username: string = '';
let email: string = '';
let firstName: string = '';
let lastName: string = '';
let password: string = '';
let useDev: boolean = (req.params.isDevCall ?? '') === 'dev'; // TBD
// API accepts both JSON in body and HTTP parameters
if (req.headers['content-type'] === 'application/json') {
username = req.body.username;
email = req.body.email;
firstName = req.body.firstName;
lastName = req.body.lastName;
password = req.body.password;
} else if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
username = (req.query.username ?? '').toString();
email = (req.query.email ?? '').toString();
firstName = (req.query.firstName ?? '').toString();
lastName = (req.query.lastName ?? '').toString();
password = (req.query.password ?? '').toString();
}
let userIP = req.socket.remoteAddress ?? '';
let deviceInfo = req.headers['user-agent'] ?? '';
if (username === '' || email === '' || firstName === '' || lastName === '' || password === '') {
res.status(400).send({
'status': 'WRONG_PARAMS',
'message': 'Missing or wrong parameters'
});
return;
}
// Check for invalid username / email
const status = await UserService.checkUsernameAndEmail(useDev, username, email);
if (status.hasProblems) {
// Username and/or email are duplicates, return error
res.status(400).send({
'message': status.messages[0],
'status': status.status[0],
'additionalMessages': status.messages.slice(1),
'additionalStatus': status.status.slice(1)
});
return;
}
// Create user
let session = await UserService.registerUser(useDev, username, email, firstName, lastName, password, userIP, deviceInfo);
res.status(201).send(session);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -0,0 +1,200 @@
import * as dotenv from 'dotenv';
import * as bcrypt from 'bcrypt';
import {Guid} from 'guid-typescript';
dotenv.config();
const mariadb = require('mariadb');
const prod_pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.PARTYPLANER_PROD_DATABASE,
connectionLimit: 5
});
const dev_pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.PARTYPLANER_DEV_DATABASE,
connectionLimit: 5
});
export const getExistingUsernamesAndEmails = async (useDev: boolean): Promise<any> => {
let conn;
try {
if (useDev) {
conn = await dev_pool.getConnection();
} else {
conn = await prod_pool.getConnection();
}
const rows = await conn.query('SELECT username, email FROM users');
let usernames: string[] = [];
let emails: string[] = [];
for (let row in rows) {
if (row !== 'meta') {
usernames.push(rows[row].username);
emails.push(rows[row].email);
}
}
return {
'usernames': usernames,
'emails': emails
};
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
}
/**
* Used in the checkUsernameAndEmail method as return value
*/
export interface Session {
userId: string;
sessionId: string;
sessionKey: string;
}
export const registerUser = async (useDev: boolean, username: string, email: string, firstName: string, lastName: string, password: string, ip: string, deviceInfo: string): Promise<Session> => {
let conn;
try {
if (useDev) {
conn = await dev_pool.getConnection();
} else {
conn = await prod_pool.getConnection();
}
const pwHash = bcrypt.hashSync(password, 10);
const sessionKey = Guid.create().toString();
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
const verifyEmailCode = Guid.create().toString();
// Create user
const userQuery = 'INSERT INTO users (username, email, first_name, last_name, password_hash, verify_email_code, registration_date, last_login) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW()) RETURNING user_id';
const userIdRes = await conn.query(userQuery, [username, email, firstName, lastName, pwHash, verifyEmailCode]);
await conn.commit();
// Get user id of the created user
let userId: number = -1;
for (const row in userIdRes) {
if (row !== 'meta' && userIdRes[row].user_id != null) {
userId = userIdRes[row].user_id;
}
}
let deviceType = 'unknown';
if (deviceInfo.includes('PartyPlaner') || deviceInfo.includes('Dart')) {
deviceType = 'mobile';
} else {
deviceType = 'desktop';
}
// Create session
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, created_Date, last_login, valid_until, valid_days, last_ip, device_info, type) VALUES (?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL 365 DAY),365,?, ?, ?) RETURNING session_id';
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip, deviceInfo, deviceType]);
await conn.commit();
// Get session id of the created session
let sessionId: number = -1;
for (const row in sessionIdRes) {
if (row !== 'meta' && sessionIdRes[row].session_id != null) {
sessionId = sessionIdRes[row].session_id;
}
}
return {
'userId': userId.toString(),
'sessionId': sessionId.toString(),
'sessionKey': sessionKey
};
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
}
/**
* Used in the checkUsernameAndEmail method as return value
*/
export interface Status {
hasProblems: boolean;
messages: string[];
status: string[];
}
/**
* Checks if the given username and email are valid and not yet used by another user
* @param useDev If the dev database has to be used
* @param username The username to check
* @param email The email to check
*/
export const checkUsernameAndEmail = async (useDev: boolean, username: string, email: string): Promise<Status> => {
let conn;
try {
if (useDev) {
conn = await dev_pool.getConnection();
} else {
conn = await prod_pool.getConnection();
}
const usernameQuery = 'SELECT username FROM users WHERE username = ?';
const emailQuery = 'SELECT email FROM users WHERE email = ?';
const usernameRes = await conn.query(usernameQuery, username);
const emailRes = await conn.query(emailQuery, email);
let res: Status = {
hasProblems: false,
messages: [],
status: []
};
const usernameRegex = RegExp('^[a-zA-Z0-9\\-\\_]{4,20}$'); // Can contain a-z, A-Z, 0-9, -, _ and has to be 4-20 chars long
if (!usernameRegex.test(username)) {
// Username doesn't match requirements
res.hasProblems = true;
res.messages.push('The username contains illegal characters or is not the allowed size');
res.status.push('INVALID_USERNAME');
}
const emailRegex = RegExp('^[a-zA-Z0-9\\-\\_.]{1,30}\\@[a-zA-Z0-9\\-.]{1,20}\\.[a-z]{1,20}$'); // Normal email regex, user@plutodev.de
if (!emailRegex.test(email)) {
// Username doesn't match requirements
res.hasProblems = true;
res.messages.push('The email contains illegal characters or is not the allowed size');
res.status.push('INVALID_EMAIL');
}
if (usernameRes.length > 0) {
// Username is a duplicate
res.hasProblems = true;
res.messages.push('This username is already in use');
res.status.push('DUPLICATE_USERNAME');
}
if (emailRes.length > 0) {
// Email is a duplicate
res.hasProblems = true;
res.messages.push('This email is already in use');
res.status.push('DUPLICATE_EMAIL');
}
return res;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};

View File

@ -2,7 +2,7 @@
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {addHighlightRouter} from "./addHighlight/AddHighlight.router";
import {addHighlightRouter} from './addHighlight/AddHighlight.router';
/**
* Router Definition
@ -13,9 +13,9 @@ highlightMarkerRouter.use('/addHighlight', addHighlightRouter);
highlightMarkerRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send(`Pluto Development Twitch Highlight Marker API Endpoint`);
res.status(200).send('Pluto Development Twitch Highlight Marker API Endpoint');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -2,7 +2,7 @@
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as AddHighlightService from "./addHighlights.service";
import * as AddHighlightService from './addHighlights.service';
/**
* Router Definition
@ -11,10 +11,10 @@ export const addHighlightRouter = express.Router();
addHighlightRouter.get('/', (req: Request, res: Response) => {
try {
res.status(200).send(`GET endpoint not defined.`);
res.status(200).send('GET endpoint not defined.');
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})
@ -23,23 +23,23 @@ addHighlightRouter.post('/', (req: Request, res: Response) => {
// Check input params
const body = req.body;
if(body.access_key !== process.env.TWITCH_HIGHLIGHTS_ACCESS_KEY){
if (body.access_key !== process.env.TWITCH_HIGHLIGHTS_ACCESS_KEY) {
// Unauthorized, return error
res.type('application/json');
res.status(403).send('{"status": "error", "description": "Unauthorized."}');
} else if(!body.streamer || !body.stream_id || !body.stream_game || !body.timestamp || !body.description || !body.username){
res.status(403).send({'status': 'error', 'description': 'Unauthorized.'});
} else if (!body.streamer || !body.stream_id || !body.stream_game || !body.timestamp || !body.description || !body.username) {
// Missing params, return error
res.type('application/json');
res.status(400).send('{"status": "error", "description": "Missing parameters."}');
res.status(400).send({'status': 'error', 'description': 'Missing parameters.'});
} else {
// Everything fine, return success
AddHighlightService.createHighlightEntry(body);
res.type('application/json');
res.status(200).send('{"status": "success", "description": ""}');
res.status(200).send({'status': 'success', 'description': ''});
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
res.status(500).send({'message': 'Internal Server Error. Try again later.'});
}
})

View File

@ -13,7 +13,6 @@ const pool = mariadb.createPool({
export const createHighlightEntry = async (req_body: any) => {
let conn;
let price: any;
try {
conn = await pool.getConnection();