Compare commits

..

No commits in common. "a3ac897818033edd9bb957df70ebd33b5549775f" and "9e9b442f3549bf156781397be9073faecbb4a632" have entirely different histories.

10 changed files with 49 additions and 1113 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,17 +11,14 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"guid-typescript": "^1.0.9",
"helmet": "^4.2.0", "helmet": "^4.2.0",
"mariadb": "^2.5.1", "mariadb": "^2.5.1",
"typeorm": "^0.2.29" "typeorm": "^0.2.29"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^3.0.1",
"@types/cors": "^2.8.8", "@types/cors": "^2.8.8",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.9", "@types/express": "^4.17.9",

View File

@ -13,7 +13,6 @@ import {pricesRouter} from './models/prices/prices.router';
import {vendorsRouter} from './models/vendors/vendors.router'; import {vendorsRouter} from './models/vendors/vendors.router';
import {errorHandler} from './middleware/error.middleware'; import {errorHandler} from './middleware/error.middleware';
import {notFoundHandler} from './middleware/notFound.middleware'; import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router';
dotenv.config(); dotenv.config();
@ -42,7 +41,6 @@ app.use('/products', productsRouter);
app.use('/categories', categoriesRouter); app.use('/categories', categoriesRouter);
app.use('/manufacturers', manufacturersRouter); app.use('/manufacturers', manufacturersRouter);
app.use('/prices', pricesRouter); app.use('/prices', pricesRouter);
app.use('/users', usersRouter);
app.use('/vendors', vendorsRouter); app.use('/vendors', vendorsRouter);
app.use(errorHandler); app.use(errorHandler);

View File

@ -82,25 +82,6 @@ pricesRouter.get('/bestDeals/:amount', async (req: Request, res: Response) => {
} }
}); });
// GET prices/byProduct/list/[]
pricesRouter.get('/byProduct/list/:ids', async (req: Request, res: Response) => {
const productIds: [number] = JSON.parse(req.params.ids);
if (!productIds) {
res.status(400).send('Missing parameters.');
return;
}
try {
const prices: Prices = await PriceService.findListByProducts(productIds);
res.status(200).send(prices);
} catch (e) {
res.status(404).send(e.message);
}
});
// POST items/ // POST items/
// pricesRouter.post('/', async (req: Request, res: Response) => { // pricesRouter.post('/', async (req: Request, res: Response) => {

View File

