mirror of
https://github.com/Mueller-Patrick/Betterzon.git
synced 2024-12-23 04:05:12 +00:00
Compare commits
No commits in common. "a3ac897818033edd9bb957df70ebd33b5549775f" and "9e9b442f3549bf156781397be9073faecbb4a632" have entirely different histories.
a3ac897818
...
9e9b442f35
816
Backend/package-lock.json
generated
816
Backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import {User} from './user.interface';
|
|
||||||
|
|
||||||
export interface Users {
|
|
||||||
[key: number]: User;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -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]};
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user