API-37: Adding endpoints for climbing route rating app (!16)
All checks were successful
Jenkins Production Deployment

Co-authored-by: Patrick Mueller <patrick@mueller-patrick.tech>
Reviewed-on: #16
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 2022-01-08 22:21:19 +00:00
parent b4d5bdd0b6
commit 25a66b060c
18 changed files with 4267 additions and 26 deletions

2
app.ts
View File

@ -9,6 +9,7 @@ import logger from './src/middleware/logger';
import {dhbwRaPlaChangesRouter} from './src/models/dhbw-rapla-changes/DHBWRaPlaChanges.router'; import {dhbwRaPlaChangesRouter} from './src/models/dhbw-rapla-changes/DHBWRaPlaChanges.router';
import {raPlaMiddlewareRouter} from './src/models/rapla-middleware/RaPlaMiddleware.router'; import {raPlaMiddlewareRouter} from './src/models/rapla-middleware/RaPlaMiddleware.router';
import {betterzonRouter} from './src/models/betterzon/Betterzon.router'; import {betterzonRouter} from './src/models/betterzon/Betterzon.router';
import {crrRouter} from './src/models/climbing-route-rating/ClimbingRouteRating.router';
let cors = require('cors'); let cors = require('cors');
@ -37,6 +38,7 @@ app.use('/partyplaner', partyPlanerRouter);
app.use('/raplachanges', dhbwRaPlaChangesRouter); app.use('/raplachanges', dhbwRaPlaChangesRouter);
app.use('/rapla-middleware', raPlaMiddlewareRouter); app.use('/rapla-middleware', raPlaMiddlewareRouter);
app.use('/betterzon', betterzonRouter); app.use('/betterzon', betterzonRouter);
app.use('/crr', crrRouter);
// this is a simple route to make sure everything is working properly // this is a simple route to make sure everything is working properly
app.get('/', (req: express.Request, res: express.Response) => { app.get('/', (req: express.Request, res: express.Response) => {

3745
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"axios": "^0.24.0",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"debug": "^4.3.1", "debug": "^4.3.1",
@ -21,6 +22,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"guid-typescript": "^1.0.9", "guid-typescript": "^1.0.9",
"mariadb": "^2.5.3", "mariadb": "^2.5.3",
"random-words": "^1.1.1",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
@ -28,6 +30,7 @@
"@types/bcrypt": "^3.0.1", "@types/bcrypt": "^3.0.1",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/express": "^4.17.11", "@types/express": "^4.17.11",
"@types/random-words": "^1.1.2",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"source-map-support": "^0.5.19", "source-map-support": "^0.5.19",
"tslint": "^6.1.3", "tslint": "^6.1.3",

View File

@ -0,0 +1,19 @@
import * as dotenv from 'dotenv';
const mariadb = require('mariadb');
dotenv.config();
export namespace ClimbingRouteRatingDB {
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.CRR_DATABASE,
connectionLimit: 5
});
export function getConnection() {
return pool;
}
}

View File

@ -0,0 +1,35 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {Guid} from 'guid-typescript';
import logger from '../../middleware/logger';
import {climbingGymRouter} from './climbing_gyms/climbingGyms.router';
import {climbingRoutesRouter} from './climbing_routes/climbingRoutes.router';
import {routeCommentsRouter} from './route_comments/routeComments.router';
import {routeRatingsRouter} from './route_ratings/routeRatings.router';
/**
* Router Definition
*/
export const crrRouter = express.Router();
// Sub-Endpoints
crrRouter.use('/gyms', climbingGymRouter);
crrRouter.use('/routes', climbingRoutesRouter);
crrRouter.use('/comments', routeCommentsRouter);
crrRouter.use('/ratings', routeRatingsRouter);
crrRouter.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send('Pluto Development Climbing Route Rating API Endpoint');
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});

View File

@ -0,0 +1,6 @@
export interface ClimbingGym {
gym_id: number;
name: string;
city: string;
verified: boolean;
}

