diff --git a/Backend/package.json b/Backend/package.json index 362c0aa..e69d685 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -11,7 +11,9 @@ "author": "", "license": "ISC", "dependencies": { + "@types/cookie-parser": "^1.4.2", "bcrypt": "^5.0.1", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", diff --git a/Backend/src/index.ts b/Backend/src/index.ts index 3f7bed5..b387dd9 100644 --- a/Backend/src/index.ts +++ b/Backend/src/index.ts @@ -14,6 +14,10 @@ import {vendorsRouter} from './models/vendors/vendors.router'; import {errorHandler} from './middleware/error.middleware'; import {notFoundHandler} from './middleware/notFound.middleware'; import {usersRouter} from './models/users/users.router'; +import {pricealarmsRouter} from './models/pricealarms/pricealarms.router'; +import {contactpersonsRouter} from './models/contact_persons/contact_persons.router'; + +const cookieParser = require('cookie-parser'); dotenv.config(); @@ -38,12 +42,15 @@ const app = express(); app.use(helmet()); app.use(cors()); app.use(express.json()); +app.use(cookieParser()); app.use('/products', productsRouter); app.use('/categories', categoriesRouter); app.use('/manufacturers', manufacturersRouter); app.use('/prices', pricesRouter); app.use('/users', usersRouter); app.use('/vendors', vendorsRouter); +app.use('/pricealarms', pricealarmsRouter); +app.use('/contactpersons', contactpersonsRouter); app.use(errorHandler); app.use(notFoundHandler); diff --git a/Backend/src/middleware/error.middleware.ts b/Backend/src/middleware/error.middleware.ts index 0083d2b..e9af467 100644 --- a/Backend/src/middleware/error.middleware.ts +++ b/Backend/src/middleware/error.middleware.ts @@ -1,5 +1,5 @@ -import HttpException from "../common/http-exception"; -import { Request, Response, NextFunction } from "express"; +import HttpException from '../common/http-exception'; +import {Request, Response, NextFunction} from 'express'; export const errorHandler = ( error: HttpException, @@ -9,7 +9,7 @@ export const errorHandler = ( ) => { const status = error.statusCode || 500; const message = - error.message || "It's not you. It's us. We are having some problems."; + error.message || 'It\'s not you. It\'s us. We are having some problems.'; response.status(status).send(message); }; diff --git a/Backend/src/middleware/notFound.middleware.ts b/Backend/src/middleware/notFound.middleware.ts index 1191911..b7bf746 100644 --- a/Backend/src/middleware/notFound.middleware.ts +++ b/Backend/src/middleware/notFound.middleware.ts @@ -1,4 +1,4 @@ -import { Request, Response, NextFunction } from "express"; +import {Request, Response, NextFunction} from 'express'; export const notFoundHandler = ( request: Request, @@ -6,7 +6,7 @@ export const notFoundHandler = ( next: NextFunction ) => { - const message = "Resource not found"; + const message = 'Resource not found'; response.status(404).send(message); }; diff --git a/Backend/src/models/categories/categories.router.ts b/Backend/src/models/categories/categories.router.ts index 500d94f..9d811f6 100644 --- a/Backend/src/models/categories/categories.router.ts +++ b/Backend/src/models/categories/categories.router.ts @@ -27,7 +27,7 @@ categoriesRouter.get('/', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -46,7 +46,7 @@ categoriesRouter.get('/:id', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -65,6 +65,6 @@ categoriesRouter.get('/search/:term', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); diff --git a/Backend/src/models/contact_persons/contact_person.interface.ts b/Backend/src/models/contact_persons/contact_person.interface.ts new file mode 100644 index 0000000..e777a40 --- /dev/null +++ b/Backend/src/models/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/Backend/src/models/contact_persons/contact_persons.interface.ts b/Backend/src/models/contact_persons/contact_persons.interface.ts new file mode 100644 index 0000000..97f8393 --- /dev/null +++ b/Backend/src/models/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/Backend/src/models/contact_persons/contact_persons.router.ts b/Backend/src/models/contact_persons/contact_persons.router.ts new file mode 100644 index 0000000..bb2d1a0 --- /dev/null +++ b/Backend/src/models/contact_persons/contact_persons.router.ts @@ -0,0 +1,129 @@ +/** + * 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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } 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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } 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/Backend/src/models/contact_persons/contact_persons.service.ts b/Backend/src/models/contact_persons/contact_persons.service.ts new file mode 100644 index 0000000..7e23191 --- /dev/null +++ b/Backend/src/models/contact_persons/contact_persons.service.ts @@ -0,0 +1,175 @@ +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const mariadb = require('mariadb'); +const pool = mariadb.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + +import {Contact_Person} from './contact_person.interface'; +import {Contact_Persons} from './contact_persons.interface'; + + +/** + * 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/Backend/src/models/manufacturers/manufacturers.router.ts b/Backend/src/models/manufacturers/manufacturers.router.ts index bbd3c09..99b0875 100644 --- a/Backend/src/models/manufacturers/manufacturers.router.ts +++ b/Backend/src/models/manufacturers/manufacturers.router.ts @@ -19,7 +19,7 @@ export const manufacturersRouter = express.Router(); * Controller Definitions */ -// GET items/ +// GET manufacturers/ manufacturersRouter.get('/', async (req: Request, res: Response) => { try { const manufacturers: Manufacturers = await ManufacturerService.findAll(); @@ -27,11 +27,11 @@ manufacturersRouter.get('/', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); -// GET items/:id +// GET manufacturers/:id manufacturersRouter.get('/:id', async (req: Request, res: Response) => { const id: number = parseInt(req.params.id, 10); @@ -46,11 +46,11 @@ manufacturersRouter.get('/:id', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); -// GET items/:term +// GET manufacturers/:term manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => { const term: string = req.params.term; @@ -65,6 +65,6 @@ manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); diff --git a/Backend/src/models/pricealarms/pricealarm.interface.ts b/Backend/src/models/pricealarms/pricealarm.interface.ts new file mode 100644 index 0000000..c8a1717 --- /dev/null +++ b/Backend/src/models/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/Backend/src/models/pricealarms/pricealarms.interface.ts b/Backend/src/models/pricealarms/pricealarms.interface.ts new file mode 100644 index 0000000..c1dcbbd --- /dev/null +++ b/Backend/src/models/pricealarms/pricealarms.interface.ts @@ -0,0 +1,5 @@ +import {PriceAlarm} from './pricealarm.interface'; + +export interface PriceAlarms { + [key: number]: PriceAlarm; +} diff --git a/Backend/src/models/pricealarms/pricealarms.router.ts b/Backend/src/models/pricealarms/pricealarms.router.ts new file mode 100644 index 0000000..4ba1f9f --- /dev/null +++ b/Backend/src/models/pricealarms/pricealarms.router.ts @@ -0,0 +1,102 @@ +/** + * 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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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/create +pricealarmsRouter.post('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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/update +pricealarmsRouter.put('/', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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; + } + + // Create price alarm + const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_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.'})); + } +}); diff --git a/Backend/src/models/pricealarms/pricealarms.service.ts b/Backend/src/models/pricealarms/pricealarms.service.ts new file mode 100644 index 0000000..279b05b --- /dev/null +++ b/Backend/src/models/pricealarms/pricealarms.service.ts @@ -0,0 +1,106 @@ +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const mariadb = require('mariadb'); +const pool = mariadb.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + connectionLimit: 5 +}); + +/** + * Data Model Interfaces + */ + +import {PriceAlarm} from './pricealarm.interface'; +import {PriceAlarms} from './pricealarms.interface'; + + +/** + * 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]); + + if (res.affectedRows === 1) { + return true; + } else { + return false; + } + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return false; +}; + +/** + * 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]); + + if (res.affectedRows === 1) { + return true; + } else { + return false; + } + } catch (err) { + throw err; + } finally { + if (conn) { + conn.end(); + } + } + + return false; +}; diff --git a/Backend/src/models/prices/price.interface.ts b/Backend/src/models/prices/price.interface.ts index 956c9d5..702015a 100644 --- a/Backend/src/models/prices/price.interface.ts +++ b/Backend/src/models/prices/price.interface.ts @@ -4,7 +4,24 @@ export interface Price { vendor_id: number; price_in_cents: number; timestamp: Date; - // Only for deals - amazonDifference?: number; - amazonDifferencePercent?: number; +} + +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/Backend/src/models/prices/prices.router.ts b/Backend/src/models/prices/prices.router.ts index f215a82..047ce1b 100644 --- a/Backend/src/models/prices/prices.router.ts +++ b/Backend/src/models/prices/prices.router.ts @@ -6,6 +6,7 @@ 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'; /** @@ -40,7 +41,7 @@ pricesRouter.get('/', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -59,7 +60,7 @@ pricesRouter.get('/:id', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -78,7 +79,7 @@ pricesRouter.get('/bestDeals/:amount', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -97,6 +98,31 @@ pricesRouter.get('/byProduct/list/:ids', async (req: Request, res: Response) => 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."})); + 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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } 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/Backend/src/models/prices/prices.service.ts b/Backend/src/models/prices/prices.service.ts index 52d811f..3175b66 100644 --- a/Backend/src/models/prices/prices.service.ts +++ b/Backend/src/models/prices/prices.service.ts @@ -15,7 +15,7 @@ const pool = mariadb.createPool({ * Data Model Interfaces */ -import {Price} from './price.interface'; +import {Deal, Price} from './price.interface'; import {Prices} from './prices.interface'; @@ -31,7 +31,7 @@ export const findAll = async (): Promise => { let priceRows = []; try { conn = await pool.getConnection(); - const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices'); + 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 = { @@ -72,7 +72,7 @@ export const find = async (id: number): Promise => { let price: any; try { conn = await pool.getConnection(); - const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE price_id = ?', id); + 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]; @@ -99,7 +99,7 @@ export const findByProduct = async (product: number): Promise => { let priceRows = []; try { conn = await pool.getConnection(); - const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ?', product); + 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]); @@ -142,16 +142,17 @@ export const findByType = async (product: string, type: string): Promise 'PARTITION BY p.vendor_id ' + 'ORDER BY p.timestamp DESC) AS rk ' + 'FROM prices p ' + - 'WHERE product_id = ? AND vendor_id != 1) ' + + '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, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1 GROUP BY DAY(timestamp) ORDER BY timestamp', product); + 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, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1', 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) { @@ -188,13 +189,13 @@ export const findByVendor = async (product: string, vendor: string, type: string 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, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? ORDER BY timestamp DESC LIMIT 1', [product, 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, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? LIMIT 1', [product, vendor]); + 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, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ?', [product, 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) { @@ -237,7 +238,7 @@ export const getBestDeals = async (amount: number): Promise => { ' ROW_NUMBER() OVER(\n' + ' PARTITION BY p.product_id, p.vendor_id\n' + ' ORDER BY p.timestamp DESC) AS rk\n' + - ' FROM prices p)\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'); @@ -254,7 +255,7 @@ export const getBestDeals = async (amount: number): Promise => { } // Iterate over all prices to find the products with the biggest difference between amazon and other vendor - let deals: Price[] = []; + let deals: Deal[] = []; Object.keys(allPrices).forEach(productId => { if (allPrices[parseInt(productId)]) { @@ -286,7 +287,7 @@ export const getBestDeals = async (amount: number): Promise => { // Push only deals were the amazon price is actually higher if (deal.amazonDifferencePercent > 0) { - deals.push(deal as Price); + deals.push(deal as Deal); } } }); @@ -298,10 +299,8 @@ export const getBestDeals = async (amount: number): Promise => { let maxAmt = Math.min(amount, deals.length); for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) { - //console.log(deals[dealIndex]); priceRows.push(deals[dealIndex] as Price); } - } catch (err) { console.log(err); throw err; @@ -316,7 +315,7 @@ export const getBestDeals = async (amount: number): Promise => { /** * Fetches and returns the lowest, latest, non-amazon price for each given product - * @param ids the ids of the products + * @param productIds the ids of the products */ export const findListByProducts = async (productIds: [number]): Promise => { let conn; @@ -336,9 +335,9 @@ export const findListByProducts = async (productIds: [number]): Promise ' ROW_NUMBER() OVER(\n' + ' PARTITION BY p.product_id, p.vendor_id\n' + ' ORDER BY p.timestamp DESC) AS rk\n' + - ' FROM prices p' + - ' WHERE p.product_id IN (?)' + - ' AND p.vendor_id != 1)\n' + + ' 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]); @@ -366,7 +365,6 @@ export const findListByProducts = async (productIds: [number]): Promise priceRows.push(pricesForProd[0]); } }); - } catch (err) { throw err; } finally { @@ -377,3 +375,28 @@ export const findListByProducts = async (productIds: [number]): Promise 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/Backend/src/models/products/products.router.ts b/Backend/src/models/products/products.router.ts index 0c5c22d..dd332e5 100644 --- a/Backend/src/models/products/products.router.ts +++ b/Backend/src/models/products/products.router.ts @@ -27,7 +27,7 @@ productsRouter.get('/', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -46,7 +46,7 @@ productsRouter.get('/:id', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -65,7 +65,7 @@ productsRouter.get('/search/:term', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -84,6 +84,25 @@ productsRouter.get('/list/:ids', async (req: Request, res: Response) => { 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."})); + 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.'})); } }); diff --git a/Backend/src/models/products/products.service.ts b/Backend/src/models/products/products.service.ts index c0bee72..7d397ab 100644 --- a/Backend/src/models/products/products.service.ts +++ b/Backend/src/models/products/products.service.ts @@ -159,3 +159,41 @@ export const findList = async (ids: [number]): Promise => { 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; +}; diff --git a/Backend/src/models/users/users.router.ts b/Backend/src/models/users/users.router.ts index ee6d9f3..db28a93 100644 --- a/Backend/src/models/users/users.router.ts +++ b/Backend/src/models/users/users.router.ts @@ -47,10 +47,13 @@ usersRouter.post('/register', async (req: Request, res: Response) => { const session: Session = await UserService.createUser(username, password, email, ip); // Send the session details back to the user - res.status(201).send(session); + res.cookie('betterauth', JSON.stringify({ + id: session.session_id, + key: session.session_key + }), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(201); } catch (e) { console.log('Error handling a request: ' + e.message); - res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); @@ -70,39 +73,34 @@ usersRouter.post('/login', async (req: Request, res: Response) => { // Update the user entry and create a session const session: Session = await UserService.login(username, password, ip); - if(!session.session_id) { + 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]})); + 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(201).send(session); + res.cookie('betterauth', JSON.stringify({ + id: session.session_id, + key: session.session_key + }), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(200); } catch (e) { console.log('Error handling a request: ' + e.message); - res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); // POST users/checkSessionValid usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { try { - const sessionId: string = req.body.sessionId; - const sessionKey: string = req.body.sessionKey; const ip: string = req.connection.remoteAddress ?? ''; - if (!sessionId || !sessionKey) { - // Missing - res.status(400).send(JSON.stringify({message: 'Missing parameters'})); - return; - } - // Update the user entry and create a session - const user: User = await UserService.checkSession(sessionId, sessionKey, ip); + const user: User = await UserService.checkSessionWithCookie(req.cookies.betterauth, ip); - if(!user.user_id) { + if (!user.user_id) { // Error logging in, probably wrong username / password - res.status(401).send(JSON.stringify({messages: ["Invalid session"], codes: [5]})); + res.status(401).send(JSON.stringify({messages: ['Invalid session'], codes: [5]})); return; } @@ -110,6 +108,6 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { res.status(201).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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); diff --git a/Backend/src/models/users/users.service.ts b/Backend/src/models/users/users.service.ts index d1af37b..6958cc6 100644 --- a/Backend/src/models/users/users.service.ts +++ b/Backend/src/models/users/users.service.ts @@ -68,7 +68,7 @@ export const createUser = async (username: string, password: string, email: stri return { session_id: sessionId, session_key: sessionKey, - session_key_hash: '', + session_key_hash: 'HIDDEN', last_IP: ip }; @@ -135,7 +135,7 @@ export const login = async (username: string, password: string, ip: string): Pro return { session_id: sessionId, session_key: sessionKey, - session_key_hash: '', + session_key_hash: 'HIDDEN', last_IP: ip }; @@ -179,7 +179,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st // Key is valid, continue // Check if the session is still valid - if(validUntil <= new Date()) { + if (validUntil <= new Date()) { // Session expired, return invalid return {} as User; } @@ -193,7 +193,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st 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 FROM users WHERE user_id = ?"; + const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?'; const userRows = await conn.query(userQuery, userId); let username = ''; let email = ''; @@ -213,7 +213,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st user_id: userId, username: username, email: email, - password_hash: '', + password_hash: 'HIDDEN', registration_date: registrationDate, last_login_date: lastLoginDate }; @@ -229,6 +229,20 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st return {} as User; }; +/** + * 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 */ diff --git a/Backend/src/models/vendors/vendors.router.ts b/Backend/src/models/vendors/vendors.router.ts index 335cac6..89d02fd 100644 --- a/Backend/src/models/vendors/vendors.router.ts +++ b/Backend/src/models/vendors/vendors.router.ts @@ -6,6 +6,7 @@ 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'; /** @@ -19,7 +20,7 @@ export const vendorsRouter = express.Router(); * Controller Definitions */ -// GET items/ +// GET vendors/ vendorsRouter.get('/', async (req: Request, res: Response) => { try { const vendors: Vendors = await VendorService.findAll(); @@ -27,11 +28,27 @@ vendorsRouter.get('/', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); -// GET items/:id +// GET vendors/managed +vendorsRouter.get('/managed', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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); @@ -46,11 +63,11 @@ vendorsRouter.get('/:id', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } }); -// GET items/:name +// GET vendors/search/:term vendorsRouter.get('/search/:term', async (req: Request, res: Response) => { const term: string = req.params.term; @@ -65,6 +82,76 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => { 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."})); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT /manage/deactivatelisting +vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => { + try { + // Authenticate user + const user_ip = req.connection.remoteAddress ?? ''; + const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT /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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } catch (e) { + console.log('Error handling a request: ' + e.message); + res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); + } +}); + +// PUT /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 user = await UserService.checkSessionWithCookie(req.cookies.betterauth, 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.sendStatus(200); + } else { + res.sendStatus(500); + } + } 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/Backend/src/models/vendors/vendors.service.ts b/Backend/src/models/vendors/vendors.service.ts index ca8bbee..2439093 100644 --- a/Backend/src/models/vendors/vendors.service.ts +++ b/Backend/src/models/vendors/vendors.service.ts @@ -17,6 +17,7 @@ const pool = mariadb.createPool({ import {Vendor} from './vendor.interface'; import {Vendors} from './vendors.interface'; +import {User} from '../users/user.interface'; /** @@ -31,7 +32,7 @@ export const findAll = async (): Promise => { 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'); + 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 = { @@ -78,7 +79,7 @@ export const find = async (id: number): Promise => { 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 = ?', id); + 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]; @@ -106,7 +107,7 @@ export const findBySearchTerm = async (term: string): Promise => { 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 ?', 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]); @@ -123,3 +124,93 @@ export const findBySearchTerm = async (term: string): Promise => { 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; +}; diff --git a/Backend/webpack.config.ts b/Backend/webpack.config.ts index adb85fd..224b052 100644 --- a/Backend/webpack.config.ts +++ b/Backend/webpack.config.ts @@ -1,32 +1,32 @@ -const webpack = require("webpack"); -const path = require("path"); -const nodeExternals = require("webpack-node-externals"); +const webpack = require('webpack'); +const path = require('path'); +const nodeExternals = require('webpack-node-externals'); module.exports = { - entry: ["webpack/hot/poll?100", "./src/index.ts"], + entry: ['webpack/hot/poll?100', './src/index.ts'], watch: false, - target: "node", + target: 'node', externals: [ nodeExternals({ - whitelist: ["webpack/hot/poll?100"] + whitelist: ['webpack/hot/poll?100'] }) ], module: { rules: [ { test: /.tsx?$/, - use: "ts-loader", + use: 'ts-loader', exclude: /node_modules/ } ] }, - mode: "development", + mode: 'development', resolve: { - extensions: [".tsx", ".ts", ".js"] + extensions: ['.tsx', '.ts', '.js'] }, plugins: [new webpack.HotModuleReplacementPlugin()], output: { - path: path.join(__dirname, "dist"), - filename: "index.js" + path: path.join(__dirname, 'dist'), + filename: 'index.js' } }; diff --git a/Crawler/Crawler.iml b/Crawler/Crawler.iml index 8568e2d..6bfc4c4 100644 --- a/Crawler/Crawler.iml +++ b/Crawler/Crawler.iml @@ -2,13 +2,12 @@ - + - \ No newline at end of file diff --git a/Crawler/api.py b/Crawler/api.py index 92617c4..7b7e0c2 100644 --- a/Crawler/api.py +++ b/Crawler/api.py @@ -1,13 +1,17 @@ +import os + from flask import Flask from flask_restful import Resource, Api, reqparse +import crawler + app = Flask(__name__) api = Api(app) # To parse request data parser = reqparse.RequestParser() -parser.add_argument('key') -parser.add_argument('products') +parser.add_argument('key', type=str) +parser.add_argument('products', type=int, action='append') class CrawlerApi(Resource): @@ -17,7 +21,12 @@ class CrawlerApi(Resource): def post(self): # Accept crawler request here args = parser.parse_args() - return args + access_key = os.getenv('CRAWLER_ACCESS_KEY') + if(args['key'] == access_key): + crawler.crawl(args['products']) + return {'message': 'success'} + else: + return {'message': 'Wrong access key'} api.add_resource(CrawlerApi, '/') diff --git a/Crawler/crawler.py b/Crawler/crawler.py index 99ff867..45bd15a 100644 --- a/Crawler/crawler.py +++ b/Crawler/crawler.py @@ -1,78 +1,107 @@ -import sql - - -def crawl(product_ids: [int]) -> dict: - """ - Crawls the given list of products and saves the results to sql - :param products: The list of product IDs to fetch - :return: A dict with the following fields: - total_crawls: number of total crawl tries (products * vendors per product) - successful_crawls: number of successful products - products_with_problems: list of products that have not been crawled successfully - """ - total_crawls = 0 - successful_crawls = 0 - products_with_problems = [] - - # Iterate over every product that has to be crawled - for product_id in product_ids: - # Get all links for this product - product_links = sql.getProductLinksForProduct(product_id) - - crawled_data = [] - - # Iterate over every link / vendor - for product_vendor_info in product_links: - total_crawls += 1 - - # Call the appropriate vendor crawling function and append the result to the list of crawled data - if product_vendor_info['vendor_id'] == 1: - # Amazon - crawled_data.append(__crawl_amazon__(product_vendor_info)) - elif product_vendor_info['vendor_id'] == 2: - # Apple - crawled_data.append(__crawl_apple__(product_vendor_info)) - elif product_vendor_info['vendor_id'] == 3: - # Media Markt - crawled_data.append(__crawl_mediamarkt__(product_vendor_info)) - else: - products_with_problems.append(product_vendor_info) - continue - - successful_crawls += 1 - - # Insert data to SQL - sql.insertData(crawled_data) - - return { - 'total_crawls': total_crawls, - 'successful_crawls': successful_crawls, - 'products_with_problems': products_with_problems - } - - -def __crawl_amazon__(product_info: dict) -> tuple: - """ - Crawls the price for the given product from amazon - :param product_info: A dict with product info containing product_id, vendor_id, url - :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) - """ - return (product_info['product_id'], product_info['vendor_id'], 123) - - -def __crawl_apple__(product_info: dict) -> tuple: - """ - Crawls the price for the given product from apple - :param product_info: A dict with product info containing product_id, vendor_id, url - :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) - """ - return (product_info['product_id'], product_info['vendor_id'], 123) - - -def __crawl_mediamarkt__(product_info: dict) -> tuple: - """ - Crawls the price for the given product from media markt - :param product_info: A dict with product info containing product_id, vendor_id, url - :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) - """ - pass +import sql +import requests +from bs4 import BeautifulSoup + +HEADERS = ({'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 ' + 'Safari/537.36'}) + + +def crawl(product_ids: [int]) -> dict: + """ + Crawls the given list of products and saves the results to sql + :param products: The list of product IDs to fetch + :return: A dict with the following fields: + total_crawls: number of total crawl tries (products * vendors per product) + successful_crawls: number of successful products + products_with_problems: list of products that have not been crawled successfully + """ + total_crawls = 0 + successful_crawls = 0 + products_with_problems = [] + + # Iterate over every product that has to be crawled + for product_id in product_ids: + # Get all links for this product + product_links = sql.getProductLinksForProduct(product_id) + + crawled_data = [] + + # Iterate over every link / vendor + for product_vendor_info in product_links: + total_crawls += 1 + + # Call the appropriate vendor crawling function and append the result to the list of crawled data + if product_vendor_info['vendor_id'] == 1: + # Amazon + data = __crawl_amazon__(product_vendor_info) + if data: + crawled_data.append(data) + elif product_vendor_info['vendor_id'] == 2: + # Apple + data = __crawl_apple__(product_vendor_info) + if data: + crawled_data.append(data) + elif product_vendor_info['vendor_id'] == 3: + # Media Markt + data = __crawl_mediamarkt__(product_vendor_info) + if data: + crawled_data.append(data) + else: + products_with_problems.append(product_vendor_info) + continue + + successful_crawls += 1 + + # Insert data to SQL + sql.insertData(crawled_data) + + return { + 'total_crawls': total_crawls, + 'successful_crawls': successful_crawls, + 'products_with_problems': products_with_problems + } + + +def __crawl_amazon__(product_info: dict) -> tuple: + """ + Crawls the price for the given product from amazon + :param product_info: A dict with product info containing product_id, vendor_id, url + :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) + """ + page = requests.get(product_info['url'], headers=HEADERS) + soup = BeautifulSoup(page.content, features="lxml") + try: + price = int( + soup.find(id='priceblock_ourprice').get_text().replace(".", "").replace(",", "").replace("€", "").strip()) + if not price: + price = int(soup.find(id='price_inside_buybox').get_text().replace(".", "").replace(",", "").replace("€", "").strip()) + + except RuntimeError: + price = -1 + except AttributeError: + price = -1 + + if price != -1: + return (product_info['product_id'], product_info['vendor_id'], price) + else: + return None + + +def __crawl_apple__(product_info: dict) -> tuple: + """ + Crawls the price for the given product from apple + :param product_info: A dict with product info containing product_id, vendor_id, url + :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) + """ + # return (product_info['product_id'], product_info['vendor_id'], 123) + pass + + +def __crawl_mediamarkt__(product_info: dict) -> tuple: + """ + Crawls the price for the given product from media markt + :param product_info: A dict with product info containing product_id, vendor_id, url + :return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) + """ + pass diff --git a/Crawler/crawler/spiders/amazon.py b/Crawler/crawler/spiders/amazon.py deleted file mode 100644 index 12ea3d5..0000000 --- a/Crawler/crawler/spiders/amazon.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -import scrapy -from urllib.parse import urlencode -from urllib.parse import urljoin -import re -import json - -queries = ['iphone'] -API = '' - - -def get_url(url): - payload = {'api_key': API, 'url': url, 'country_code': 'us'} - proxy_url = 'http://api.scraperapi.com/?' + urlencode(payload) - return proxy_url - - -class AmazonSpider(scrapy.Spider): - name = 'amazon' - - def start_requests(self): - for query in queries: - url = 'https://www.amazon.de/s?' + urlencode({'k': query}) - yield scrapy.Request(url=url, callback=self.parse_keyword_response) - - def parse_keyword_response(self, response): - products = response.xpath('//*[@data-asin]') - - for product in products: - asin = product.xpath('@data-asin').extract_first() - product_url = f"https://www.amazon.de/dp/{asin}" - yield scrapy.Request(url=product_url, callback=self.parse_product_page, meta={'asin': asin}) - - next_page = response.xpath('//li[@class="a-last"]/a/@href').extract_first() - if next_page: - url = urljoin("https://www.amazon.de", next_page) - yield scrapy.Request(url=url, callback=self.parse_keyword_response) - - def parse_product_page(self, response): - asin = response.meta['asin'] - title = response.xpath('//*[@id="productTitle"]/text()').extract_first() - image = re.search('"large":"(.*?)"', response.text).groups()[0] - rating = response.xpath('//*[@id="acrPopover"]/@title').extract_first() - number_of_reviews = response.xpath('//*[@id="acrCustomerReviewText"]/text()').extract_first() - price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first() - - if not price: - price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \ - response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first() - - temp = response.xpath('//*[@id="twister"]') - sizes = [] - colors = [] - if temp: - s = re.search('"variationValues" : ({.*})', response.text).groups()[0] - json_acceptable = s.replace("'", "\"") - di = json.loads(json_acceptable) - sizes = di.get('size_name', []) - colors = di.get('color_name', []) - - bullet_points = response.xpath('//*[@id="feature-bullets"]//li/span/text()').extract() - seller_rank = response.xpath( - '//*[text()="Amazon Best Sellers Rank:"]/parent::*//text()[not(parent::style)]').extract() - yield {'asin': asin, 'Title': title, 'MainImage': image, 'Rating': rating, 'NumberOfReviews': number_of_reviews, - 'Price': price, 'AvailableSizes': sizes, 'AvailableColors': colors, 'BulletPoints': bullet_points, - 'SellerRank': seller_rank} diff --git a/Crawler/requirements.txt b/Crawler/requirements.txt index ba99df1..a704f27 100644 --- a/Crawler/requirements.txt +++ b/Crawler/requirements.txt @@ -1,5 +1,7 @@ pymysql -flask +flask==1.1.2 flask-sqlalchemy flask_restful -scrapy \ No newline at end of file +beautifulsoup4 +requests +lxml \ No newline at end of file diff --git a/Crawler/sql.py b/Crawler/sql.py index 1cf3a58..c1b2669 100644 --- a/Crawler/sql.py +++ b/Crawler/sql.py @@ -54,7 +54,6 @@ def getProductLinksForProduct(product_id: int) -> [dict]: cur = conn.cursor() query = 'SELECT vendor_id, url FROM product_links WHERE product_id = %s' - cur.execute(query, (product_id,)) products = list(map(lambda x: {'product_id': product_id, 'vendor_id': x[0], 'url': x[1]}, cur.fetchall())) diff --git a/Crawler/unused/scrapy/amazonspider.py b/Crawler/unused/scrapy/amazonspider.py new file mode 100644 index 0000000..5f88e20 --- /dev/null +++ b/Crawler/unused/scrapy/amazonspider.py @@ -0,0 +1,33 @@ +import scrapy +from scrapy.crawler import CrawlerProcess +import re + +class AmazonSpider(scrapy.Spider): + name = 'amazon' + allowed_domains = ['amazon.de'] + start_urls = ['https://amazon.de/dp/B083DRCPJG'] + + # def __init__(self, start_urls): + # self.start_urls = start_urls + + def parse(self, response): + price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first() + if not price: + price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \ + response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first() + + euros = re.match('(\d*),\d\d', price).group(1) + cents = re.match('\d*,(\d\d)', price).group(1) + priceincents = euros + cents + + yield {'price': priceincents} + + +def start_crawling(): + process = CrawlerProcess( + settings={'COOKIES_ENABLED': 'False', 'CONCURRENT_REQUESTS_PER_IP': 1, 'ROBOTSTXT_OBEY': False, + 'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', + 'DOWNLOAD_DELAY': 3} + , install_root_handler=False) + process.crawl() + process.start() diff --git a/Crawler/crawler/__init__.py b/Crawler/unused/scrapy/crawler/__init__.py similarity index 100% rename from Crawler/crawler/__init__.py rename to Crawler/unused/scrapy/crawler/__init__.py diff --git a/Crawler/crawler/items.py b/Crawler/unused/scrapy/crawler/items.py similarity index 100% rename from Crawler/crawler/items.py rename to Crawler/unused/scrapy/crawler/items.py diff --git a/Crawler/crawler/middlewares.py b/Crawler/unused/scrapy/crawler/middlewares.py similarity index 100% rename from Crawler/crawler/middlewares.py rename to Crawler/unused/scrapy/crawler/middlewares.py diff --git a/Crawler/crawler/pipelines.py b/Crawler/unused/scrapy/crawler/pipelines.py similarity index 100% rename from Crawler/crawler/pipelines.py rename to Crawler/unused/scrapy/crawler/pipelines.py diff --git a/Crawler/crawler/settings.py b/Crawler/unused/scrapy/crawler/settings.py similarity index 100% rename from Crawler/crawler/settings.py rename to Crawler/unused/scrapy/crawler/settings.py diff --git a/Crawler/scrapy.cfg b/Crawler/unused/scrapy/scrapy.cfg similarity index 92% rename from Crawler/scrapy.cfg rename to Crawler/unused/scrapy/scrapy.cfg index 83a4eef..9c0c1bc 100644 --- a/Crawler/scrapy.cfg +++ b/Crawler/unused/scrapy/scrapy.cfg @@ -8,4 +8,4 @@ default = crawler.settings [deploy] #url = http://localhost:6800/ -project = crawler +project = crawler \ No newline at end of file diff --git a/Crawler/crawler/spiders/__init__.py b/Crawler/unused/scrapy/spiders/__init__.py similarity index 100% rename from Crawler/crawler/spiders/__init__.py rename to Crawler/unused/scrapy/spiders/__init__.py diff --git a/Crawler/unused/scrapy/spiders/amazon.py b/Crawler/unused/scrapy/spiders/amazon.py new file mode 100644 index 0000000..c74196b --- /dev/null +++ b/Crawler/unused/scrapy/spiders/amazon.py @@ -0,0 +1,25 @@ +import scrapy +import re + +class AmazonSpider(scrapy.Spider): + name = 'amazon' + allowed_domains = ['amazon.de'] + start_urls = ['https://amazon.de/dp/B083DRCPJG'] + + def parse(self, response): + price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first() + if not price: + price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \ + response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first() + + euros = re.match('(\d*),\d\d', price).group(1) + cents = re.match('\d*,(\d\d)', price).group(1) + priceincents = euros + cents + + yield {'price': priceincents} + + + + + + diff --git a/CucumberTests/CucumberTests.iml b/CucumberTests/CucumberTests.iml index 70e64e8..d398c4f 100644 --- a/CucumberTests/CucumberTests.iml +++ b/CucumberTests/CucumberTests.iml @@ -10,17 +10,24 @@ - + - - - - - - - + + + + + + + + + + + + + + @@ -53,5 +60,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CucumberTests/pom.xml b/CucumberTests/pom.xml index 8aaba7a..c72eb52 100644 --- a/CucumberTests/pom.xml +++ b/CucumberTests/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - de.taskhub + xyz.betterzon CucumberTests 1.0-SNAPSHOT @@ -13,21 +13,30 @@ - io.cucumber - cucumber-java - 2.3.1 + junit + junit + 4.12 test + + io.cucumber + cucumber-java + 6.10.3 + io.cucumber cucumber-junit - 2.3.1 - test + 6.10.3 org.apache.maven.plugins maven-compiler-plugin 3.8.1 + + org.seleniumhq.selenium + selenium-java + 3.141.59 + - \ No newline at end of file + diff --git a/CucumberTests/src/test/java/RunTest.java b/CucumberTests/src/test/java/RunTest.java index f7c5387..fcbc954 100644 --- a/CucumberTests/src/test/java/RunTest.java +++ b/CucumberTests/src/test/java/RunTest.java @@ -1,6 +1,10 @@ -import cucumber.api.CucumberOptions; -import cucumber.api.junit.Cucumber; +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.runner.RunWith; +import org.openqa.selenium.firefox.FirefoxDriver; +import stepdefs.Preconditions; @RunWith(Cucumber.class) @CucumberOptions( @@ -9,4 +13,13 @@ import org.junit.runner.RunWith; ) public class RunTest { + @BeforeClass + public static void setup() { + Preconditions.driver= new FirefoxDriver(); + } + + @AfterClass + public static void teardown() { + Preconditions.driver.close(); + } } diff --git a/CucumberTests/src/test/java/stepdefs/Preconditions.java b/CucumberTests/src/test/java/stepdefs/Preconditions.java new file mode 100644 index 0000000..f9c22b1 --- /dev/null +++ b/CucumberTests/src/test/java/stepdefs/Preconditions.java @@ -0,0 +1,7 @@ +package stepdefs; + +import org.openqa.selenium.WebDriver; + +public class Preconditions { + public static WebDriver driver; +} diff --git a/CucumberTests/src/test/java/stepdefs/PriceAlarm.java b/CucumberTests/src/test/java/stepdefs/PriceAlarm.java index 8c82759..83c332e 100644 --- a/CucumberTests/src/test/java/stepdefs/PriceAlarm.java +++ b/CucumberTests/src/test/java/stepdefs/PriceAlarm.java @@ -1,67 +1,68 @@ package stepdefs; -import cucumber.api.java.en.Given; -import cucumber.api.java.en.Then; -import cucumber.api.java.en.When; +import io.cucumber.java.PendingException; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; public class PriceAlarm { - @Given("^the user has at least (\\d+) price alarm set$") - public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception { - } + @Given("^the user has at least (\\d+) price alarm set$") + public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception { + } - @When("^the user clicks on the profile icon$") - public void the_user_clicks_on_the_profile_icon() throws Exception { - } + @When("^the user clicks on the profile icon$") + public void the_user_clicks_on_the_profile_icon() throws Exception { + } - @Then("^the profile details popup should open$") - public void the_profile_details_popup_should_open() throws Exception { - } + @Then("^the profile details popup should open$") + public void the_profile_details_popup_should_open() throws Exception { + } - @When("^the user clicks on price alarms$") - public void the_user_clicks_on_price_alarms() throws Exception { - } + @When("^the user clicks on price alarms$") + public void the_user_clicks_on_price_alarms() throws Exception { + } - @Then("^the price alarm list should open$") - public void the_price_alarm_list_should_open() throws Exception { - } + @Then("^the price alarm list should open$") + public void the_price_alarm_list_should_open() throws Exception { + } - @Then("^the price alarm list should contain at least (\\d+) entry$") - public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception { - } + @Then("^the price alarm list should contain at least (\\d+) entry$") + public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception { + } - @Then("^the price alarm list should contain a maximum of (\\d+) entries per page$") - public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception { - } + @Then("^the price alarm list should contain a maximum of (\\d+) entries per page$") + public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception { + } - @Given("^the user is on the price alarm list page$") - public void the_user_is_on_the_price_alarm_list_page() throws Exception { - } + @Given("^the user is on the price alarm list page$") + public void the_user_is_on_the_price_alarm_list_page() throws Exception { + } - @When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$") - public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception { - } + @When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$") + public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception { + } - @Then("^a popup should open asking the user to confirm the removal$") - public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception { - } + @Then("^a popup should open asking the user to confirm the removal$") + public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception { + } - @When("^the user confirms the removal of the price alarm$") - public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception { - } + @When("^the user confirms the removal of the price alarm$") + public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception { + } - @Then("^the price alarm should be removed from the database$") - public void the_price_alarm_should_be_removed_from_the_database() throws Exception { - } + @Then("^the price alarm should be removed from the database$") + public void the_price_alarm_should_be_removed_from_the_database() throws Exception { + } - @Then("^a popup should open where the user can edit the alarm$") - public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception { - } + @Then("^a popup should open where the user can edit the alarm$") + public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception { + } - @When("^the user clicks on the \"([^\"]*)\" button$") - public void the_user_clicks_on_the_button(String arg1) throws Exception { - } + @When("^the user clicks on the \"([^\"]*)\" button$") + public void the_user_clicks_on_the_button(String arg1) throws Exception { + } - @Then("^the price alarm should be updated in the database$") - public void the_price_alarm_should_be_updated_in_the_database() throws Exception { - } + @Then("^the price alarm should be updated in the database$") + public void the_price_alarm_should_be_updated_in_the_database() throws Exception { + } } diff --git a/CucumberTests/src/test/java/stepdefs/SearchProduct.java b/CucumberTests/src/test/java/stepdefs/SearchProduct.java index 536a952..af32faa 100644 --- a/CucumberTests/src/test/java/stepdefs/SearchProduct.java +++ b/CucumberTests/src/test/java/stepdefs/SearchProduct.java @@ -1,52 +1,72 @@ package stepdefs; -import cucumber.api.PendingException; -import cucumber.api.java.en.Given; -import cucumber.api.java.en.Then; -import cucumber.api.java.en.When; +import io.cucumber.java.PendingException; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; public class SearchProduct { - @Given("^the user is on the landing page$") - public void the_user_is_on_the_landing_page() throws Exception { - } + @Given("^the user is on the landing page$") + public void the_user_is_on_the_landing_page() throws Exception { + //throw new PendingException(); + Preconditions.driver.get("https://betterzon.xyz"); + WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) + .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); + } - @When("^the user enters the search term \"([^\"]*)\" and clicks search$") - public void the_user_enters_the_search_term_and_clicks_search(String arg0) throws Exception { - } + @When("^the user enters the search term \"([^\"]*)\" and clicks search$") + public void the_user_enters_the_search_term_and_clicks_search(String searchTerm) throws Exception { + WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid")); + searchField.sendKeys(searchTerm); + searchField.sendKeys(Keys.ENTER); + WebElement logo = (new WebDriverWait(Preconditions.driver, 10)) + .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo"))); + } - @Then("^the user should see the error page \"([^\"]*)\"$") - public void the_user_should_see_the_error_page(String arg0) throws Exception { - } + @Then("^the user should see the error page \"([^\"]*)\"$") + public void the_user_should_see_the_error_page(String arg0) throws Exception { + WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, 10)) + .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".ng-star-inserted"))); + assert(noProdsFoundMsg.getText().contains("No Products found!")); + } - @Given("^the user is not logged in$") - public void the_user_is_not_logged_in() throws Exception { - } + @Given("^the user is not logged in$") + public void the_user_is_not_logged_in() throws Exception { + } - @Given("^the user is logged in$") - public void the_user_is_logged_in() throws Exception { - } + @Given("^the user is logged in$") + public void the_user_is_logged_in() throws Exception { + } - @Then("^the user should see a list of products$") - public void the_user_should_see_a_list_of_products() throws Exception { - } + @Then("^the user should see a list of products$") + public void the_user_should_see_a_list_of_products() throws Exception { + WebElement product = (new WebDriverWait(Preconditions.driver, 10)) + .until(ExpectedConditions.elementToBeClickable(By.cssSelector(".productItem.ng-star-inserted"))); + assert(product.isDisplayed()); + } - @When("^the user clicks on the first product$") - public void the_user_clicks_on_the_first_product() throws Exception { - } + @When("^the user clicks on the first product$") + public void the_user_clicks_on_the_first_product() throws Exception { + } - @Then("^the user should see the product detail page$") - public void the_user_should_see_the_product_detail_page() throws Exception { - } + @Then("^the user should see the product detail page$") + public void the_user_should_see_the_product_detail_page() throws Exception { + } - @Then("^the set price alarm box should show \"([^\"]*)\"$") - public void the_set_price_alarm_box_should_show(String arg0) throws Exception { - } + @Then("^the set price alarm box should show \"([^\"]*)\"$") + public void the_set_price_alarm_box_should_show(String arg0) throws Exception { + } - @When("^the user sets a price alarm$") - public void the_user_sets_a_price_alarm() throws Exception { - } + @When("^the user sets a price alarm$") + public void the_user_sets_a_price_alarm() throws Exception { + } - @Then("^the user should receive an email confirming the price alarm$") - public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception { - } + @Then("^the user should receive an email confirming the price alarm$") + public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception { + } } diff --git a/CucumberTests/src/test/resource/priceAlarms.feature b/CucumberTests/src/test/resource/priceAlarms.feature index 1e75acb..0550eef 100644 --- a/CucumberTests/src/test/resource/priceAlarms.feature +++ b/CucumberTests/src/test/resource/priceAlarms.feature @@ -1,28 +1,28 @@ Feature: Price Alarms - Scenario: Show a list of price alarms - Given the user is on the landing page - And the user is logged in - And the user has at least 1 price alarm set - When the user clicks on the profile icon - Then the profile details popup should open - When the user clicks on price alarms - Then the price alarm list should open - And the price alarm list should contain at least 1 entry - And the price alarm list should contain a maximum of 20 entries per page + Scenario: Show a list of price alarms + Given the user is on the landing page + And the user is logged in + And the user has at least 1 price alarm set + When the user clicks on the profile icon + Then the profile details popup should open + When the user clicks on price alarms + Then the price alarm list should open + And the price alarm list should contain at least 1 entry + And the price alarm list should contain a maximum of 20 entries per page - Scenario: Remove a price alarm - Given the user is on the price alarm list page - And the user is logged in - When the user clicks on the "remove" button next to a price alarm - Then a popup should open asking the user to confirm the removal - When the user confirms the removal of the price alarm - Then the price alarm should be removed from the database + Scenario: Remove a price alarm + Given the user is on the price alarm list page + And the user is logged in + When the user clicks on the "remove" button next to a price alarm + Then a popup should open asking the user to confirm the removal + When the user confirms the removal of the price alarm + Then the price alarm should be removed from the database - Scenario: Edit a price alarm - Given the user is on the price alarm list page - And the user is logged in - When the user clicks on the "edit" button next to a price alarm - Then a popup should open where the user can edit the alarm - When the user clicks on the "save changes" button - Then the price alarm should be updated in the database + Scenario: Edit a price alarm + Given the user is on the price alarm list page + And the user is logged in + When the user clicks on the "edit" button next to a price alarm + Then a popup should open where the user can edit the alarm + When the user clicks on the "save changes" button + Then the price alarm should be updated in the database diff --git a/CucumberTests/src/test/resource/searchProduct.feature b/CucumberTests/src/test/resource/searchProduct.feature index 67d17f1..e73bdc5 100644 --- a/CucumberTests/src/test/resource/searchProduct.feature +++ b/CucumberTests/src/test/resource/searchProduct.feature @@ -1,26 +1,26 @@ Feature: Search a Product - Scenario: User searches for unknown product - Given the user is on the landing page - When the user enters the search term "iPhone 13" and clicks search - Then the user should see the error page "No products found" + Scenario: User searches for unknown product + Given the user is on the landing page + When the user enters the search term "iPhone 13" and clicks search + Then the user should see the error page "No products found" - Scenario: User is not logged in, searches for known product - Given the user is on the landing page - And the user is not logged in - When the user enters the search term "iPhone 12" and clicks search - Then the user should see a list of products - When the user clicks on the first product - Then the user should see the product detail page - And the set price alarm box should show "Log in to continue" + Scenario: User is not logged in, searches for known product + Given the user is on the landing page + And the user is not logged in + When the user enters the search term "iPhone 12" and clicks search + Then the user should see a list of products + When the user clicks on the first product + Then the user should see the product detail page + And the set price alarm box should show "Log in to continue" - Scenario: User is logged in, searches for known product - Given the user is on the landing page - And the user is logged in - When the user enters the search term "iPhone 12" and clicks search - Then the user should see a list of products - When the user clicks on the first product - Then the user should see the product detail page - And the set price alarm box should show "Set price alarm" - When the user sets a price alarm - Then the user should receive an email confirming the price alarm + Scenario: User is logged in, searches for known product + Given the user is on the landing page + And the user is logged in + When the user enters the search term "iPhone 12" and clicks search + Then the user should see a list of products + When the user clicks on the first product + Then the user should see the product detail page + And the set price alarm box should show "Set price alarm" + When the user sets a price alarm + Then the user should receive an email confirming the price alarm diff --git a/Frontend/src/app/models/pricealarm.ts b/Frontend/src/app/models/pricealarm.ts new file mode 100644 index 0000000..c8a1717 --- /dev/null +++ b/Frontend/src/app/models/pricealarm.ts @@ -0,0 +1,6 @@ +export interface PriceAlarm { + alarm_id: number; + user_id: number; + product_id: number; + defined_price: number; +} diff --git a/Frontend/src/app/services/api.service.ts b/Frontend/src/app/services/api.service.ts index ec91051..640b317 100644 --- a/Frontend/src/app/services/api.service.ts +++ b/Frontend/src/app/services/api.service.ts @@ -5,6 +5,7 @@ import {Product} from '../models/product'; import {Price} from '../models/price'; import {Observable, of} from 'rxjs'; import {Vendor} from '../models/vendor'; +import {PriceAlarm} from '../models/pricealarm'; @Injectable({ providedIn: 'root' @@ -17,83 +18,191 @@ export class ApiService { ) { } + + /* ____ __ __ + / __ \_________ ____/ /_ _______/ /______ + / /_/ / ___/ __ \/ __ / / / / ___/ __/ ___/ + / ____/ / / /_/ / /_/ / /_/ / /__/ /_(__ ) + /_/ /_/ \____/\__,_/\__,_/\___/\__/____/ + */ + + /** + * Gets the specified product from the API + * @param id The id of the product to get + * @return Observable An observable containing a single product + */ getProduct(id): Observable { try { - const prod = this.http.get((this.apiUrl + '/products/' + id)); - return prod; + return this.http.get((this.apiUrl + '/products/' + id)); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + + /** + * Gets a list of products that match the given search term + * @param query The search term to match + * @return Observable An observable list of products + */ getProductsByQuery(query): Observable { try { - const prods = this.http.get((this.apiUrl + '/products/search/' + query)); - return prods; + return this.http.get((this.apiUrl + '/products/search/' + query)); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + /** + * Gets a list of all products + * @return Observable An observable list of products + */ getProducts(): Observable { try { - const prods = this.http.get((this.apiUrl + '/products')); - return prods; + return this.http.get((this.apiUrl + '/products')); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + + /* ____ _ + / __ \_____(_)_______ _____ + / /_/ / ___/ / ___/ _ \/ ___/ + / ____/ / / / /__/ __(__ ) + /_/ /_/ /_/\___/\___/____/ + */ + + /** + * Gets a list of all prices + * @return Observable An observable list of prices + */ getPrices(): Observable { try { - const prices = this.http.get((this.apiUrl + '/prices')); - return prices; + return this.http.get((this.apiUrl + '/prices')); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + /** + * Gets the lowest prices of every vendor for the given product + * @param productId The product id of the product to fetch the prices for + * @return Observable An observable list of prices + */ getLowestPrices(productId): Observable { try { let params = new HttpParams(); params = params.append('product', productId); params = params.append('type', 'lowest'); - const prices = this.http.get((this.apiUrl + '/prices'), {params}); - return prices; + return this.http.get((this.apiUrl + '/prices'), {params}); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + /** + * Gets the latest amazon price for the given product + * @param productId The product id of the product to get the price for + * @return Observable An observable containing a single price + */ getAmazonPrice(productId): Observable { try { let params = new HttpParams(); params = params.append('product', productId); params = params.append('vendor', '1'); params = params.append('type', 'newest'); - const price = this.http.get((this.apiUrl + '/prices'), {params}); - return price; + return this.http.get((this.apiUrl + '/prices'), {params}); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + /** + * Gets the newest prices of every vendor for the given product + * @param productId The product id of the product to fetch the prices for + * @return Observable An observable list of prices + */ getCurrentPricePerVendor(productId): Observable { try { let params = new HttpParams(); params = params.append('product', productId); params = params.append('type', 'newest'); - const prices = this.http.get((this.apiUrl + '/prices'), {params}); - return prices; + return this.http.get((this.apiUrl + '/prices'), {params}); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); } } + + /* _ __ __ + | | / /__ ____ ____/ /___ __________ + | | / / _ \/ __ \/ __ / __ \/ ___/ ___/ + | |/ / __/ / / / /_/ / /_/ / / (__ ) + |___/\___/_/ /_/\__,_/\____/_/ /____/ + */ + + /** + * Gets a list of all vendors + * @return Observable An observable list of vendors + */ getVendors(): Observable { try { - const vendors = this.http.get((this.apiUrl + '/vendors')); - return vendors; + return this.http.get((this.apiUrl + '/vendors')); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + + /* ____ _ ___ __ + / __ \_____(_)_______ / | / /___ __________ ___ _____ + / /_/ / ___/ / ___/ _ \ / /| | / / __ `/ ___/ __ `__ \/ ___/ + / ____/ / / / /__/ __/ / ___ |/ / /_/ / / / / / / / (__ ) + /_/ /_/ /_/\___/\___/ /_/ |_/_/\__,_/_/ /_/ /_/ /_/____/ + */ + + /** + * Gets a list of all price alarms + * @return Observable An observable list of price alarms + */ + getPriceAlarms(): Observable { + try { + return this.http.get((this.apiUrl + '/pricealarms')); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Creates a new price alarm + * @param productId The product id of the product to create the alarm for + * @param definedPrice The defined target price + * @return Observable The observable response of the api + */ + createPriceAlarms(productId: number, definedPrice: number): Observable { + try { + return this.http.post((this.apiUrl + '/pricealarms'), JSON.stringify({ + product_id: productId, + defined_price: definedPrice + })); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Updates the given price alarm + * @param alarmId The alarm id of the alarm to update + * @param definedPrice The defined target price + * @return Observable The observable response of the api + */ + updatePriceAlarms(alarmId: number, definedPrice: number): Observable { + try { + return this.http.put((this.apiUrl + '/pricealarms'), JSON.stringify({ + alarm_id: alarmId, + defined_price: definedPrice + })); } catch (exception) { process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); }