diff --git a/app.ts b/app.ts index d0a5498..0f4cf87 100644 --- a/app.ts +++ b/app.ts @@ -8,6 +8,7 @@ import {dhbwServiceRouter} from './src/models/dhbw-service/DHBWService.router'; import logger from './src/middleware/logger'; import {dhbwRaPlaChangesRouter} from './src/models/dhbw-rapla-changes/DHBWRaPlaChanges.router'; import {raPlaMiddlewareRouter} from './src/models/rapla-middleware/RaPlaMiddleware.router'; +import {betterzonRouter} from './src/models/betterzon/Betterzon.router'; let cors = require('cors'); @@ -35,6 +36,7 @@ app.use('/twitch-highlight-marker', highlightMarkerRouter); app.use('/partyplaner', partyPlanerRouter); app.use('/raplachanges', dhbwRaPlaChangesRouter); app.use('/rapla-middleware', raPlaMiddlewareRouter); +app.use('/betterzon', betterzonRouter); // this is a simple route to make sure everything is working properly app.get('/', (req: express.Request, res: express.Response) => { diff --git a/src/models/betterzon/Betterzon.router.ts b/src/models/betterzon/Betterzon.router.ts new file mode 100644 index 0000000..b8dddf0 --- /dev/null +++ b/src/models/betterzon/Betterzon.router.ts @@ -0,0 +1,47 @@ +/** + * Required External Modules and Interfaces + */ +import express, {Request, Response} from 'express'; +import {Guid} from 'guid-typescript'; +import logger from '../../middleware/logger'; +import {productsRouter} from './products/products.router'; +import {contactpersonsRouter} from './contact_persons/contact_persons.router'; +import {pricealarmsRouter} from './pricealarms/pricealarms.router'; +import {usersRouter} from './users/users.router'; +import {pricesRouter} from './prices/prices.router'; +import {vendorsRouter} from './vendors/vendors.router'; +import {categoriesRouter} from './categories/categories.router'; +import {manufacturersRouter} from './manufacturers/manufacturers.router'; +import {favoriteshopsRouter} from './favorite_shops/favoriteshops.router'; +import {crawlingstatusRouter} from './crawling_status/crawling_status.router'; + +/** + * Router Definition + */ +export const betterzonRouter = express.Router(); + +betterzonRouter.use('/products', productsRouter); +betterzonRouter.use('/categories', categoriesRouter); +betterzonRouter.use('/manufacturers', manufacturersRouter); +betterzonRouter.use('/prices', pricesRouter); +betterzonRouter.use('/users', usersRouter); +betterzonRouter.use('/vendors', vendorsRouter); +betterzonRouter.use('/pricealarms', pricealarmsRouter); +betterzonRouter.use('/contactpersons', contactpersonsRouter); +betterzonRouter.use('/favoriteshops', favoriteshopsRouter); +betterzonRouter.use('/crawlingstatus', crawlingstatusRouter); + + +betterzonRouter.get('/', async (req: Request, res: Response) => { + try { + res.status(200).send('Pluto Development PartyPlaner API Endpoint V2'); + } 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 + }); + } +}); diff --git a/src/models/betterzon/categories/categories.interface.ts b/src/models/betterzon/categories/categories.interface.ts new file mode 100644 index 0000000..136ee49 --- /dev/null +++ b/src/models/betterzon/categories/categories.interface.ts @@ -0,0 +1,5 @@ +import {Category} from './category.interface'; + +export interface Categories { + [key: number]: Category; +} diff --git a/src/models/betterzon/categories/categories.router.ts b/src/models/betterzon/categories/categories.router.ts new file mode 100644 index 0000000..9d811f6 --- /dev/null +++ b/src/models/betterzon/categories/categories.router.ts @@ -0,0 +1,70 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as CategoryService from './categories.service'; +import {Category} from './category.interface'; +import {Categories} from './categories.interface'; + + +/** + * Router Definition + */ + +export const categoriesRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET categories/ +categoriesRouter.get('/', async (req: Request, res: Response) => { + try { + const categories: Categories = await CategoryService.findAll(); + + res.status(200).send(categories); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET categories/:id +categoriesRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const category: Category = await CategoryService.find(id); + + res.status(200).send(category); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET categories/search/:term +categoriesRouter.get('/search/:term', async (req: Request, res: Response) => { + const term: string = req.params.term; + + if (!term) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const categories: Categories = await CategoryService.findBySearchTerm(term); + + res.status(200).send(categories); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/categories/categories.service.ts b/src/models/betterzon/categories/categories.service.ts new file mode 100644 index 0000000..ed88cc8 --- /dev/null +++ b/src/models/betterzon/categories/categories.service.ts @@ -0,0 +1,112 @@ +import * as dotenv from 'dotenv'; +import {Category} from './category.interface'; +import {Categories} from './categories.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known categories + */ +export const findAll = async (): Promise => { + let conn; + let categRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT category_id, name FROM categories'); + for (let row in rows) { + if (row !== 'meta') { + let categ: Category = { + category_id: 0, + name: '' + }; + const sqlCateg = rows[row]; + + categ.category_id = sqlCateg.category_id; + categ.name = sqlCateg.name; + categRows.push(categ); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return categRows; +}; + +/** + * Fetches and returns the category with the specified id + * @param id The id of the category to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let categ: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT category_id, name FROM categories WHERE category_id = ?', id); + for (let row in rows) { + if (row !== 'meta') { + categ = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return categ; +}; + +/** + * Fetches and returns all categories that match the search term + * @param term the term to match + */ +export const findBySearchTerm = async (term: string): Promise => { + let conn; + let categRows = []; + try { + conn = await pool.getConnection(); + term = '%' + term + '%'; + const rows = await conn.query('SELECT category_id, name FROM categories WHERE name LIKE ?', term); + for (let row in rows) { + if (row !== 'meta') { + categRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return categRows; +}; diff --git a/src/models/betterzon/categories/category.interface.ts b/src/models/betterzon/categories/category.interface.ts new file mode 100644 index 0000000..a909167 --- /dev/null +++ b/src/models/betterzon/categories/category.interface.ts @@ -0,0 +1,4 @@ +export interface Category { + category_id: number; + name: string; +} diff --git a/src/models/betterzon/contact_persons/contact_person.interface.ts b/src/models/betterzon/contact_persons/contact_person.interface.ts new file mode 100644 index 0000000..e777a40 --- /dev/null +++ b/src/models/betterzon/contact_persons/contact_person.interface.ts @@ -0,0 +1,9 @@ +export interface Contact_Person { + contact_person_id: number; + first_name: string; + last_name: string; + gender: string; + email: string; + phone: string; + vendor_id: number; +} diff --git a/src/models/betterzon/contact_persons/contact_persons.interface.ts b/src/models/betterzon/contact_persons/contact_persons.interface.ts new file mode 100644 index 0000000..97f8393 --- /dev/null +++ b/src/models/betterzon/contact_persons/contact_persons.interface.ts @@ -0,0 +1,5 @@ +import {Contact_Person} from './contact_person.interface'; + +export interface Contact_Persons { + [key: number]: Contact_Person; +} diff --git a/src/models/betterzon/contact_persons/contact_persons.router.ts b/src/models/betterzon/contact_persons/contact_persons.router.ts new file mode 100644 index 0000000..071482b --- /dev/null +++ b/src/models/betterzon/contact_persons/contact_persons.router.ts @@ -0,0 +1,133 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as ContactPersonService from './contact_persons.service'; +import {Contact_Person} from './contact_person.interface'; +import {Contact_Persons} from './contact_persons.interface'; +import * as UserService from '../users/users.service'; +import * as PriceService from '../prices/prices.service'; + + +/** + * Router Definition + */ + +export const contactpersonsRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET contactpersons/ +contactpersonsRouter.get('/', async (req: Request, res: Response) => { + try { + const contacts: Contact_Persons = await ContactPersonService.findAll(); + + res.status(200).send(contacts); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET contactpersons/:id +contactpersonsRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const contact: Contact_Person = await ContactPersonService.find(id); + + res.status(200).send(contact); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET contactpersons/byvendor/:id +contactpersonsRouter.get('/byvendor/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const contacts: Contact_Persons = await ContactPersonService.findByVendor(id); + + res.status(200).send(contacts); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST contactpersons/ +contactpersonsRouter.post('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const vendor_id = req.body.vendor_id; + const first_name = req.body.first_name; + const last_name = req.body.last_name; + const gender = req.body.gender; + const email = req.body.email; + const phone = req.body.phone; + + const success = await ContactPersonService.createContactEntry(user.user_id, vendor_id, first_name, last_name, gender, email, phone); + + if (success) { + res.status(201).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT contactpersons/:id +contactpersonsRouter.put('/:id', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const contact_person_id = parseInt(req.params.id, 10); + const vendor_id = req.body.vendor_id; + const first_name = req.body.first_name; + const last_name = req.body.last_name; + const gender = req.body.gender; + const email = req.body.email; + const phone = req.body.phone; + + const success = await ContactPersonService.updateContactEntry(user.user_id, contact_person_id, vendor_id, first_name, last_name, gender, email, phone); + + if (success) { + res.status(200).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/contact_persons/contact_persons.service.ts b/src/models/betterzon/contact_persons/contact_persons.service.ts new file mode 100644 index 0000000..f302bba --- /dev/null +++ b/src/models/betterzon/contact_persons/contact_persons.service.ts @@ -0,0 +1,174 @@ +import * as dotenv from 'dotenv'; +import {Contact_Person} from './contact_person.interface'; +import {Contact_Persons} from './contact_persons.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known contact persons + */ +export const findAll = async (): Promise => { + let conn; + let contRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons'); + for (let row in rows) { + if (row !== 'meta') { + contRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return contRows; +}; + +/** + * Fetches and returns the contact person with the specified id + * @param id The id of the contact person to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let cont: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons WHERE contact_person_id = ?', id); + for (let row in rows) { + if (row !== 'meta') { + cont = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return cont; +}; + +/** + * Fetches and returns the contact persons for the specified vendor + * @param id The id of the vendor to fetch contact persons for + */ +export const findByVendor = async (id: number): Promise => { + let conn; + let contRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT contact_person_id, first_name, last_name, gender, email, phone, vendor_id FROM contact_persons WHERE vendor_id = ?', id); + for (let row in rows) { + if (row !== 'meta') { + contRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return contRows; +}; + +/** + * Creates a contact entry record + * @param user_id The user id of the issuing user + * @param vendor_id The vendor id of the vendor to create the record for + * @param first_name The first name of the contact person + * @param last_name The last name of the contact person + * @param gender The gender of the contact person + * @param email The email of the contact person + * @param phone The phone number of the contact person + */ +export const createContactEntry = async (user_id: number, vendor_id: number, first_name: string, last_name: string, gender: string, email: string, phone: string): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Check if the user is authorized to manage the requested vendor + const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]); + if (user_vendor_rows.length !== 1) { + return false; + } + + // Create contact person entry + const res = await conn.query('INSERT INTO contact_persons (first_name, last_name, gender, email, phone, vendor_id) VALUES (?, ?, ?, ?, ?, ?)', [first_name, last_name, gender, email, phone, vendor_id]); + + // If there are more / less than 1 affected rows, return false + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Updates a contact entry record + * @param user_id The user id of the issuing user + * @param contact_person_id The id of the record to update + * @param vendor_id The vendor id of the vendor to create the record for + * @param first_name The first name of the contact person + * @param last_name The last name of the contact person + * @param gender The gender of the contact person + * @param email The email of the contact person + * @param phone The phone number of the contact person + */ +export const updateContactEntry = async (user_id: number, contact_person_id: number, vendor_id: number, first_name: string, last_name: string, gender: string, email: string, phone: string): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Check if the user is authorized to manage the requested vendor + const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]); + if (user_vendor_rows.length !== 1) { + return false; + } + + // Create contact person entry + const res = await conn.query('UPDATE contact_persons SET first_name = ?, last_name = ?, gender = ?, email = ?, phone = ? WHERE contact_person_id = ? AND vendor_id = ?', [first_name, last_name, gender, email, phone, contact_person_id, vendor_id]); + + // If there are more / less than 1 affected rows, return false + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; diff --git a/src/models/betterzon/crawling_status/crawling_status.interface.ts b/src/models/betterzon/crawling_status/crawling_status.interface.ts new file mode 100644 index 0000000..14def6a --- /dev/null +++ b/src/models/betterzon/crawling_status/crawling_status.interface.ts @@ -0,0 +1,7 @@ +export interface Crawling_Status { + process_id: number; + started_timestamp: Date; + combinations_to_crawl: number; + successful_crawls: number; + failed_crawls: number; +} diff --git a/src/models/betterzon/crawling_status/crawling_status.router.ts b/src/models/betterzon/crawling_status/crawling_status.router.ts new file mode 100644 index 0000000..f2d7dfd --- /dev/null +++ b/src/models/betterzon/crawling_status/crawling_status.router.ts @@ -0,0 +1,44 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as CrawlingStatusService from './crawling_status.service'; +import {Crawling_Status} from './crawling_status.interface'; +import {Crawling_Statuses} from './crawling_statuses.interface'; +import * as UserService from '../users/users.service'; + + +/** + * Router Definition + */ + +export const crawlingstatusRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET crawlingstatus/ +crawlingstatusRouter.get('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + if (!user.is_admin) { + res.status(403).send({}); + return; + } + + const status: Crawling_Status = await CrawlingStatusService.getCurrent(); + + res.status(200).send(status); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/crawling_status/crawling_status.service.ts b/src/models/betterzon/crawling_status/crawling_status.service.ts new file mode 100644 index 0000000..c41c08c --- /dev/null +++ b/src/models/betterzon/crawling_status/crawling_status.service.ts @@ -0,0 +1,73 @@ +import * as dotenv from 'dotenv'; +import {Crawling_Status} from './crawling_status.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns the current crawling status if the issuing user is an admin + */ +export const getCurrent = async (): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Get the current crawling process + let process_info = { + process_id: -1, + started_timestamp: new Date(), + combinations_to_crawl: -1 + }; + const process = await conn.query('SELECT process_id, started_timestamp, combinations_to_crawl FROM crawling_processes ORDER BY started_timestamp DESC LIMIT 1'); + for (let row in process) { + if (row !== 'meta') { + process_info = process[row]; + } + } + + // Get the current status + let total_crawls = 0; + let successful_crawls = 0; + const rows = await conn.query('SELECT COUNT(status_id) as total, SUM(success) as successful FROM crawling_status WHERE process_id = ?', process_info.process_id); + for (let row in rows) { + if (row !== 'meta') { + total_crawls = rows[row].total; + successful_crawls = rows[row].successful; + } + } + + const failed_crawls = total_crawls - successful_crawls; + + return { + process_id: process_info.process_id, + started_timestamp: process_info.started_timestamp, + combinations_to_crawl: process_info.combinations_to_crawl, + successful_crawls: successful_crawls, + failed_crawls: failed_crawls + }; + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; diff --git a/src/models/betterzon/crawling_status/crawling_statuses.interface.ts b/src/models/betterzon/crawling_status/crawling_statuses.interface.ts new file mode 100644 index 0000000..10815d1 --- /dev/null +++ b/src/models/betterzon/crawling_status/crawling_statuses.interface.ts @@ -0,0 +1,5 @@ +import {Crawling_Status} from './crawling_status.interface'; + +export interface Crawling_Statuses { + [key: number]: Crawling_Status; +} diff --git a/src/models/betterzon/favorite_shops/favoriteshop.interface.ts b/src/models/betterzon/favorite_shops/favoriteshop.interface.ts new file mode 100644 index 0000000..71652b1 --- /dev/null +++ b/src/models/betterzon/favorite_shops/favoriteshop.interface.ts @@ -0,0 +1,5 @@ +export interface FavoriteShop { + favorite_id: number; + vendor_id: number; + user_id: number; +} diff --git a/src/models/betterzon/favorite_shops/favoriteshops.interface.ts b/src/models/betterzon/favorite_shops/favoriteshops.interface.ts new file mode 100644 index 0000000..b921b0d --- /dev/null +++ b/src/models/betterzon/favorite_shops/favoriteshops.interface.ts @@ -0,0 +1,5 @@ +import {FavoriteShop} from './favoriteshop.interface'; + +export interface FavoriteShops { + [key: number]: FavoriteShop; +} diff --git a/src/models/betterzon/favorite_shops/favoriteshops.router.ts b/src/models/betterzon/favorite_shops/favoriteshops.router.ts new file mode 100644 index 0000000..eab15bd --- /dev/null +++ b/src/models/betterzon/favorite_shops/favoriteshops.router.ts @@ -0,0 +1,106 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as FavoriteShopsService from './favoriteshops.service'; +import {FavoriteShop} from './favoriteshop.interface'; +import {FavoriteShops} from './favoriteshops.interface'; +import * as UserService from '../users/users.service'; + + +/** + * Router Definition + */ +export const favoriteshopsRouter = express.Router(); + + +/** + * Controller Definitions + */ + +//GET favoriteshops/ +favoriteshopsRouter.get('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + const priceAlarms = await FavoriteShopsService.getFavoriteShops(user.user_id); + + res.status(200).send(priceAlarms); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST favoriteshops/ +favoriteshopsRouter.post('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get info for price alarm creation + const vendor_id = req.body.vendor_id; + + if (!vendor_id) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Create price alarm + const success = await FavoriteShopsService.createFavoriteShop(user.user_id, vendor_id); + + if (success) { + res.status(201).send(JSON.stringify({success: true})); + return; + } else { + res.status(500).send(JSON.stringify({success: false})); + return; + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// DELETE favoriteshops/ +favoriteshopsRouter.delete('/:id', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get info for price alarm creation + const favorite_id = parseInt(req.params.id, 10); + + if (!favorite_id) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Create price alarm + const success = await FavoriteShopsService.deleteFavoriteShop(user.user_id, favorite_id); + + if (success) { + res.status(201).send(JSON.stringify({success: true})); + return; + } else { + res.status(500).send(JSON.stringify({success: false})); + return; + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/favorite_shops/favoriteshops.service.ts b/src/models/betterzon/favorite_shops/favoriteshops.service.ts new file mode 100644 index 0000000..732d209 --- /dev/null +++ b/src/models/betterzon/favorite_shops/favoriteshops.service.ts @@ -0,0 +1,90 @@ +import * as dotenv from 'dotenv'; +import {FavoriteShops} from './favoriteshops.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Creates a favorite shop entry for the given user for the given shop + * @param user_id The id of the user to create the favorite shop entry for + * @param vendor_id The id of the vendor to set as favorite + */ +export const createFavoriteShop = async (user_id: number, vendor_id: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + const res = await conn.query('INSERT INTO favorite_shops (vendor_id, user_id) VALUES (?, ?)', [vendor_id, user_id]); + + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Fetches and returns all favorite shops for the given user + * @param user_id + */ +export const getFavoriteShops = async (user_id: number): Promise => { + let conn; + let shops = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT favorite_id, vendor_id, user_id FROM favorite_shops WHERE user_id = ?', user_id); + for (let row in rows) { + if (row !== 'meta') { + shops.push(rows[row]); + } + } + + return shops; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Deletes the given favorite shop entry + * @param user_id The id of the user that wants to delete the favorite shop entry + * @param favorite_id The favorite shop to delete + */ +export const deleteFavoriteShop = async (user_id: number, favorite_id: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + const res = await conn.query('DELETE FROM favorite_shops WHERE favorite_id = ? AND user_id = ?', [favorite_id, user_id]); + + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; diff --git a/src/models/betterzon/manufacturers/manufacturer.interface.ts b/src/models/betterzon/manufacturers/manufacturer.interface.ts new file mode 100644 index 0000000..08e8ea8 --- /dev/null +++ b/src/models/betterzon/manufacturers/manufacturer.interface.ts @@ -0,0 +1,4 @@ +export interface Manufacturer { + manufacturer_id: number; + name: string; +} diff --git a/src/models/betterzon/manufacturers/manufacturers.interface.ts b/src/models/betterzon/manufacturers/manufacturers.interface.ts new file mode 100644 index 0000000..94836e2 --- /dev/null +++ b/src/models/betterzon/manufacturers/manufacturers.interface.ts @@ -0,0 +1,5 @@ +import {Manufacturer} from './manufacturer.interface'; + +export interface Manufacturers { + [key: number]: Manufacturer; +} diff --git a/src/models/betterzon/manufacturers/manufacturers.router.ts b/src/models/betterzon/manufacturers/manufacturers.router.ts new file mode 100644 index 0000000..99b0875 --- /dev/null +++ b/src/models/betterzon/manufacturers/manufacturers.router.ts @@ -0,0 +1,70 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as ManufacturerService from './manufacturers.service'; +import {Manufacturer} from './manufacturer.interface'; +import {Manufacturers} from './manufacturers.interface'; + + +/** + * Router Definition + */ + +export const manufacturersRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET manufacturers/ +manufacturersRouter.get('/', async (req: Request, res: Response) => { + try { + const manufacturers: Manufacturers = await ManufacturerService.findAll(); + + res.status(200).send(manufacturers); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET manufacturers/:id +manufacturersRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const manufacturer: Manufacturer = await ManufacturerService.find(id); + + res.status(200).send(manufacturer); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET manufacturers/:term +manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => { + const term: string = req.params.term; + + if (!term) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const manufacturer: Manufacturers = await ManufacturerService.findBySearchTerm(term); + + res.status(200).send(manufacturer); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/manufacturers/manufacturers.service.ts b/src/models/betterzon/manufacturers/manufacturers.service.ts new file mode 100644 index 0000000..a49b5cf --- /dev/null +++ b/src/models/betterzon/manufacturers/manufacturers.service.ts @@ -0,0 +1,112 @@ +import * as dotenv from 'dotenv'; +import {Manufacturer} from './manufacturer.interface'; +import {Manufacturers} from './manufacturers.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known manufacturers + */ +export const findAll = async (): Promise => { + let conn; + let manRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT manufacturer_id, name FROM manufacturers'); + for (let row in rows) { + if (row !== 'meta') { + let man: Manufacturer = { + manufacturer_id: 0, + name: '' + }; + const sqlMan = rows[row]; + + man.manufacturer_id = sqlMan.manufacturer_id; + man.name = sqlMan.name; + manRows.push(man); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return manRows; +}; + +/** + * Fetches and returns the manufacturer with the specified id + * @param id The id of the manufacturer to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let man: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT manufacturer_id, name FROM manufacturers WHERE manufacturer_id = ?', id); + for (let row in rows) { + if (row !== 'meta') { + man = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return man; +}; + +/** + * Fetches and returns all manufacturers that match the search term + * @param term the term to match + */ +export const findBySearchTerm = async (term: string): Promise => { + let conn; + let manRows = []; + try { + conn = await pool.getConnection(); + term = '%' + term + '%'; + const rows = await conn.query('SELECT manufacturer_id, name FROM manufacturers WHERE name LIKE ?', term); + for (let row in rows) { + if (row !== 'meta') { + manRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return manRows; +}; diff --git a/src/models/betterzon/pricealarms/pricealarm.interface.ts b/src/models/betterzon/pricealarms/pricealarm.interface.ts new file mode 100644 index 0000000..c8a1717 --- /dev/null +++ b/src/models/betterzon/pricealarms/pricealarm.interface.ts @@ -0,0 +1,6 @@ +export interface PriceAlarm { + alarm_id: number; + user_id: number; + product_id: number; + defined_price: number; +} diff --git a/src/models/betterzon/pricealarms/pricealarms.interface.ts b/src/models/betterzon/pricealarms/pricealarms.interface.ts new file mode 100644 index 0000000..c1dcbbd --- /dev/null +++ b/src/models/betterzon/pricealarms/pricealarms.interface.ts @@ -0,0 +1,5 @@ +import {PriceAlarm} from './pricealarm.interface'; + +export interface PriceAlarms { + [key: number]: PriceAlarm; +} diff --git a/src/models/betterzon/pricealarms/pricealarms.router.ts b/src/models/betterzon/pricealarms/pricealarms.router.ts new file mode 100644 index 0000000..8e0114a --- /dev/null +++ b/src/models/betterzon/pricealarms/pricealarms.router.ts @@ -0,0 +1,134 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as PriceAlarmsService from './pricealarms.service'; +import {PriceAlarm} from './pricealarm.interface'; +import {PriceAlarms} from './pricealarms.interface'; +import * as UserService from '../users/users.service'; + + +/** + * Router Definition + */ +export const pricealarmsRouter = express.Router(); + + +/** + * Controller Definitions + */ + +//GET pricealarms/ +pricealarmsRouter.get('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id); + + res.status(200).send(priceAlarms); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST pricealarms/ +pricealarmsRouter.post('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get info for price alarm creation + const product_id = req.body.product_id; + const defined_price = req.body.defined_price; + + if (!product_id || !defined_price) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Create price alarm + const success = await PriceAlarmsService.createPriceAlarm(user.user_id, product_id, defined_price); + + if (success) { + res.status(201).send(JSON.stringify({success: true})); + return; + } else { + res.status(500).send(JSON.stringify({success: false})); + return; + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT pricealarms/ +pricealarmsRouter.put('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get info for price alarm creation + const alarm_id = req.body.alarm_id; + const defined_price = req.body.defined_price; + + if (!alarm_id || !defined_price) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Update price alarm + const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_id, defined_price); + + if (success) { + res.status(200).send(JSON.stringify({success: true})); + return; + } else { + res.status(500).send(JSON.stringify({success: false})); + return; + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// DELETE pricealarms/:id +pricealarmsRouter.delete('/:id', async (req, res) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + const id: number = parseInt(req.params.id, 10); + + const success = await PriceAlarmsService.deletePriceAlarm(id, user.user_id); + + if (success) { + res.status(200).send(JSON.stringify({success: true})); + return; + } else { + res.status(500).send(JSON.stringify({success: false})); + return; + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/pricealarms/pricealarms.service.ts b/src/models/betterzon/pricealarms/pricealarms.service.ts new file mode 100644 index 0000000..720d6e0 --- /dev/null +++ b/src/models/betterzon/pricealarms/pricealarms.service.ts @@ -0,0 +1,113 @@ +import * as dotenv from 'dotenv'; +import {PriceAlarms} from './pricealarms.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Creates a price alarm for the given user for the product with the defined price + * @param user_id The id of the user to create the price alarm for + * @param product_id The id of the product to create the price alarm for + * @param defined_price The defined price for the price alarm + */ +export const createPriceAlarm = async (user_id: number, product_id: number, defined_price: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + const res = await conn.query('INSERT INTO price_alarms (user_id, product_id, defined_price) VALUES (?, ?, ?)', [user_id, product_id, defined_price]); + + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Fetches and returns all price alarms for the given user + * @param user_id + */ +export const getPriceAlarms = async (user_id: number): Promise => { + let conn; + let priceAlarms = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT alarm_id, user_id, product_id, defined_price FROM price_alarms WHERE user_id = ?', user_id); + for (let row in rows) { + if (row !== 'meta') { + priceAlarms.push(rows[row]); + } + } + + return priceAlarms; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Updates the given price alarm with the given fields + * @param alarm_id The id of the price alarm to update + * @param user_id The id of the user that wants to update the price alarm + * @param defined_price The defined price for the price alarm + */ +export const updatePriceAlarm = async (alarm_id: number, user_id: number, defined_price: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + const res = await conn.query('UPDATE price_alarms SET defined_price = ? WHERE alarm_id = ? AND user_id = ?', [defined_price, alarm_id, user_id]); + + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Deletes the given price alarm + * @param alarm_id The id of the price alarm to update + * @param user_id The id of the user that wants to update the price alarm + */ +export const deletePriceAlarm = async (alarm_id: number, user_id: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + const res = await conn.query('DELETE FROM price_alarms WHERE alarm_id = ? AND user_id = ?', [alarm_id, user_id]); + + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; diff --git a/src/models/betterzon/prices/price.interface.ts b/src/models/betterzon/prices/price.interface.ts new file mode 100644 index 0000000..702015a --- /dev/null +++ b/src/models/betterzon/prices/price.interface.ts @@ -0,0 +1,27 @@ +export interface Price { + price_id: number; + product_id: number; + vendor_id: number; + price_in_cents: number; + timestamp: Date; +} + +export class Deal implements Price { + price_id: number; + product_id: number; + vendor_id: number; + price_in_cents: number; + timestamp: Date; + amazonDifference: number; + amazonDifferencePercent: number; + + constructor(price_id: number, product_id: number, vendor_id: number, price_in_cents: number, timestamp: Date, amazonDifference: number, amazonDifferencePercent: number) { + this.price_id = price_id; + this.product_id = product_id; + this.vendor_id = vendor_id; + this.price_in_cents = price_in_cents; + this.timestamp = timestamp; + this.amazonDifference = amazonDifference; + this.amazonDifferencePercent = amazonDifferencePercent; + } +} diff --git a/src/models/betterzon/prices/prices.interface.ts b/src/models/betterzon/prices/prices.interface.ts new file mode 100644 index 0000000..9469832 --- /dev/null +++ b/src/models/betterzon/prices/prices.interface.ts @@ -0,0 +1,5 @@ +import {Price} from './price.interface'; + +export interface Prices { + [key: number]: Price; +} diff --git a/src/models/betterzon/prices/prices.router.ts b/src/models/betterzon/prices/prices.router.ts new file mode 100644 index 0000000..61ed812 --- /dev/null +++ b/src/models/betterzon/prices/prices.router.ts @@ -0,0 +1,130 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as PriceService from './prices.service'; +import {Price} from './price.interface'; +import {Prices} from './prices.interface'; +import * as UserService from '../users/users.service'; + + +/** + * Router Definition + */ + +export const pricesRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET prices/ +pricesRouter.get('/', async (req: Request, res: Response) => { + try { + let prices: Prices = []; + const product = req.query.product; + const vendor = req.query.vendor; + const type = req.query.type; + + if (product) { + if (vendor) { + prices = await PriceService.findByVendor( product, vendor, type); + } else { + prices = await PriceService.findByType( product, type); + } + } else { + prices = await PriceService.findAll(); + } + + res.status(200).send(prices); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET prices/:id +pricesRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const price: Price = await PriceService.find(id); + + res.status(200).send(price); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET prices/bestDeals +pricesRouter.get('/bestDeals/:amount', async (req: Request, res: Response) => { + const amount: number = parseInt(req.params.amount, 10); + + if (!amount) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const prices: Prices = await PriceService.getBestDeals(amount); + + res.status(200).send(prices); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// 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) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST prices/ +pricesRouter.post('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const vendor_id = req.body.vendor_id; + const product_id = req.body.product_id; + const price_in_cents = req.body.price_in_cents; + + const success = await PriceService.createPriceEntry(user.user_id, vendor_id, product_id, price_in_cents); + + if (success) { + res.status(201).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/prices/prices.service.ts b/src/models/betterzon/prices/prices.service.ts new file mode 100644 index 0000000..6de0047 --- /dev/null +++ b/src/models/betterzon/prices/prices.service.ts @@ -0,0 +1,401 @@ +import * as dotenv from 'dotenv'; +import {Deal, Price} from './price.interface'; +import {Prices} from './prices.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known prices + */ +export const findAll = async (): Promise => { + let conn; + let priceRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT price_id, product_id, v.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE active_listing = true AND v.isActive = true'); + for (let row in rows) { + if (row !== 'meta') { + let price: Price = { + price_id: 0, + price_in_cents: 0, + product_id: 0, + timestamp: new Date(), + vendor_id: 0 + }; + const sqlPrice = rows[row]; + + price.price_id = sqlPrice.price_id; + price.product_id = sqlPrice.product_id; + price.vendor_id = sqlPrice.vendor_id; + price.price_in_cents = sqlPrice.price_in_cents; + price.timestamp = sqlPrice.timestamp; + priceRows.push(price); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return priceRows; +}; + +/** + * Fetches and returns the price with the specified id + * @param id The id of the price to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let price: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE price_id = ? AND active_listing = true AND v.isActive = true', id); + for (let row in rows) { + if (row !== 'meta') { + price = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return price; +}; + +/** + * Fetches and returns all prices that belong to the specified product + * @param product the product to fetch the prices for + */ +export const findByProduct = async (product: number): Promise => { + let conn; + let priceRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND active_listing = true AND v.isActive = true', product); + for (let row in rows) { + if (row !== 'meta') { + priceRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return priceRows; +}; + +/** + * Fetches and returns prices that belong to the specified product. + * If type is newest, only the newest prices for each vendor will be returned. + * If type is lowest, the lowest daily price for the product is returned. + * Otherwise, all prices for this product are returned. + * @param product The product to fetch the prices for + * @param type The type of prices, e.g. newest / lowest + */ +export const findByType = async (product: string, type: string): Promise => { + let conn; + let priceRows = []; + try { + conn = await pool.getConnection(); + let rows = []; + if (type === 'newest') { + // Used to get the newest price for this product per vendor + rows = await conn.query(('WITH summary AS ( ' + + 'SELECT p.product_id, ' + + 'p.vendor_id, ' + + 'p.price_in_cents, ' + + 'p.timestamp, ' + + 'ROW_NUMBER() OVER( ' + + 'PARTITION BY p.vendor_id ' + + 'ORDER BY p.timestamp DESC) AS rk ' + + 'FROM prices p ' + + 'LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id ' + + 'WHERE product_id = ? AND p.vendor_id != 1 AND active_listing = true AND v.isActive = true) ' + + 'SELECT s.* ' + + 'FROM summary s ' + + 'WHERE s.rk = 1 '), product); + } else if (type === 'lowest') { + // Used to get the lowest prices for this product over a period of time + rows = await conn.query('SELECT price_id, product_id, p.vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND v.vendor_id != 1 AND active_listing = true AND v.isActive = true GROUP BY DAY(timestamp) ORDER BY timestamp', product); + } else { + // If no type is given, return all prices for this product + rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id != 1 AND active_listing = true AND v.isActive = true', product); + } + + for (let row in rows) { + if (row !== 'meta') { + priceRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return priceRows; +}; + +/** + * Fetches and returns prices that belong to the specified product and vendor. + * If type is newest, only the newest known price for the product at the vendor is returned. + * If type is lowest, only the lowest ever known price for the product at the vendor is returned. + * Otherwise, all prices for this product are returned. + * @param product The product to fetch the prices for + * @param vendor The vendor to fetch the prices for + * @param type The type of prices, e.g. newest / lowest + */ +export const findByVendor = async (product: string, vendor: string, type: string): Promise => { + let conn; + let priceRows = []; + try { + conn = await pool.getConnection(); + let rows = []; + if (type === 'newest') { + // Used to get the newest price for this product and vendor + rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true ORDER BY timestamp DESC LIMIT 1', [product, vendor]); + } else if (type === 'lowest') { + // Used to get the lowest prices for this product and vendor in all time + rows = await conn.query('SELECT price_id, product_id, p.vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true LIMIT 1', [product, vendor]); + } else { + // If no type is given, return all prices for this product and vendor + rows = await conn.query('SELECT price_id, product_id, p.vendor_id, price_in_cents, timestamp FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE product_id = ? AND p.vendor_id = ? AND active_listing = true AND v.isActive = true', [product, vendor]); + } + + for (let row in rows) { + if (row !== 'meta') { + priceRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return priceRows; +}; + +/** + * Fetches and returns the best current deals, i.e. the non-amazon prices that have the biggest difference to amazon prices. + * Only the latest known prices for every vendor are taken into consideration so we only get up-to-date-deals. + * @param amount The amount of deals to return + */ +export const getBestDeals = async (amount: number): Promise => { + let conn; + let priceRows = []; + try { + conn = await pool.getConnection(); + + let allPrices: Record = {}; + + // Get newest prices for every 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 LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id WHERE active_listing = true AND v.isActive = true)\n' + + 'SELECT s.*\n' + + 'FROM summary s\n' + + 'WHERE s.rk = 1'); + + // 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 prices to find the products with the biggest difference between amazon and other vendor + let deals: Deal[] = []; + + Object.keys(allPrices).forEach(productId => { + if (allPrices[parseInt(productId)]) { + let pricesForProd = allPrices[parseInt(productId)]; + + // Get amazon price and lowest price from other vendor + let amazonPrice = {} as Price; + let lowestPrice = {} as Price; + pricesForProd.forEach(function (price, priceIndex) { + if (price.vendor_id === 1) { + amazonPrice = price; + } 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) { + lowestPrice = price; + } + } + }); + + // Create deal object and add it to list + let deal = { + 'product_id': lowestPrice.product_id, + 'vendor_id': lowestPrice.vendor_id, + 'price_in_cents': lowestPrice.price_in_cents, + 'timestamp': lowestPrice.timestamp, + 'amazonDifference': (amazonPrice.price_in_cents - lowestPrice.price_in_cents), + 'amazonDifferencePercent': ((amazonPrice.price_in_cents / lowestPrice.price_in_cents) * 100) + }; + + // Push only deals were the amazon price is actually higher + if (deal.amazonDifferencePercent > 0 && deal.amazonDifference > 0) { + deals.push(deal as Deal); + } + } + }); + + // Sort to have the best deals on the top + 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 + let maxAmt = Math.min(amount, deals.length); + + for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) { + priceRows.push(deals[dealIndex] as Price); + } + } catch (err) { + console.log(err); + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return priceRows; +}; + +/** + * Fetches and returns the lowest, latest, non-amazon price for each given product + * @param productIds the ids of the products + */ +export const findListByProducts = async (productIds: [number]): Promise => { + let conn; + let priceRows: Price[] = []; + try { + conn = await pool.getConnection(); + + let allPrices: Record = {}; + + // 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 LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id ' + + ' WHERE p.product_id IN (?) AND v.isActive = true' + + ' AND p.vendor_id != 1 AND active_listing = true)\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 createPriceEntry = async (user_id: number, vendor_id: number, product_id: number, price_in_cents: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Check if the user is authorized to manage the requested vendor + const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]); + if (user_vendor_rows.length !== 1) { + return false; + } + + // Create price entry + const res = await conn.query('INSERT INTO prices (product_id, vendor_id, price_in_cents) VALUES (?,?,?)', [product_id, vendor_id, price_in_cents]); + + // If there are more / less than 1 affected rows, return false + return res.affectedRows === 1; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; diff --git a/src/models/betterzon/products/product.interface.ts b/src/models/betterzon/products/product.interface.ts new file mode 100644 index 0000000..8c51860 --- /dev/null +++ b/src/models/betterzon/products/product.interface.ts @@ -0,0 +1,14 @@ +export interface Product { + product_id: number; + asin: string; + is_active: boolean; + name: string; + short_description: string; + long_description: string; + image_guid: string; + date_added: Date; + last_modified: Date; + manufacturer_id: number; + selling_rank: string; + category_id: number; +} diff --git a/src/models/betterzon/products/products.interface.ts b/src/models/betterzon/products/products.interface.ts new file mode 100644 index 0000000..00b5e36 --- /dev/null +++ b/src/models/betterzon/products/products.interface.ts @@ -0,0 +1,5 @@ +import {Product} from './product.interface'; + +export interface Products { + [key: number]: Product; +} diff --git a/src/models/betterzon/products/products.router.ts b/src/models/betterzon/products/products.router.ts new file mode 100644 index 0000000..a30e8ef --- /dev/null +++ b/src/models/betterzon/products/products.router.ts @@ -0,0 +1,131 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as ProductService from './products.service'; +import {Product} from './product.interface'; +import {Products} from './products.interface'; + + +/** + * Router Definition + */ + +export const productsRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET products/ +productsRouter.get('/', async (req: Request, res: Response) => { + try { + const products: Products = await ProductService.findAll(); + + res.status(200).send(products); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET products/:id +productsRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const product: Product = await ProductService.find(id); + + res.status(200).send(product); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET products/search/:term +productsRouter.get('/search/:term', async (req: Request, res: Response) => { + const term: string = req.params.term; + + if (!term) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const products: Products = await ProductService.findBySearchTerm(term); + + res.status(200).send(products); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET products/list/[1,2,3] +productsRouter.get('/list/:ids', async (req: Request, res: Response) => { + const ids: [number] = JSON.parse(req.params.ids); + + if (!ids) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const products: Products = await ProductService.findList(ids); + + res.status(200).send(products); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET products/vendor/:id +productsRouter.get('/vendor/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const products: Products = await ProductService.findByVendor(id); + + res.status(200).send(products); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST products/ +productsRouter.post('/', async (req: Request, res: Response) => { + const asin: string = req.body.asin; + + if (!asin) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const result: boolean = await ProductService.addNewProduct(asin); + + if (result) { + res.status(201).send({}); + } else { + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/products/products.service.ts b/src/models/betterzon/products/products.service.ts new file mode 100644 index 0000000..ba26473 --- /dev/null +++ b/src/models/betterzon/products/products.service.ts @@ -0,0 +1,228 @@ +import * as dotenv from 'dotenv'; +import {Product} from './product.interface'; +import {Products} from './products.interface'; +import * as http from 'http'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known products + */ +export const findAll = async (): Promise => { + let conn; + let prodRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products'); + for (let row in rows) { + if (row !== 'meta') { + let prod: Product = { + asin: '', + category_id: 0, + date_added: new Date(), + image_guid: '', + is_active: false, + last_modified: new Date(), + long_description: '', + manufacturer_id: 0, + name: '', + product_id: 0, + selling_rank: '', + short_description: '' + }; + const sqlProd = rows[row]; + + prod.product_id = sqlProd.product_id; + prod.name = sqlProd.name; + prod.asin = sqlProd.asin; + prod.is_active = sqlProd.is_active; + prod.short_description = sqlProd.short_description; + prod.long_description = sqlProd.long_description; + prod.image_guid = sqlProd.image_guid; + prod.date_added = sqlProd.date_added; + prod.last_modified = sqlProd.last_modified; + prod.manufacturer_id = sqlProd.manufacturer_id; + prod.selling_rank = sqlProd.selling_rank; + prod.category_id = sqlProd.category_id; + prodRows.push(prod); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return prodRows; +}; + +/** + * Fetches and returns the product with the specified id + * @param id The id of the product to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let prod: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products WHERE product_id = ?', id); + for (let row in rows) { + if (row !== 'meta') { + prod = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return prod; +}; + +/** + * Fetches and returns all products that match the search term + * @param term the term to match + */ +export const findBySearchTerm = async (term: string): Promise => { + let conn; + let prodRows = []; + try { + conn = await pool.getConnection(); + term = '%' + term + '%'; + const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products WHERE name LIKE ?', term); + for (let row in rows) { + if (row !== 'meta') { + prodRows.push(rows[row]); + } + } + + } catch (err) { + console.log(err); + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return prodRows; +}; + +/** + * Fetches and returns the product details for the given list of product ids + * @param ids The list of product ids to fetch the details for + */ +export const findList = async (ids: [number]): Promise => { + let conn; + let prodRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products WHERE product_id IN (?)', [ids]); + for (let row in rows) { + if (row !== 'meta') { + prodRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return prodRows; +}; + +/** + * Fetches and returns the products that the given vendor has price entries for + * @param id The id of the vendor to fetch the products for + */ +export const findByVendor = async (id: number): Promise => { + let conn; + let prodRows = []; + try { + conn = await pool.getConnection(); + + // Get the relevant product ids + let relevant_prod_ids = []; + const relevantProds = await conn.query('SELECT product_id FROM prices WHERE vendor_id = ? GROUP BY product_id', id); + for (let row in relevantProds) { + if (row !== 'meta') { + relevant_prod_ids.push(relevantProds[row].product_id); + } + } + + // Fetch products + const rows = await conn.query('SELECT product_id, name, asin, is_active, short_description, long_description, image_guid, date_added, last_modified, manufacturer_id, selling_rank, category_id FROM products WHERE product_id IN (?)', [relevant_prod_ids]); + for (let row in rows) { + if (row !== 'meta') { + prodRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return prodRows; +}; + +/** + * Makes a callout to a crawler instance to search for the requested product + * @param asin The amazon asin of the product to look for + */ +export const addNewProduct = async (asin: string): Promise => { + try { + let options = { + host: 'crawl.p4ddy.com', + path: '/searchNew', + port: '443', + method: 'POST' + }; + + let req = http.request(options, res => { + return res.statusCode === 202; + }); + req.write(JSON.stringify({ + asin: asin, + key: process.env.CRAWLER_ACCESS_KEY + })); + req.end(); + } catch (err) { + console.log(err); + throw(err); + } + + return false; +}; diff --git a/src/models/betterzon/users/session.interface.ts b/src/models/betterzon/users/session.interface.ts new file mode 100644 index 0000000..4b68e9e --- /dev/null +++ b/src/models/betterzon/users/session.interface.ts @@ -0,0 +1,10 @@ +export interface Session { + session_id: number; + session_key: string; + session_key_hash: string; + createdDate?: Date; + lastLogin?: Date; + validUntil?: Date; + validDays?: number; + last_IP: string; +} diff --git a/src/models/betterzon/users/user.interface.ts b/src/models/betterzon/users/user.interface.ts new file mode 100644 index 0000000..fbbe5a6 --- /dev/null +++ b/src/models/betterzon/users/user.interface.ts @@ -0,0 +1,9 @@ +export interface User { + user_id: number; + username: string; + email: string; + password_hash: string; + registration_date: Date; + last_login_date: Date; + is_admin: boolean; +} diff --git a/src/models/betterzon/users/users.interface.ts b/src/models/betterzon/users/users.interface.ts new file mode 100644 index 0000000..9a81dcf --- /dev/null +++ b/src/models/betterzon/users/users.interface.ts @@ -0,0 +1,5 @@ +import {User} from './user.interface'; + +export interface Users { + [key: number]: User; +} diff --git a/src/models/betterzon/users/users.router.ts b/src/models/betterzon/users/users.router.ts new file mode 100644 index 0000000..7851ef7 --- /dev/null +++ b/src/models/betterzon/users/users.router.ts @@ -0,0 +1,121 @@ +/** + * 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_id: session.session_id, + session_key: session.session_key + }); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST users/login +usersRouter.post('/login', async (req: Request, res: Response) => { + try { + const username: string = req.body.username; + const password: string = req.body.password; + const ip: string = req.connection.remoteAddress ?? ''; + + if (!username || !password) { + // Missing + res.status(400).send(JSON.stringify({message: 'Missing parameters'})); + return; + } + + // Update the user entry and create a session + const session: Session = await UserService.login(username, password, ip); + + if (!session.session_id) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['Wrong username and / or password'], codes: [1, 4]})); + return; + } + + // Send the session details back to the user + res.status(200).send({ + session_id: session.session_id, + session_key: session.session_key + }); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// POST users/checkSessionValid +usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { + try { + const ip: string = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + + if(!session_id || !session_key) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['No session detected'], codes: [5]})); + return; + } + + // Update the user entry and create a session + const user: User = await UserService.checkSession(session_id, session_key, ip); + + if (!user.user_id) { + // Error logging in, probably wrong username / password + res.status(401).send(JSON.stringify({messages: ['Invalid session'], codes: [5]})); + return; + } + + // Send the session details back to the user + res.status(200).send(user); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/users/users.service.ts b/src/models/betterzon/users/users.service.ts new file mode 100644 index 0000000..8eea031 --- /dev/null +++ b/src/models/betterzon/users/users.service.ts @@ -0,0 +1,314 @@ +import * as dotenv from 'dotenv'; +import * as bcrypt from 'bcrypt'; +import {Guid} from 'guid-typescript'; +import {User} from './user.interface'; +import {Session} from './session.interface'; + + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * 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 => { + let conn; + try { + // Hash password and generate + hash session key + const pwHash = bcrypt.hashSync(password, 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 session + 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: 'HIDDEN', + last_IP: ip + }; + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return {} as Session; +}; + +/** + * Checks if the given credentials are valid and creates a new session if they are. + * Returns the session information in case of a successful login + */ +export const login = async (username: string, password: string, ip: string): Promise => { + let conn; + try { + // Get saved password hash + conn = await pool.getConnection(); + const query = 'SELECT user_id, bcrypt_password_hash FROM users WHERE username = ?'; + const userRows = await conn.query(query, username); + let savedHash = ''; + let userId = -1; + for (const row in userRows) { + if (row !== 'meta' && userRows[row].user_id != null) { + savedHash = userRows[row].bcrypt_password_hash; + userId = userRows[row].user_id; + } + } + + // Check for correct password + if (!bcrypt.compareSync(password, savedHash)) { + // Wrong password, return invalid + return {} as Session; + } + // Password is valid, continue + + // Generate + hash session key + const sessionKey = Guid.create().toString(); + const sessionKeyHash = bcrypt.hashSync(sessionKey, 10); + + // Update user entry in SQL + const userQuery = 'UPDATE users SET last_login_date = NOW() WHERE user_id = ?'; + const userIdRes = await conn.query(userQuery, userId); + await conn.commit(); + + // 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 session + 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: 'HIDDEN', + last_IP: ip + }; + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return {} as Session; +}; + +/** + * Checks if the given session information are valid and returns the user information if they are + */ +export const checkSession = async (sessionId: string, sessionKey: string, ip: string): Promise => { + let conn; + try { + // Get saved session key hash + conn = await pool.getConnection(); + const query = 'SELECT user_id, session_key_hash, validUntil FROM sessions WHERE session_id = ?'; + const sessionRows = await conn.query(query, sessionId); + let savedHash = ''; + let userId = -1; + let validUntil = new Date(); + for (const row in sessionRows) { + if (row !== 'meta' && sessionRows[row].user_id != null) { + savedHash = sessionRows[row].session_key_hash; + userId = sessionRows[row].user_id; + validUntil = sessionRows[row].validUntil; + } + } + + // Check for correct key + if (!bcrypt.compareSync(sessionKey, savedHash)) { + // Wrong key, return invalid + return {} as User; + } + // Key is valid, continue + + // Check if the session is still valid + if (validUntil <= new Date()) { + // Session expired, return invalid + return {} as User; + } + // Session still valid, continue + + // Update session entry in SQL + const updateSessionsQuery = 'UPDATE sessions SET lastLogin = NOW(), last_IP = ? WHERE session_id = ?'; + const updateUsersQuery = 'UPDATE users SET last_login_date = NOW() WHERE user_id = ?'; + const userIdRes = await conn.query(updateSessionsQuery, [ip, sessionId]); + await conn.query(updateUsersQuery, userId); + await conn.commit(); + + // Get the other required user information and update the user + const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date, is_admin FROM users WHERE user_id = ?'; + const userRows = await conn.query(userQuery, userId); + let username = ''; + let email = ''; + let registrationDate = new Date(); + let lastLoginDate = new Date(); + let is_admin = false; + for (const row in userRows) { + if (row !== 'meta' && userRows[row].user_id != null) { + username = userRows[row].username; + email = userRows[row].email; + registrationDate = userRows[row].registration_date; + lastLoginDate = userRows[row].last_login_date; + is_admin = userRows[row].is_admin; + } + } + + // Everything is fine, return user information + return { + user_id: userId, + username: username, + email: email, + password_hash: 'HIDDEN', + registration_date: registrationDate, + last_login_date: lastLoginDate, + is_admin: is_admin + }; + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } +}; + +/** + * Calls the checkSession method after extracting the required information from the authentication cookie + * @param cookie The betterauth cookie + * @param ip The users IP address + */ +export const checkSessionWithCookie = async (cookie: any, ip: string): Promise => { + const parsedCookie = JSON.parse(cookie); + const session_id = parsedCookie.id; + const session_key = parsedCookie.key; + + + return checkSession(session_id, session_key, ''); +}; + +/** + * 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, 4 = wrong password, 5 = wrong session +} + +/** + * 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 => { + 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(); + } + } +}; diff --git a/src/models/betterzon/vendors/vendor.interface.ts b/src/models/betterzon/vendors/vendor.interface.ts new file mode 100644 index 0000000..51afc49 --- /dev/null +++ b/src/models/betterzon/vendors/vendor.interface.ts @@ -0,0 +1,10 @@ +export interface Vendor { + vendor_id: number; + name: string; + streetname: string; + zip_code: string; + city: string; + country_code: string; + phone: string; + website: string; +} diff --git a/src/models/betterzon/vendors/vendors.interface.ts b/src/models/betterzon/vendors/vendors.interface.ts new file mode 100644 index 0000000..3d460bf --- /dev/null +++ b/src/models/betterzon/vendors/vendors.interface.ts @@ -0,0 +1,5 @@ +import {Vendor} from './vendor.interface'; + +export interface Vendors { + [key: number]: Vendor; +} diff --git a/src/models/betterzon/vendors/vendors.router.ts b/src/models/betterzon/vendors/vendors.router.ts new file mode 100644 index 0000000..383d6eb --- /dev/null +++ b/src/models/betterzon/vendors/vendors.router.ts @@ -0,0 +1,165 @@ +/** + * Required External Modules and Interfaces + */ + +import express, {Request, Response} from 'express'; +import * as VendorService from './vendors.service'; +import {Vendor} from './vendor.interface'; +import {Vendors} from './vendors.interface'; +import * as UserService from '../users/users.service'; + + +/** + * Router Definition + */ + +export const vendorsRouter = express.Router(); + + +/** + * Controller Definitions + */ + +// GET vendors/ +vendorsRouter.get('/', async (req: Request, res: Response) => { + try { + const vendors: Vendors = await VendorService.findAll(); + + res.status(200).send(vendors); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET vendors/managed +vendorsRouter.get('/managed', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = (req.query.session_id ?? '').toString(); + const session_key = (req.query.session_key ?? '').toString(); + const user = await UserService.checkSession(session_id, session_key, user_ip); + + const vendors = await VendorService.getManagedShops(user.user_id); + + res.status(200).send(vendors); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET vendors/:id +vendorsRouter.get('/:id', async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id, 10); + + if (!id) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const vendor: Vendor = await VendorService.find(id); + + res.status(200).send(vendor); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// GET vendors/search/:term +vendorsRouter.get('/search/:term', async (req: Request, res: Response) => { + const term: string = req.params.term; + + if (!term) { + res.status(400).send('Missing parameters.'); + return; + } + + try { + const vendors: Vendors = await VendorService.findBySearchTerm(term); + + res.status(200).send(vendors); + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT vendors/manage/deactivatelisting +vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const vendor_id = req.body.vendor_id; + const product_id = req.body.product_id; + + const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id); + + if (success) { + res.status(200).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT vendors/manage/shop/deactivate/:id +vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const vendor_id = parseInt(req.params.id, 10); + + const success = await VendorService.setShopStatus(user.user_id, vendor_id, false); + + if (success) { + res.status(200).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT vendors/manage/shop/activate/:id +vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const session_id = req.body.session_id; + const session_key = req.body.session_key; + const user = await UserService.checkSession(session_id, session_key, user_ip); + + // Get required parameters + const vendor_id = parseInt(req.params.id, 10); + + const success = await VendorService.setShopStatus(user.user_id, vendor_id, true); + + if (success) { + res.status(200).send({}); + } else { + res.status(500).send({}); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); diff --git a/src/models/betterzon/vendors/vendors.service.ts b/src/models/betterzon/vendors/vendors.service.ts new file mode 100644 index 0000000..fe2e979 --- /dev/null +++ b/src/models/betterzon/vendors/vendors.service.ts @@ -0,0 +1,214 @@ +import * as dotenv from 'dotenv'; +import {Vendor} from './vendor.interface'; +import {Vendors} from './vendors.interface'; + +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.BETTERZON_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + + +/** + * Service Methods + */ + +/** + * Fetches and returns all known vendors + */ +export const findAll = async (): Promise => { + let conn; + let vendorRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE isActive = true'); + for (let row in rows) { + if (row !== 'meta') { + let vendor: Vendor = { + city: '', + country_code: '', + name: '', + phone: '', + streetname: '', + vendor_id: 0, + website: '', + zip_code: '' + }; + const sqlVendor = rows[row]; + + vendor.vendor_id = sqlVendor.vendor_id; + vendor.name = sqlVendor.name; + vendor.streetname = sqlVendor.streetname; + vendor.zip_code = sqlVendor.zip_code; + vendor.city = sqlVendor.city; + vendor.country_code = sqlVendor.country_code; + vendor.phone = sqlVendor.phone; + vendor.website = sqlVendor.website; + vendorRows.push(vendor); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return vendorRows; +}; + +/** + * Fetches and returns the vendor with the specified id + * @param id The id of the vendor to fetch + */ +export const find = async (id: number): Promise => { + let conn; + let vendor: any; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE vendor_id = ? AND isActive = true', id); + for (let row in rows) { + if (row !== 'meta') { + vendor = rows[row]; + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return vendor; +}; + +/** + * Fetches and returns all vendors that match the search term + * @param term the term to match + */ +export const findBySearchTerm = async (term: string): Promise => { + let conn; + let vendorRows = []; + try { + conn = await pool.getConnection(); + term = '%' + term + '%'; + const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE name LIKE ? AND isActive = true', term); + for (let row in rows) { + if (row !== 'meta') { + vendorRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return vendorRows; +}; + +/** + * Get all vendors that have the given user as admin + * @param user The user to return the managed shops for + */ +export const getManagedShops = async (user_id: number): Promise => { + let conn; + let vendorRows = []; + try { + conn = await pool.getConnection(); + const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE admin_id LIKE ?', user_id); + for (let row in rows) { + if (row !== 'meta') { + vendorRows.push(rows[row]); + } + } + + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return vendorRows; +}; + +/** + * Deactivates a product listing for a specific vendor + * @param user_id The user id of the issuing user + * @param vendor_id The vendor id of the vendor to deactivate the listing for + * @param product_id The product id of the product to deactivate the listing for + */ +export const deactivateListing = async (user_id: number, vendor_id: number, product_id: number): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Check if the user is authorized to manage the requested vendor + const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]); + if (user_vendor_rows.length !== 1) { + return false; + } + + const status = await conn.query('UPDATE prices SET active_listing = false WHERE vendor_id = ? and product_id = ?', [vendor_id, product_id]); + + return status.affectedRows > 0; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return false; +}; + +/** + * Set the specified shop to either active or not active + * @param user_id The user id of the issuing user + * @param vendor_id The vendor id of the shop to update + * @param isActive The new active state + */ +export const setShopStatus = async (user_id: number, vendor_id: number, isActive: boolean): Promise => { + let conn; + try { + conn = await pool.getConnection(); + + // Check if the user is authorized to manage the requested vendor + const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]); + if (user_vendor_rows.length !== 1) { + return false; + } + + // Update the vendor state + const status = await conn.query('UPDATE vendors SET isActive = ? WHERE vendor_id = ?', [isActive, vendor_id]); + + return status.affectedRows > 0; + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return false; +};