View File

@ -0,0 +1,65 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {Guid} from 'guid-typescript';
import logger from '../../../middleware/logger';
import {ClimbingGym} from './ClimbingGym.interface';
import * as GymService from './climbingGyms.service';
import {verifyCaptcha} from '../common/VerifyCaptcha';
/**
* Router Definition
*/
export const climbingGymRouter = express.Router();
climbingGymRouter.get('/', async (req: Request, res: Response) => {
try {
const gyms: ClimbingGym[] = await GymService.findAll();
res.status(200).send(gyms);
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});
climbingGymRouter.post('/', async (req: Request, res: Response) => {
try {
let name = req.query.name as string;
let city = req.query.city as string;
let captcha_token = req.query.captcha_token as string;
if (!name || !city || !captcha_token) {
res.status(400).send({'message': 'Missing parameters'});
return;
}
// Verify captcha
if (!await verifyCaptcha(captcha_token)) {
res.status(403).send({'message': 'Invalid Captcha. Please try again.'});
return;
}
let result = await GymService.createGym(name, city);
if (result) {
res.status(201).send({'gym_id': result});
} else {
res.status(500).send({});
}
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});

View File

@ -0,0 +1,33 @@
import {ClimbingRouteRatingDB} from '../ClimbingRouteRating.db';
import {ClimbingGym} from './ClimbingGym.interface';
/**
* Fetches and returns all known climbing gyms
* @return Promise<ClimbingHall[]> The climbing halls
*/
export const findAll = async (): Promise<ClimbingGym[]> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
return await conn.query('SELECT gym_id, name, city, verified FROM climbing_gyms');
} catch (err) {
throw err;
}
};
/**
* Creates a climbing gym and returns the id of the created gym
* @param name The name of the climbing hall
* @param city The city of the climbing hall
* @return number The id of the climbing hall
*/
export const createGym = async (name: string, city: string): Promise<number> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
let res = await conn.query('INSERT INTO climbing_gyms (name, city) VALUES (?, ?) RETURNING gym_id', [name, city]);
return res[0].hall_id;
} catch (err) {
throw err;
}
};

View File

@ -0,0 +1,7 @@
export interface ClimbingRoute {
route_id: string;
gym_id: number;
name: string;
difficulty: string;
route_setting_date: Date;
}

View File