@ -195,6 +195,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
let allPrices: Record<number, Price[]> = {}; let allPrices: Record<number, Price[]> = {};
// Get newest prices for every product at every vendor // Get newest prices for every product at every vendor
const rows = await conn.query( const rows = await conn.query(
'WITH summary AS (\n' + 'WITH summary AS (\n' +
' SELECT p.product_id,\n' + ' SELECT p.product_id,\n' +
@ -221,11 +222,10 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
} }
// Iterate over all prices to find the products with the biggest difference between amazon and other vendor // Iterate over all prices to find the products with the biggest difference between amazon and other vendor
let deals: Price[] = []; let deals = [];
for (let productId in Object.keys(allPrices)) {
Object.keys(allPrices).forEach(productId => { if (allPrices[productId]) {
if (allPrices[parseInt(productId)]) { let pricesForProd = allPrices[productId];
let pricesForProd = allPrices[parseInt(productId)];
// Get amazon price and lowest price from other vendor // Get amazon price and lowest price from other vendor
let amazonPrice = {} as Price; let amazonPrice = {} as Price;
@ -234,7 +234,6 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
if (price.vendor_id === 1) { if (price.vendor_id === 1) {
amazonPrice = price; amazonPrice = price;
} else { } else {
// If there is no lowest price yet or the price of the current iteration is lower, set / replace it
if (!lowestPrice.price_in_cents || lowestPrice.price_in_cents > price.price_in_cents) { if (!lowestPrice.price_in_cents || lowestPrice.price_in_cents > price.price_in_cents) {
lowestPrice = price; lowestPrice = price;
} }
@ -246,25 +245,25 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
'product_id': lowestPrice.product_id, 'product_id': lowestPrice.product_id,
'vendor_id': lowestPrice.vendor_id, 'vendor_id': lowestPrice.vendor_id,
'price_in_cents': lowestPrice.price_in_cents, 'price_in_cents': lowestPrice.price_in_cents,
'timestamp': lowestPrice.timestamp, 'timestamp' :lowestPrice.timestamp,
'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents), 'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents),
'amazonDifferencePercent': ((1 - (lowestPrice.price_in_cents / amazonPrice.price_in_cents)) * 100), 'amazonDifferencePercent': ((1 - (lowestPrice.price_in_cents / amazonPrice.price_in_cents)) * 100),
}; };
// Push only deals were the amazon price is actually higher // Push only deals were the amazon price is actually higher
if (deal.amazonDifferencePercent > 0) { if(deal.amazonDifferencePercent > 0) {
deals.push(deal as Price); deals.push(deal);
} }
} }
}); }
// Sort to have the best deals on the top // Sort to have the best deals on the top
deals.sort((a, b) => a.amazonDifferencePercent! < b.amazonDifferencePercent! ? 1 : -1); deals.sort((a, b) => a.amazonDifferencePercent < b.amazonDifferencePercent ? 1 : -1);
// Return only as many records as requested or the maximum amount of found deals, whatever is less // Return only as many records as requested or the maximum amount of found deals, whatever is less
let maxAmt = Math.min(amount, deals.length); let maxAmt = Math.min(amount, deals.length);
for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) { for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++){
//console.log(deals[dealIndex]); //console.log(deals[dealIndex]);
priceRows.push(deals[dealIndex] as Price); priceRows.push(deals[dealIndex] as Price);
} }
@ -281,70 +280,6 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
return priceRows; return priceRows;
}; };
/**
* Get the lowest, latest, non-amazon price for each given product
* @param ids the ids of the products
*/
export const findListByProducts = async (productIds: [number]): Promise<Prices> => {
let conn;
let priceRows: Price[] = [];
try {
conn = await pool.getConnection();
let allPrices: Record<number, Price[]> = {};
// Get newest prices for every given product at every vendor
const rows = await conn.query(
'WITH summary AS (\n' +
' SELECT p.product_id,\n' +
' p.vendor_id,\n' +
' p.price_in_cents,\n' +
' p.timestamp,\n' +
' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p' +
' WHERE p.product_id IN (?)' +
' AND p.vendor_id != 1)\n' +
'SELECT s.*\n' +
'FROM summary s\n' +
'WHERE s.rk = 1', [productIds]);
// Write returned values to allPrices map with product id as key and a list of prices as value
for (let row in rows) {
if (row !== 'meta') {
if (!allPrices[parseInt(rows[row].product_id)]) {
allPrices[parseInt(rows[row].product_id)] = [];
}
allPrices[parseInt(rows[row].product_id)].push(rows[row]);
}
}
// Iterate over all products to find lowest price
Object.keys(allPrices).forEach(productId => {
if (allPrices[parseInt(productId)]) {
let pricesForProd = allPrices[parseInt(productId)];
// Sort ascending by price so index 0 has the lowest price
pricesForProd.sort((a, b) => a.price_in_cents > b.price_in_cents ? 1 : -1);
// Push the lowest price to the return list
priceRows.push(pricesForProd[0]);
}
});
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return priceRows;
};
// export const create = async (newItem: Product): Promise<void> => { // export const create = async (newItem: Product): Promise<void> => {
// let conn; // let conn;
// try { // try {

View File

@ -1,10 +0,0 @@
export interface Session {
session_id: number;
session_key: string;
session_key_hash: string;
createdDate?: Date;
lastLogin?: Date;
validUntil?: Date;
validDays?: number;
last_IP: string;
}

View File

@ -1,9 +0,0 @@
export interface User {
user_id: number;
username: string;
email: string;
password_hash: string;
hashing_salt: string;
registration_date: Date;
last_login_date: Date;
}

View File

@ -1,5 +0,0 @@
import {User} from './user.interface';
export interface Users {
[key: number]: User;
}

View File

@ -1,54 +0,0 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as UserService from './users.service';
import {User} from './user.interface';
import {Users} from './users.interface';
import {Session} from './session.interface';
/**
* Router Definition
*/
export const usersRouter = express.Router();
/**
* Controller Definitions
*/
// POST users/register
usersRouter.post('/register', async (req: Request, res: Response) => {
try {
const username: string = req.body.username;
const password: string = req.body.password;
const email: string = req.body.email;
const ip: string = req.connection.remoteAddress?? '';
if (!username || !password || !email) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Check if username and / or email are already used
const status = await UserService.checkUsernameAndEmail(username, email);
if (status.hasProblems) {
// Username and/or email are duplicates, return error
res.status(400).send(JSON.stringify({messages: status.messages, codes: status.codes}));
return;
}
// Create the user and a session
const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user
res.status(201).send(session);
} catch (e) {
res.status(404).send(e.message);
}
});

View File

@ -1,157 +0,0 @@
import * as dotenv from 'dotenv';
import * as bcrypt from 'bcrypt';
import {Guid} from 'guid-typescript';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {User} from './user.interface';
import {Users} from './users.interface';
import {Session} from './session.interface';
/**
* Service Methods
*/
/**
* Creates a user record in the database, also creates a session. Returns the session if successful.
*/
export const createUser = async (username: string, password: string, email: string, ip: string): Promise<Session> => {
let conn;
try {
// Hash password and generate + hash session key
const pwHash = bcrypt.hashSync('123', 10);
const sessionKey = Guid.create().toString();
const sessionKeyHash = bcrypt.hashSync(sessionKey, 10);
// Create user entry in SQL
conn = await pool.getConnection();
const userQuery = 'INSERT INTO users (username, email, bcrypt_password_hash) VALUES (?, ?, ?) RETURNING user_id';
const userIdRes = await conn.query(userQuery, [username, email, pwHash]);
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;
}
}
// Create session
const sessionQuery = 'INSERT INTO sessions (user_id, session_key_hash, createdDate, lastLogin, validUntil, validDays, last_IP) VALUES (?,?,NOW(),NOW(),DATE_ADD(NOW(), INTERVAL 30 DAY),30,?) RETURNING session_id';
const sessionIdRes = await conn.query(sessionQuery, [userId, sessionKeyHash, ip]);
await conn.commit();
// Get session id of the created user
let sessionId: number = -1;
for (const row in sessionIdRes) {
if (row !== 'meta' && sessionIdRes[row].session_id != null) {
sessionId = sessionIdRes[row].session_id;
}
}
return {
session_id: sessionId,
session_key: sessionKey,
session_key_hash: '',
last_IP: ip
};
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return {} as Session;
};
/**
* Used in the checkUsernameAndEmail method as return value
*/
export interface Status {
hasProblems: boolean;
messages: string[];
codes: number[]; // 0 = all good, 1 = wrong username, 2 = wrong email, 3 = server error
}
/**
* Checks if the given username and email are not used yet by another user
* @param username The username to check
* @param email The email to check
*/
export const checkUsernameAndEmail = async (username: string, email: string): Promise<Status> => {
let conn;
try {
// Create user entry in SQL
conn = await 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: [],
codes: []
};
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('Invalid username');
res.codes.push(1);
}
const emailRegex = RegExp('^[a-zA-Z0-9\\-\\_.]{1,30}\\@[a-zA-Z0-9\\-.]{1,20}\\.[a-z]{1,20}$'); // Normal email regex, user@betterzon.xyz
if (!emailRegex.test(email)) {
// Username doesn't match requirements
res.hasProblems = true;
res.messages.push('Invalid email');
res.codes.push(2);
}
if (usernameRes.length > 0) {
// Username is a duplicate
res.hasProblems = true;
res.messages.push('Duplicate username');
res.codes.push(1);
}
if (emailRes.length > 0) {
// Email is a duplicate
res.hasProblems = true;
res.messages.push('Duplicate email');
res.codes.push(2);
}
return res;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return {hasProblems: true, messages: ['Internal server error'], codes: [3]};
};