@ -0,0 +1,80 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import {Guid} from 'guid-typescript';
import logger from '../../../middleware/logger';
import {ClimbingRoute} from './ClimbingRoute.interface';
import * as RouteService from './climbingRoutes.service';
import {verifyCaptcha} from '../common/VerifyCaptcha';
/**
* Router Definition
*/
export const climbingRoutesRouter = express.Router();
climbingRoutesRouter.get('/', async (req: Request, res: Response) => {
try {
const routes: ClimbingRoute[] = await RouteService.findAll();
res.status(200).send(routes);
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});
climbingRoutesRouter.get('/:id', async (req: Request, res: Response) => {
try {
let route_id = req.params.id;
const route: ClimbingRoute = await RouteService.findById(route_id);
res.status(200).send(route);
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});
climbingRoutesRouter.post('/', async (req: Request, res: Response) => {
try {
let gym_id = Number(req.query.gym_id);
let name = req.query.name as string;
let difficulty = req.query.difficulty as string;
let captcha_token = req.query.captcha_token as string;
if (isNaN(gym_id) || !name || !difficulty || !captcha_token) {
res.status(400).send({'message': 'Missing parameters'});
return;
}
// Verify captcha
if (!await verifyCaptcha(captcha_token)) {
res.status(403).send({'message': 'Invalid Captcha. Please try again.'});
return;
}
let route_id = await RouteService.createRoute(gym_id, name, difficulty);
res.status(201).send({'route_id': route_id});
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});

View File

@ -0,0 +1,59 @@
import {ClimbingRouteRatingDB} from '../ClimbingRouteRating.db';
import {ClimbingRoute} from './ClimbingRoute.interface';
import random from 'random-words';
/**
* Fetches and returns all known climbing routes
* @return Promise<ClimbingRoute[]> The climbing routes
*/
export const findAll = async (): Promise<ClimbingRoute[]> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
return await conn.query('SELECT route_id, gym_id, name, difficulty, route_setting_date FROM climbing_routes');
} catch (err) {
throw err;
}
};
/**
* Fetches and returns information about the given route
* @param route_id The id of the route
* @return Promise<ClimbingRoute> The climbing route
*/
export const findById = async (route_id: string): Promise<ClimbingRoute> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
return await conn.query('SELECT route_id, gym_id, name, difficulty, route_setting_date FROM climbing_routes WHERE route_id = ?', route_id);
} catch (err) {
throw err;
}
};
/**
* Creates a new route and returns the id of the created route
* @param gym_id The id of the climbing gym that the route belongs to
* @param name The name of the climbing route
* @param difficulty The difficulty of the climbing route
* @return string The id of the created route
*/
export const createRoute = async (gym_id: number, name: string, difficulty: string): Promise<string> => {
let conn = ClimbingRouteRatingDB.getConnection();
// Generate route id
let route_id = '';
let randWords = random(3);
for (let i = 0; i <= 2; i++) {
route_id += randWords[i];
if (i < 2) {
route_id += '-';
}
}
try {
await conn.query('INSERT INTO climbing_routes (route_id, gym_id, name, difficulty) VALUES (?, ?, ?, ?)', [route_id, gym_id, name, difficulty]);
return route_id;
} catch (err) {
throw err;
}
};

View File

@ -0,0 +1,22 @@
import * as dotenv from 'dotenv';
import * as querystring from 'querystring';
import axios from 'axios';
dotenv.config();
export const verifyCaptcha = async (captcha_token: string): Promise<boolean> => {
var postData = querystring.stringify({
response: captcha_token,
secret: process.env.HCAPTCHA_SECRET
});
axios.post('https://hcaptcha.com/siteverify', postData)
.then(res => {
return res.data.success;
})
.catch(error => {
throw(error);
});
return false;
};

View File

@ -0,0 +1,6 @@
export interface RouteComment {
comment_id: number;
route_id: string;
comment: string;
timestamp: Date;
}

View File

@ -0,0 +1,61 @@
import express, {Request, Response} from 'express';
import * as CommentService from './routeComments.service';
import {Guid} from 'guid-typescript';
import logger from '../../../middleware/logger';
import {RouteComment} from './RouteComment.interface';
import {verifyCaptcha} from '../common/VerifyCaptcha';
export const routeCommentsRouter = express.Router();
routeCommentsRouter.get('/by/route/:id', async (req: Request, res: Response) => {
try {
let route_id = req.params.id;
const comments: RouteComment[] = await CommentService.findByRoute(route_id);
res.status(200).send(comments);
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});
routeCommentsRouter.post('/', async (req: Request, res: Response) => {
try {
let route_id = req.query.route_id as string;
let comment = req.query.comment as string;
let captcha_token = req.query.captcha_token as string;
if (!route_id || !comment || !captcha_token) {
res.status(400).send({'message': 'Missing parameters'});
return;
}
// Verify captcha
if (!await verifyCaptcha(captcha_token)) {
res.status(403).send({'message': 'Invalid Captcha. Please try again.'});
return;
}
let result = await CommentService.createComment(route_id, comment);
if (result) {
res.status(201).send({'comment_id': result});
} else {
res.status(500).send({});
}
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});

View File

@ -0,0 +1,32 @@
import {ClimbingRouteRatingDB} from '../ClimbingRouteRating.db';
import {RouteComment} from './RouteComment.interface';
/**
* Fetches and returns all comments that belong to the given route
* @return Promise<RouteComment[]> The comments
*/
export const findByRoute = async (route_id: string): Promise<RouteComment[]> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
return await conn.query('SELECT comment_id, route_id, comment, timestamp FROM route_comments WHERE route_id = ?', route_id);
} catch (err) {
throw err;
}
};
/**
* Creates a new comment and returns the id of the created comment
* @param route_id The id of the route to create the comment for
* @param comment The comment
* @return number The id of the comment
*/
export const createComment = async (route_id: string, comment: string): Promise<number> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
let res = await conn.query('INSERT INTO route_comments (route_id, comment) VALUES (?, ?) RETURNING comment_id', [route_id, comment]);
return res[0].comment_id;
} catch (err) {
throw err;
}
};

View File

@ -0,0 +1,6 @@
export interface RouteRating {
rating_id: number;
route_id: string;
stars: number;
timestamp: Date;
}

View File

@ -0,0 +1,60 @@
import express, {Request, Response} from 'express';
import * as RatingService from './routeRatings.service';
import {Guid} from 'guid-typescript';
import logger from '../../../middleware/logger';
export const routeRatingsRouter = express.Router();
routeRatingsRouter.get('/by/route/:id', async (req: Request, res: Response) => {
try {
let route_id = req.params.id;
let rating = await RatingService.getStarsForRoute(route_id);
res.status(200).send({'rating': rating});
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});
routeRatingsRouter.post('/', async (req: Request, res: Response) => {
try {
let route_id = req.query.route_id as string;
let stars = Number(req.query.stars);
let captcha_token = req.query.captcha_token as string;
if (!route_id || isNaN(stars) || !captcha_token) {
console.log(route_id, stars, captcha_token);
res.status(400).send({'message': 'Missing parameters'});
return;
}
// Verify captcha
/*if (!await verifyCaptcha(captcha_token)) {
res.status(403).send({'message': 'Invalid Captcha. Please try again.'});
return;
}*/
let result = await RatingService.createRating(route_id, stars);
if (result) {
res.status(201).send({'rating_id': result});
} else {
res.status(500).send({});
}
} catch (e) {
let errorGuid = Guid.create().toString();
logger.error('Error handling a request: ' + e.message, {reference: errorGuid});
res.status(500).send({
'status': 'PROCESSING_ERROR',
'message': 'Internal Server Error. Try again later.',
'reference': errorGuid
});
}
});

View File

@ -0,0 +1,52 @@
import {ClimbingRouteRatingDB} from '../ClimbingRouteRating.db';
import {RouteRating} from './RouteRating.interface';
/**
* Fetches and returns all ratings for the given route
* @param route_id The id of the route to get the ratings for
* @return Promise<RouteRating[]> The ratings
*/
export const findByRoute = async (route_id: string): Promise<RouteRating[]> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
return await conn.query('SELECT rating_id, route_id, stars, timestamp FROM route_ratings WHERE route_id = ?', route_id);
} catch (err) {
throw err;
}
};
/**
* Get the median amount of stars the given route got from climbers
* @param route_id The id of the route to get the rating for
* @return number The median amount of stars with 1 fraction digit.
*/
export const getStarsForRoute = async (route_id: string): Promise<number> => {
let ratings = await findByRoute(route_id);
let starsSum = 0;
let starsAmount = 0;
for (let rating of ratings) {
starsSum += rating.stars;
starsAmount++;
}
return Number((starsSum / starsAmount).toFixed(1));
};
/**
* Creates a new rating and returns the id
* @param route_id The id of the route to be rated
* @param stars The amount of stars to be given
* @return number The id of the created rating
*/
export const createRating = async (route_id: string, stars: number): Promise<number> => {
let conn = ClimbingRouteRatingDB.getConnection();
try {
let res = await conn.query('INSERT INTO route_ratings (route_id, stars) VALUES (?, ?) RETURNING rating_id', [route_id, stars]);
return res[0].comment_id;
} catch (err) {
throw err;
}
};