Compare commits

...

19 Commits

Author SHA1 Message Date
8987d3deff Merge remote-tracking branch 'origin/develop' into BETTERZON-106
# Conflicts:
#	Frontend/src/app/app.component.html
#	Frontend/src/app/app.module.ts
#	Frontend/src/app/components/hot-deals-widget/hot-deals-widget.component.css
#	Frontend/src/app/pages/landingpage/landingpage.component.html
#	Frontend/src/styles.css
2021-05-20 10:46:06 +02:00
Reboooooorn
854cb454e6
BETTERZON-108 (#58)
* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

* wip: new component created and added to the app.module.ts. Added a minimal grid layout.

* wip: all components were wrapped now. Grid structure has been applied to the main wrapper-class "container".

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
2021-05-20 10:40:52 +02:00
Reboooooorn
e4565f7435
BETTERZON-109 (#57)
* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

* wip: component rewritten, simple grid applied.

* wip: new component created and added to the app.module.ts. Added a minimal grid layout.

* wip: all components were wrapped now. Grid structure has been applied to the main wrapper-class "container".

* wip: component created and added to the app.module.ts

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
2021-05-20 10:36:52 +02:00
Reboooooorn
fc9c7f63cf
BETTERZON-78 (#39)
* BETTERZON-31, dependencies.

* BETTERZON-31: Fixing dependencies

* BETTERZON-31,
BETTERZON-50

info popover and footer had been changed.

* BETTERZON-74

simple top-bar has been created.

* WIP: creating footer using grid.

* BETTERZON-78 adding bottom bar and top bar

* Adding cookieconsent as dependency again since it was removed by a merge

* Adding cookieconsent as dependency again since it was removed by a merge

* Apply suggestions from code review

Switching from single to double quotes

* BETTERZON-78 - grid added, structured as in Adobe XD mockup

Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
2021-05-20 10:26:10 +02:00
Patrick
e7543e6430
BETTERZON-107: Refactoring code with Proxy as design pattern (#49) 2021-05-20 10:23:41 +02:00
Patrick
ead1f10b25
BETTERZON-110: Refactoring, reformatting and commenting api service (#56) 2021-05-20 10:20:50 +02:00
Patrick
712f6c9034
BETTERZON-101: Adding service functions for pricealarms api (#55)
- Not properly tested though as login functionality is required to test but not yet implemented
2021-05-19 19:46:30 +02:00
Patrick
1581184b57
BETTERZON-96: Adding API endpoint for delisting a whole vendor (#54) 2021-05-19 09:08:52 +02:00
henningxtro
26ba21156a
BETTERZON-58 (#53)
* BETTERZON-58: Basic Functionality with scrapy

* Added independent crawler function, yielding price

* moved logic to amazon.py

* .

* moved scrapy files to unused folder

* Added basic amazon crawler using beautifulsoup4

* Connected Api to Crawler

* Fixed string concatenation for sql statement in getProductLinksForProduct

* BETTERZON-58: Fixing SQL insert

* BETTERZON-58: Adding access key verification

* BETTERZON-58: Fixing API endpoint of the crawler
- The list of products in the API request was treated like a string and henceforth, only the first product has been crawled

* Added another selector for price on amazon (does not work for books)

Co-authored-by: root <root@DESKTOP-ARBPL82.localdomain>
Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-authored-by: Patrick <50352812+Mueller-Patrick@users.noreply.github.com>
2021-05-19 00:46:14 +02:00
Patrick
3ae68b3df3
BETTERZON-95: Adding API endpoint for getting, inserting and updating contact persons (#52) 2021-05-18 21:14:00 +02:00
Patrick
8f17ae7896
BETTERZON-98: Adding API endpoint for adding price entries as a registered vendor manager (#51) 2021-05-18 00:40:24 +02:00
Patrick
16ed1070c2
BETTERZON-97: Adding API endpoint to get all products listed by a specific vendor (#50) 2021-05-18 00:24:00 +02:00
Patrick
061d1a46e0
BETTERZON-94: Adding API endpoint to deactivate price listings as a vendor manager (#48) 2021-05-16 13:11:22 +02:00
Patrick
b185e4e5e3
BETTERZON-93: Adding API endpoint to get managed shops (#47) 2021-05-16 12:28:11 +02:00
Patrick
cb55cae692
BETTERZON-100: Switching to cookies for session management (#46)
* BETTERZON-100: Switching session handling to cookies

* BETTERZON-100: Some code reformatting

* BETTERZON-100: Some more code reformatting
2021-05-13 18:47:50 +02:00
Patrick
5cc91654c3
BETTERZON-99: Adding some basic cucumber tests (#45) 2021-05-13 16:42:55 +02:00
Patrick
0be394fc1d
BETTERZON-92: Adding API endpoint to edit (update) price alarms (#44) 2021-05-13 00:48:56 +02:00
Patrick
cd0c11dbc7
BETTERZON-91: Adding API endpoint to GET all price alarms for the currently logged in user (#43) 2021-05-13 00:29:01 +02:00
Patrick
f333bbfc05
BETTERZON-90: Adding API endpoint for creating price alarms (#42) 2021-05-12 23:57:24 +02:00
59 changed files with 6534 additions and 431 deletions

4984
Backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/cookie-parser": "^1.4.2",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",

View File

@ -14,6 +14,10 @@ import {vendorsRouter} from './models/vendors/vendors.router';
import {errorHandler} from './middleware/error.middleware'; import {errorHandler} from './middleware/error.middleware';
import {notFoundHandler} from './middleware/notFound.middleware'; import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router'; 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(); dotenv.config();
@ -38,12 +42,15 @@ const app = express();
app.use(helmet()); app.use(helmet());
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(cookieParser());
app.use('/products', productsRouter); app.use('/products', productsRouter);
app.use('/categories', categoriesRouter); app.use('/categories', categoriesRouter);
app.use('/manufacturers', manufacturersRouter); app.use('/manufacturers', manufacturersRouter);
app.use('/prices', pricesRouter); app.use('/prices', pricesRouter);
app.use('/users', usersRouter); app.use('/users', usersRouter);
app.use('/vendors', vendorsRouter); app.use('/vendors', vendorsRouter);
app.use('/pricealarms', pricealarmsRouter);
app.use('/contactpersons', contactpersonsRouter);
app.use(errorHandler); app.use(errorHandler);
app.use(notFoundHandler); app.use(notFoundHandler);

View File

@ -1,5 +1,5 @@
import HttpException from "../common/http-exception"; import HttpException from '../common/http-exception';
import { Request, Response, NextFunction } from "express"; import {Request, Response, NextFunction} from 'express';
export const errorHandler = ( export const errorHandler = (
error: HttpException, error: HttpException,
@ -9,7 +9,7 @@ export const errorHandler = (
) => { ) => {
const status = error.statusCode || 500; const status = error.statusCode || 500;
const message = 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); response.status(status).send(message);
}; };

View File

@ -1,4 +1,4 @@
import { Request, Response, NextFunction } from "express"; import {Request, Response, NextFunction} from 'express';
export const notFoundHandler = ( export const notFoundHandler = (
request: Request, request: Request,
@ -6,7 +6,7 @@ export const notFoundHandler = (
next: NextFunction next: NextFunction
) => { ) => {
const message = "Resource not found"; const message = 'Resource not found';
response.status(404).send(message); response.status(404).send(message);
}; };

View File

@ -27,7 +27,7 @@ categoriesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(categories); res.status(200).send(categories);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(category);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(categories);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
import {Contact_Person} from './contact_person.interface';
export interface Contact_Persons {
[key: number]: Contact_Person;
}

View File

@ -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.'}));
}
});

View File

@ -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<Contact_Persons> => {
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<Contact_Person> => {
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<Contact_Persons> => {
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<Boolean> => {
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<Boolean> => {
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();
}
}
};

View File

@ -19,7 +19,7 @@ export const manufacturersRouter = express.Router();
* Controller Definitions * Controller Definitions
*/ */
// GET items/ // GET manufacturers/
manufacturersRouter.get('/', async (req: Request, res: Response) => { manufacturersRouter.get('/', async (req: Request, res: Response) => {
try { try {
const manufacturers: Manufacturers = await ManufacturerService.findAll(); const manufacturers: Manufacturers = await ManufacturerService.findAll();
@ -27,11 +27,11 @@ manufacturersRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(manufacturers); res.status(200).send(manufacturers);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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) => { manufacturersRouter.get('/:id', async (req: Request, res: Response) => {
const id: number = parseInt(req.params.id, 10); 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); res.status(200).send(manufacturer);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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) => { manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term; const term: string = req.params.term;
@ -65,6 +65,6 @@ manufacturersRouter.get('/search/:term', async (req: Request, res: Response) =>
res.status(200).send(manufacturer); res.status(200).send(manufacturer);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -0,0 +1,6 @@
export interface PriceAlarm {
alarm_id: number;
user_id: number;
product_id: number;
defined_price: number;
}

View File

@ -0,0 +1,5 @@
import {PriceAlarm} from './pricealarm.interface';
export interface PriceAlarms {
[key: number]: PriceAlarm;
}

View File

@ -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.'}));
}
});

View File

@ -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<Boolean> => {
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<PriceAlarms> => {
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<Boolean> => {
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;
};

View File

@ -4,7 +4,24 @@ export interface Price {
vendor_id: number; vendor_id: number;
price_in_cents: number; price_in_cents: number;
timestamp: Date; 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;
}
} }

View File

@ -6,6 +6,7 @@ import express, {Request, Response} from 'express';
import * as PriceService from './prices.service'; import * as PriceService from './prices.service';
import {Price} from './price.interface'; import {Price} from './price.interface';
import {Prices} from './prices.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); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(price);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -15,7 +15,7 @@ const pool = mariadb.createPool({
* Data Model Interfaces * Data Model Interfaces
*/ */
import {Price} from './price.interface'; import {Deal, Price} from './price.interface';
import {Prices} from './prices.interface'; import {Prices} from './prices.interface';
@ -31,7 +31,7 @@ export const findAll = async (): Promise<Prices> => {
let priceRows = []; let priceRows = [];
try { try {
conn = await pool.getConnection(); 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
let price: Price = { let price: Price = {
@ -72,7 +72,7 @@ export const find = async (id: number): Promise<Price> => {
let price: any; let price: any;
try { try {
conn = await pool.getConnection(); 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
price = rows[row]; price = rows[row];
@ -99,7 +99,7 @@ export const findByProduct = async (product: number): Promise<Prices> => {
let priceRows = []; let priceRows = [];
try { try {
conn = await pool.getConnection(); 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
priceRows.push(rows[row]); priceRows.push(rows[row]);
@ -142,16 +142,17 @@ export const findByType = async (product: string, type: string): Promise<Prices>
'PARTITION BY p.vendor_id ' + 'PARTITION BY p.vendor_id ' +
'ORDER BY p.timestamp DESC) AS rk ' + 'ORDER BY p.timestamp DESC) AS rk ' +
'FROM prices p ' + '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.* ' + 'SELECT s.* ' +
'FROM summary s ' + 'FROM summary s ' +
'WHERE s.rk = 1 '), product); 'WHERE s.rk = 1 '), product);
} else if (type === 'lowest') { } else if (type === 'lowest') {
// Used to get the lowest prices for this product over a period of time // 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 { } else {
// If no type is given, return all prices for this product // 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) { for (let row in rows) {
@ -188,13 +189,13 @@ export const findByVendor = async (product: string, vendor: string, type: string
let rows = []; let rows = [];
if (type === 'newest') { if (type === 'newest') {
// Used to get the newest price for this product and vendor // 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') { } else if (type === 'lowest') {
// Used to get the lowest prices for this product and vendor in all time // 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 { } else {
// If no type is given, return all prices for this product and vendor // 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) { for (let row in rows) {
@ -237,7 +238,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
' ROW_NUMBER() OVER(\n' + ' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' + ' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\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' + 'SELECT s.*\n' +
'FROM summary s\n' + 'FROM summary s\n' +
'WHERE s.rk = 1'); 'WHERE s.rk = 1');
@ -254,7 +255,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
} }
// Iterate over all prices to find the products with the biggest difference between amazon and other vendor // Iterate over all prices to find the products with the biggest difference between amazon and other vendor
let deals: Price[] = []; let deals: Deal[] = [];
Object.keys(allPrices).forEach(productId => { Object.keys(allPrices).forEach(productId => {
if (allPrices[parseInt(productId)]) { if (allPrices[parseInt(productId)]) {
@ -286,7 +287,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
// Push only deals were the amazon price is actually higher // Push only deals were the amazon price is actually higher
if (deal.amazonDifferencePercent > 0) { if (deal.amazonDifferencePercent > 0) {
deals.push(deal as Price); deals.push(deal as Deal);
} }
} }
}); });
@ -298,10 +299,8 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
let maxAmt = Math.min(amount, deals.length); let maxAmt = Math.min(amount, deals.length);
for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) { for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) {
//console.log(deals[dealIndex]);
priceRows.push(deals[dealIndex] as Price); priceRows.push(deals[dealIndex] as Price);
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
throw err; throw err;
@ -316,7 +315,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
/** /**
* Fetches and returns the lowest, latest, non-amazon price for each given product * 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<Prices> => { export const findListByProducts = async (productIds: [number]): Promise<Prices> => {
let conn; let conn;
@ -336,9 +335,9 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
' ROW_NUMBER() OVER(\n' + ' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' + ' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\n' + ' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p' + ' FROM prices p LEFT OUTER JOIN vendors v ON v.vendor_id = p.vendor_id ' +
' WHERE p.product_id IN (?)' + ' WHERE p.product_id IN (?) AND v.isActive = true' +
' AND p.vendor_id != 1)\n' + ' AND p.vendor_id != 1 AND active_listing = true)\n' +
'SELECT s.*\n' + 'SELECT s.*\n' +
'FROM summary s\n' + 'FROM summary s\n' +
'WHERE s.rk = 1', [productIds]); 'WHERE s.rk = 1', [productIds]);
@ -366,7 +365,6 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
priceRows.push(pricesForProd[0]); priceRows.push(pricesForProd[0]);
} }
}); });
} catch (err) { } catch (err) {
throw err; throw err;
} finally { } finally {
@ -377,3 +375,28 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
return priceRows; return priceRows;
}; };
export const createPriceEntry = async (user_id: number, vendor_id: number, product_id: number, price_in_cents: number): Promise<Boolean> => {
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();
}
}
};

View File

@ -27,7 +27,7 @@ productsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(products); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(product);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -159,3 +159,41 @@ export const findList = async (ids: [number]): Promise<Products> => {
return prodRows; 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<Products> => {
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;
};

View File

@ -47,10 +47,13 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
const session: Session = await UserService.createUser(username, password, email, ip); const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user // 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) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 // Update the user entry and create a session
const session: Session = await UserService.login(username, password, ip); const session: Session = await UserService.login(username, password, ip);
if(!session.session_id) { if (!session.session_id) {
// Error logging in, probably wrong username / password // 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; return;
} }
// Send the session details back to the user // 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) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 // POST users/checkSessionValid
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
try { try {
const sessionId: string = req.body.sessionId;
const sessionKey: string = req.body.sessionKey;
const ip: string = req.connection.remoteAddress ?? ''; 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 // 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 // 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; return;
} }
@ -110,6 +108,6 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
res.status(201).send(user); res.status(201).send(user);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -68,7 +68,7 @@ export const createUser = async (username: string, password: string, email: stri
return { return {
session_id: sessionId, session_id: sessionId,
session_key: sessionKey, session_key: sessionKey,
session_key_hash: '', session_key_hash: 'HIDDEN',
last_IP: ip last_IP: ip
}; };
@ -135,7 +135,7 @@ export const login = async (username: string, password: string, ip: string): Pro
return { return {
session_id: sessionId, session_id: sessionId,
session_key: sessionKey, session_key: sessionKey,
session_key_hash: '', session_key_hash: 'HIDDEN',
last_IP: ip last_IP: ip
}; };
@ -179,7 +179,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
// Key is valid, continue // Key is valid, continue
// Check if the session is still valid // Check if the session is still valid
if(validUntil <= new Date()) { if (validUntil <= new Date()) {
// Session expired, return invalid // Session expired, return invalid
return {} as User; return {} as User;
} }
@ -193,7 +193,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
await conn.commit(); await conn.commit();
// Get the other required user information and update the user // 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); const userRows = await conn.query(userQuery, userId);
let username = ''; let username = '';
let email = ''; let email = '';
@ -213,7 +213,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
user_id: userId, user_id: userId,
username: username, username: username,
email: email, email: email,
password_hash: '', password_hash: 'HIDDEN',
registration_date: registrationDate, registration_date: registrationDate,
last_login_date: lastLoginDate last_login_date: lastLoginDate
}; };
@ -229,6 +229,20 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
return {} as User; 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<User> => {
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 * Used in the checkUsernameAndEmail method as return value
*/ */

View File

@ -6,6 +6,7 @@ import express, {Request, Response} from 'express';
import * as VendorService from './vendors.service'; import * as VendorService from './vendors.service';
import {Vendor} from './vendor.interface'; import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface'; import {Vendors} from './vendors.interface';
import * as UserService from '../users/users.service';
/** /**
@ -19,7 +20,7 @@ export const vendorsRouter = express.Router();
* Controller Definitions * Controller Definitions
*/ */
// GET items/ // GET vendors/
vendorsRouter.get('/', async (req: Request, res: Response) => { vendorsRouter.get('/', async (req: Request, res: Response) => {
try { try {
const vendors: Vendors = await VendorService.findAll(); const vendors: Vendors = await VendorService.findAll();
@ -27,11 +28,27 @@ vendorsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(vendors); res.status(200).send(vendors);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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) => { vendorsRouter.get('/:id', async (req: Request, res: Response) => {
const id: number = parseInt(req.params.id, 10); 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); res.status(200).send(vendor);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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) => { vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term; const term: string = req.params.term;
@ -65,6 +82,76 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(vendors); res.status(200).send(vendors);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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.'}));
} }
}); });

View File

@ -17,6 +17,7 @@ const pool = mariadb.createPool({
import {Vendor} from './vendor.interface'; import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface'; import {Vendors} from './vendors.interface';
import {User} from '../users/user.interface';
/** /**
@ -31,7 +32,7 @@ export const findAll = async (): Promise<Vendors> => {
let vendorRows = []; let vendorRows = [];
try { try {
conn = await pool.getConnection(); 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
let vendor: Vendor = { let vendor: Vendor = {
@ -78,7 +79,7 @@ export const find = async (id: number): Promise<Vendor> => {
let vendor: any; let vendor: any;
try { try {
conn = await pool.getConnection(); 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
vendor = rows[row]; vendor = rows[row];
@ -106,7 +107,7 @@ export const findBySearchTerm = async (term: string): Promise<Vendors> => {
try { try {
conn = await pool.getConnection(); conn = await pool.getConnection();
term = '%' + term + '%'; 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) { for (let row in rows) {
if (row !== 'meta') { if (row !== 'meta') {
vendorRows.push(rows[row]); vendorRows.push(rows[row]);
@ -123,3 +124,93 @@ export const findBySearchTerm = async (term: string): Promise<Vendors> => {
return vendorRows; 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<Vendors> => {
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<Boolean> => {
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<Boolean> => {
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;
};

View File

@ -1,32 +1,32 @@
const webpack = require("webpack"); const webpack = require('webpack');
const path = require("path"); const path = require('path');
const nodeExternals = require("webpack-node-externals"); const nodeExternals = require('webpack-node-externals');
module.exports = { module.exports = {
entry: ["webpack/hot/poll?100", "./src/index.ts"], entry: ['webpack/hot/poll?100', './src/index.ts'],
watch: false, watch: false,
target: "node", target: 'node',
externals: [ externals: [
nodeExternals({ nodeExternals({
whitelist: ["webpack/hot/poll?100"] whitelist: ['webpack/hot/poll?100']
}) })
], ],
module: { module: {
rules: [ rules: [
{ {
test: /.tsx?$/, test: /.tsx?$/,
use: "ts-loader", use: 'ts-loader',
exclude: /node_modules/ exclude: /node_modules/
} }
] ]
}, },
mode: "development", mode: 'development',
resolve: { resolve: {
extensions: [".tsx", ".ts", ".js"] extensions: ['.tsx', '.ts', '.js']
}, },
plugins: [new webpack.HotModuleReplacementPlugin()], plugins: [new webpack.HotModuleReplacementPlugin()],
output: { output: {
path: path.join(__dirname, "dist"), path: path.join(__dirname, 'dist'),
filename: "index.js" filename: 'index.js'
} }
}; };

View File

@ -2,13 +2,12 @@
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="FacetManager"> <component name="FacetManager">
<facet type="Python" name="Python"> <facet type="Python" name="Python">
<configuration sdkName="Python 3.9" /> <configuration sdkName="Python 3.9 (venv)" />
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
</component> </component>
</module> </module>

View File

@ -1,13 +1,17 @@
import os
from flask import Flask from flask import Flask
from flask_restful import Resource, Api, reqparse from flask_restful import Resource, Api, reqparse
import crawler
app = Flask(__name__) app = Flask(__name__)
api = Api(app) api = Api(app)
# To parse request data # To parse request data
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('key') parser.add_argument('key', type=str)
parser.add_argument('products') parser.add_argument('products', type=int, action='append')
class CrawlerApi(Resource): class CrawlerApi(Resource):
@ -17,7 +21,12 @@ class CrawlerApi(Resource):
def post(self): def post(self):
# Accept crawler request here # Accept crawler request here
args = parser.parse_args() 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, '/') api.add_resource(CrawlerApi, '/')

View File

@ -1,78 +1,107 @@
import sql import sql
import requests
from bs4 import BeautifulSoup
def crawl(product_ids: [int]) -> dict:
""" HEADERS = ({'User-Agent':
Crawls the given list of products and saves the results to sql 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 '
:param products: The list of product IDs to fetch 'Safari/537.36'})
:return: A dict with the following fields:
total_crawls: number of total crawl tries (products * vendors per product)
successful_crawls: number of successful products def crawl(product_ids: [int]) -> dict:
products_with_problems: list of products that have not been crawled successfully """
""" Crawls the given list of products and saves the results to sql
total_crawls = 0 :param products: The list of product IDs to fetch
successful_crawls = 0 :return: A dict with the following fields:
products_with_problems = [] total_crawls: number of total crawl tries (products * vendors per product)
successful_crawls: number of successful products
# Iterate over every product that has to be crawled products_with_problems: list of products that have not been crawled successfully
for product_id in product_ids: """
# Get all links for this product total_crawls = 0
product_links = sql.getProductLinksForProduct(product_id) successful_crawls = 0
products_with_problems = []
crawled_data = []
# Iterate over every product that has to be crawled
# Iterate over every link / vendor for product_id in product_ids:
for product_vendor_info in product_links: # Get all links for this product
total_crawls += 1 product_links = sql.getProductLinksForProduct(product_id)
# Call the appropriate vendor crawling function and append the result to the list of crawled data crawled_data = []
if product_vendor_info['vendor_id'] == 1:
# Amazon # Iterate over every link / vendor
crawled_data.append(__crawl_amazon__(product_vendor_info)) for product_vendor_info in product_links:
elif product_vendor_info['vendor_id'] == 2: total_crawls += 1
# Apple
crawled_data.append(__crawl_apple__(product_vendor_info)) # Call the appropriate vendor crawling function and append the result to the list of crawled data
elif product_vendor_info['vendor_id'] == 3: if product_vendor_info['vendor_id'] == 1:
# Media Markt # Amazon
crawled_data.append(__crawl_mediamarkt__(product_vendor_info)) data = __crawl_amazon__(product_vendor_info)
else: if data:
products_with_problems.append(product_vendor_info) crawled_data.append(data)
continue elif product_vendor_info['vendor_id'] == 2:
# Apple
successful_crawls += 1 data = __crawl_apple__(product_vendor_info)
if data:
# Insert data to SQL crawled_data.append(data)
sql.insertData(crawled_data) elif product_vendor_info['vendor_id'] == 3:
# Media Markt
return { data = __crawl_mediamarkt__(product_vendor_info)
'total_crawls': total_crawls, if data:
'successful_crawls': successful_crawls, crawled_data.append(data)
'products_with_problems': products_with_problems else:
} products_with_problems.append(product_vendor_info)
continue
def __crawl_amazon__(product_info: dict) -> tuple: successful_crawls += 1
"""
Crawls the price for the given product from amazon # Insert data to SQL
:param product_info: A dict with product info containing product_id, vendor_id, url sql.insertData(crawled_data)
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents)
""" return {
return (product_info['product_id'], product_info['vendor_id'], 123) 'total_crawls': total_crawls,
'successful_crawls': successful_crawls,
'products_with_problems': products_with_problems
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 def __crawl_amazon__(product_info: dict) -> tuple:
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) """
""" Crawls the price for the given product from amazon
return (product_info['product_id'], product_info['vendor_id'], 123) :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)
"""
def __crawl_mediamarkt__(product_info: dict) -> tuple: page = requests.get(product_info['url'], headers=HEADERS)
""" soup = BeautifulSoup(page.content, features="lxml")
Crawls the price for the given product from media markt try:
:param product_info: A dict with product info containing product_id, vendor_id, url price = int(
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents) soup.find(id='priceblock_ourprice').get_text().replace(".", "").replace(",", "").replace("", "").strip())
""" if not price:
pass 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

View File

@ -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}

View File

@ -1,5 +1,7 @@
pymysql pymysql
flask flask==1.1.2
flask-sqlalchemy flask-sqlalchemy
flask_restful flask_restful
scrapy beautifulsoup4
requests
lxml

View File

@ -54,7 +54,6 @@ def getProductLinksForProduct(product_id: int) -> [dict]:
cur = conn.cursor() cur = conn.cursor()
query = 'SELECT vendor_id, url FROM product_links WHERE product_id = %s' query = 'SELECT vendor_id, url FROM product_links WHERE product_id = %s'
cur.execute(query, (product_id,)) cur.execute(query, (product_id,))
products = list(map(lambda x: {'product_id': product_id, 'vendor_id': x[0], 'url': x[1]}, cur.fetchall())) products = list(map(lambda x: {'product_id': product_id, 'vendor_id': x[0], 'url': x[1]}, cur.fetchall()))

View File

@ -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()

View File

@ -8,4 +8,4 @@ default = crawler.settings
[deploy] [deploy]
#url = http://localhost:6800/ #url = http://localhost:6800/
project = crawler project = crawler

View File

@ -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}

View File

@ -10,17 +10,24 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="jdk" jdkName="openjdk-16" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-java:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-core:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: info.cukes:cucumber-html:0.2.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-jvm-deps:1.0.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:gherkin:5.0.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:tag-expressions:1.1.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-junit:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" /> <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-java:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-core:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin-messages:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:messages:15.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:tag-expressions:3.0.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-expressions:10.3.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:datatable:3.5.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-plugin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:docstring:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:html-formatter:13.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:create-meta:4.0.0" level="project" />
<orderEntry type="library" name="Maven: org.apiguardian:apiguardian-api:1.1.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-junit:6.10.3" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven.plugins:maven-compiler-plugin:3.8.1" level="project" /> <orderEntry type="library" name="Maven: org.apache.maven.plugins:maven-compiler-plugin:3.8.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-plugin-api:3.0" level="project" /> <orderEntry type="library" name="Maven: org.apache.maven:maven-plugin-api:3.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-model:3.0" level="project" /> <orderEntry type="library" name="Maven: org.apache.maven:maven-model:3.0" level="project" />
@ -53,5 +60,25 @@
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-api:2.8.4" level="project" /> <orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-api:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-manager:2.8.4" level="project" /> <orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-manager:2.8.4" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.codehaus.plexus:plexus-compiler-javac:2.8.4" level="project" /> <orderEntry type="library" scope="RUNTIME" name="Maven: org.codehaus.plexus:plexus-compiler-javac:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-java:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-api:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-chrome-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-edge-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-firefox-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-ie-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-opera-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-remote-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-safari-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-support:3.141.59" level="project" />
<orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.8.15" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-exec:1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:25.0-jre" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: org.checkerframework:checker-compat-qual:2.0.0" level="project" />
<orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.1" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.mojo:animal-sniffer-annotations:1.14" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okhttp3:okhttp:3.11.0" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okio:okio:1.14.0" level="project" />
</component> </component>
</module> </module>

View File

@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.taskhub</groupId> <groupId>xyz.betterzon</groupId>
<artifactId>CucumberTests</artifactId> <artifactId>CucumberTests</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<properties> <properties>
@ -13,21 +13,30 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>io.cucumber</groupId> <groupId>junit</groupId>
<artifactId>cucumber-java</artifactId> <artifactId>junit</artifactId>
<version>2.3.1</version> <version>4.12</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.10.3</version>
</dependency>
<dependency> <dependency>
<groupId>io.cucumber</groupId> <groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId> <artifactId>cucumber-junit</artifactId>
<version>2.3.1</version> <version>6.10.3</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <version>3.8.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,6 +1,10 @@
import cucumber.api.CucumberOptions; import io.cucumber.junit.Cucumber;
import cucumber.api.junit.Cucumber; import io.cucumber.junit.CucumberOptions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.openqa.selenium.firefox.FirefoxDriver;
import stepdefs.Preconditions;
@RunWith(Cucumber.class) @RunWith(Cucumber.class)
@CucumberOptions( @CucumberOptions(
@ -9,4 +13,13 @@ import org.junit.runner.RunWith;
) )
public class RunTest { public class RunTest {
@BeforeClass
public static void setup() {
Preconditions.driver= new FirefoxDriver();
}
@AfterClass
public static void teardown() {
Preconditions.driver.close();
}
} }

View File

@ -0,0 +1,7 @@
package stepdefs;
import org.openqa.selenium.WebDriver;
public class Preconditions {
public static WebDriver driver;
}

View File

@ -1,67 +1,68 @@
package stepdefs; package stepdefs;
import cucumber.api.java.en.Given; import io.cucumber.java.PendingException;
import cucumber.api.java.en.Then; import io.cucumber.java.en.Given;
import cucumber.api.java.en.When; import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
public class PriceAlarm { public class PriceAlarm {
@Given("^the user has at least (\\d+) price alarm set$") @Given("^the user has at least (\\d+) price alarm set$")
public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception { public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception {
} }
@When("^the user clicks on the profile icon$") @When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception { public void the_user_clicks_on_the_profile_icon() throws Exception {
} }
@Then("^the profile details popup should open$") @Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception { public void the_profile_details_popup_should_open() throws Exception {
} }
@When("^the user clicks on price alarms$") @When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception { public void the_user_clicks_on_price_alarms() throws Exception {
} }
@Then("^the price alarm list should open$") @Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception { public void the_price_alarm_list_should_open() throws Exception {
} }
@Then("^the price alarm list should contain at least (\\d+) entry$") @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 { 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$") @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 { 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$") @Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception { 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$") @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 { 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$") @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 { 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$") @When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception { public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
} }
@Then("^the price alarm should be removed from the database$") @Then("^the price alarm should be removed from the database$")
public void the_price_alarm_should_be_removed_from_the_database() throws Exception { 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$") @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 { public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception {
} }
@When("^the user clicks on the \"([^\"]*)\" button$") @When("^the user clicks on the \"([^\"]*)\" button$")
public void the_user_clicks_on_the_button(String arg1) throws Exception { public void the_user_clicks_on_the_button(String arg1) throws Exception {
} }
@Then("^the price alarm should be updated in the database$") @Then("^the price alarm should be updated in the database$")
public void the_price_alarm_should_be_updated_in_the_database() throws Exception { public void the_price_alarm_should_be_updated_in_the_database() throws Exception {
} }
} }

View File

@ -1,52 +1,72 @@
package stepdefs; package stepdefs;
import cucumber.api.PendingException; import io.cucumber.java.PendingException;
import cucumber.api.java.en.Given; import io.cucumber.java.en.Given;
import cucumber.api.java.en.Then; import io.cucumber.java.en.Then;
import cucumber.api.java.en.When; 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 { public class SearchProduct {
@Given("^the user is on the landing page$") @Given("^the user is on the landing page$")
public void the_user_is_on_the_landing_page() throws Exception { 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$") @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 { 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 \"([^\"]*)\"$") @Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception { 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$") @Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception { public void the_user_is_not_logged_in() throws Exception {
} }
@Given("^the user is logged in$") @Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception { public void the_user_is_logged_in() throws Exception {
} }
@Then("^the user should see a list of products$") @Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception { 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$") @When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception { public void the_user_clicks_on_the_first_product() throws Exception {
} }
@Then("^the user should see the product detail page$") @Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception { public void the_user_should_see_the_product_detail_page() throws Exception {
} }
@Then("^the set price alarm box should show \"([^\"]*)\"$") @Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception { public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
} }
@When("^the user sets a price alarm$") @When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception { public void the_user_sets_a_price_alarm() throws Exception {
} }
@Then("^the user should receive an email confirming the price alarm$") @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 { public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
} }
} }

View File

@ -1,28 +1,28 @@
Feature: Price Alarms Feature: Price Alarms
Scenario: Show a list of price alarms Scenario: Show a list of price alarms
Given the user is on the landing page Given the user is on the landing page
And the user is logged in And the user is logged in
And the user has at least 1 price alarm set And the user has at least 1 price alarm set
When the user clicks on the profile icon When the user clicks on the profile icon
Then the profile details popup should open Then the profile details popup should open
When the user clicks on price alarms When the user clicks on price alarms
Then the price alarm list should open 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 at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page And the price alarm list should contain a maximum of 20 entries per page
Scenario: Remove a price alarm Scenario: Remove a price alarm
Given the user is on the price alarm list page Given the user is on the price alarm list page
And the user is logged in And the user is logged in
When the user clicks on the "remove" button next to a price alarm 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 Then a popup should open asking the user to confirm the removal
When the user confirms the removal of the price alarm When the user confirms the removal of the price alarm
Then the price alarm should be removed from the database Then the price alarm should be removed from the database
Scenario: Edit a price alarm Scenario: Edit a price alarm
Given the user is on the price alarm list page Given the user is on the price alarm list page
And the user is logged in And the user is logged in
When the user clicks on the "edit" button next to a price alarm 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 Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button When the user clicks on the "save changes" button
Then the price alarm should be updated in the database Then the price alarm should be updated in the database

View File

@ -1,26 +1,26 @@
Feature: Search a Product Feature: Search a Product
Scenario: User searches for unknown product Scenario: User searches for unknown product
Given the user is on the landing page Given the user is on the landing page
When the user enters the search term "iPhone 13" and clicks search When the user enters the search term "iPhone 13" and clicks search
Then the user should see the error page "No products found" Then the user should see the error page "No products found"
Scenario: User is not logged in, searches for known product Scenario: User is not logged in, searches for known product
Given the user is on the landing page Given the user is on the landing page
And the user is not logged in And the user is not logged in
When the user enters the search term "iPhone 12" and clicks search When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products Then the user should see a list of products
When the user clicks on the first product When the user clicks on the first product
Then the user should see the product detail page Then the user should see the product detail page
And the set price alarm box should show "Log in to continue" And the set price alarm box should show "Log in to continue"
Scenario: User is logged in, searches for known product Scenario: User is logged in, searches for known product
Given the user is on the landing page Given the user is on the landing page
And the user is logged in And the user is logged in
When the user enters the search term "iPhone 12" and clicks search When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products Then the user should see a list of products
When the user clicks on the first product When the user clicks on the first product
Then the user should see the product detail page Then the user should see the product detail page
And the set price alarm box should show "Set price alarm" And the set price alarm box should show "Set price alarm"
When the user sets a price alarm When the user sets a price alarm
Then the user should receive an email confirming the price alarm Then the user should receive an email confirming the price alarm

View File

@ -1,7 +1,13 @@
<app-top-bar></app-top-bar> <div class="container">
<div class="page-content"> <div class="header">
<router-outlet></router-outlet> <app-top-bar></app-top-bar>
</div>
<div class="page-content">
<router-outlet></router-outlet>
</div>
<div class="footer">
<app-bottom-bar></app-bottom-bar>
</div>
</div> </div>
<app-bottom-bar></app-bottom-bar>

View File

@ -30,6 +30,7 @@ import {MatSidenavModule} from '@angular/material/sidenav';
import {MatListModule} from "@angular/material/list"; import {MatListModule} from "@angular/material/list";
import {BottomBarComponent} from './components/bottom-bar/bottom-bar.component'; import {BottomBarComponent} from './components/bottom-bar/bottom-bar.component';
import { HotDealsWidgetComponent } from './components/hot-deals-widget/hot-deals-widget.component'; import { HotDealsWidgetComponent } from './components/hot-deals-widget/hot-deals-widget.component';
import { SliderForProductsComponent } from './components/slider-for-products/slider-for-products.component';
// For cookie popup // For cookie popup
@ -87,7 +88,8 @@ const cookieConfig: NgcCookieConsentConfig = {
PrivacyComponent, PrivacyComponent,
TopBarComponent, TopBarComponent,
BottomBarComponent, BottomBarComponent,
HotDealsWidgetComponent HotDealsWidgetComponent,
SliderForProductsComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1 @@
<p>slider-for-products works!</p>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SliderForProductsComponent } from './slider-for-products.component';
describe('SliderForProductsComponent', () => {
let component: SliderForProductsComponent;
let fixture: ComponentFixture<SliderForProductsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SliderForProductsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SliderForProductsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-slider-for-products',
templateUrl: './slider-for-products.component.html',
styleUrls: ['./slider-for-products.component.css']
})
export class SliderForProductsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,6 @@
export interface PriceAlarm {
alarm_id: number;
user_id: number;
product_id: number;
defined_price: number;
}

View File

@ -1,4 +1,6 @@
<div id="mainComponents"> <app-hot-deals-widget></app-hot-deals-widget>
<!--<div id="mainComponents">
<div id="searchContainer"> <div id="searchContainer">
<input type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()"> <input type="text" [(ngModel)]="searchInput" placeholder="Search" (keyup.enter)="startedSearch()">
</div> </div>
@ -14,3 +16,4 @@
</div> </div>
</div> </div>
</div> </div>
-->

View File

@ -5,6 +5,7 @@ import {Product} from '../models/product';
import {Price} from '../models/price'; import {Price} from '../models/price';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {Vendor} from '../models/vendor'; import {Vendor} from '../models/vendor';
import {PriceAlarm} from '../models/pricealarm';
@Injectable({ @Injectable({
providedIn: 'root' 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<Product> An observable containing a single product
*/
getProduct(id): Observable<Product> { getProduct(id): Observable<Product> {
try { try {
const prod = this.http.get<Product>((this.apiUrl + '/products/' + id)); return this.http.get<Product>((this.apiUrl + '/products/' + id));
return prod;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); 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<Product[]> An observable list of products
*/
getProductsByQuery(query): Observable<Product[]> { getProductsByQuery(query): Observable<Product[]> {
try { try {
const prods = this.http.get<Product[]>((this.apiUrl + '/products/search/' + query)); return this.http.get<Product[]>((this.apiUrl + '/products/search/' + query));
return prods;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
} }
} }
/**
* Gets a list of all products
* @return Observable<Product[]> An observable list of products
*/
getProducts(): Observable<Product[]> { getProducts(): Observable<Product[]> {
try { try {
const prods = this.http.get<Product[]>((this.apiUrl + '/products')); return this.http.get<Product[]>((this.apiUrl + '/products'));
return prods;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
} }
} }
/* ____ _
/ __ \_____(_)_______ _____
/ /_/ / ___/ / ___/ _ \/ ___/
/ ____/ / / / /__/ __(__ )
/_/ /_/ /_/\___/\___/____/
*/
/**
* Gets a list of all prices
* @return Observable<Price[]> An observable list of prices
*/
getPrices(): Observable<Price[]> { getPrices(): Observable<Price[]> {
try { try {
const prices = this.http.get<Price[]>((this.apiUrl + '/prices')); return this.http.get<Price[]>((this.apiUrl + '/prices'));
return prices;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); 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<Price[]> An observable list of prices
*/
getLowestPrices(productId): Observable<Price[]> { getLowestPrices(productId): Observable<Price[]> {
try { try {
let params = new HttpParams(); let params = new HttpParams();
params = params.append('product', productId); params = params.append('product', productId);
params = params.append('type', 'lowest'); params = params.append('type', 'lowest');
const prices = this.http.get<Price[]>((this.apiUrl + '/prices'), {params}); return this.http.get<Price[]>((this.apiUrl + '/prices'), {params});
return prices;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); 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<Price> An observable containing a single price
*/
getAmazonPrice(productId): Observable<Price> { getAmazonPrice(productId): Observable<Price> {
try { try {
let params = new HttpParams(); let params = new HttpParams();
params = params.append('product', productId); params = params.append('product', productId);
params = params.append('vendor', '1'); params = params.append('vendor', '1');
params = params.append('type', 'newest'); params = params.append('type', 'newest');
const price = this.http.get<Price>((this.apiUrl + '/prices'), {params}); return this.http.get<Price>((this.apiUrl + '/prices'), {params});
return price;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); 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<Price[]> An observable list of prices
*/
getCurrentPricePerVendor(productId): Observable<Price[]> { getCurrentPricePerVendor(productId): Observable<Price[]> {
try { try {
let params = new HttpParams(); let params = new HttpParams();
params = params.append('product', productId); params = params.append('product', productId);
params = params.append('type', 'newest'); params = params.append('type', 'newest');
const prices = this.http.get<Price[]>((this.apiUrl + '/prices'), {params}); return this.http.get<Price[]>((this.apiUrl + '/prices'), {params});
return prices;
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
} }
} }
/* _ __ __
| | / /__ ____ ____/ /___ __________
| | / / _ \/ __ \/ __ / __ \/ ___/ ___/
| |/ / __/ / / / /_/ / /_/ / / (__ )
|___/\___/_/ /_/\__,_/\____/_/ /____/
*/
/**
* Gets a list of all vendors
* @return Observable<Vendor[]> An observable list of vendors
*/
getVendors(): Observable<Vendor[]> { getVendors(): Observable<Vendor[]> {
try { try {
const vendors = this.http.get<Vendor[]>((this.apiUrl + '/vendors')); return this.http.get<Vendor[]>((this.apiUrl + '/vendors'));
return vendors; } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
}
}
/* ____ _ ___ __
/ __ \_____(_)_______ / | / /___ __________ ___ _____
/ /_/ / ___/ / ___/ _ \ / /| | / / __ `/ ___/ __ `__ \/ ___/
/ ____/ / / / /__/ __/ / ___ |/ / /_/ / / / / / / / (__ )
/_/ /_/ /_/\___/\___/ /_/ |_/_/\__,_/_/ /_/ /_/ /_/____/
*/
/**
* Gets a list of all price alarms
* @return Observable<PriceAlarm[]> An observable list of price alarms
*/
getPriceAlarms(): Observable<PriceAlarm[]> {
try {
return this.http.get<PriceAlarm[]>((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<any> The observable response of the api
*/
createPriceAlarms(productId: number, definedPrice: number): Observable<any> {
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<any> The observable response of the api
*/
updatePriceAlarms(alarmId: number, definedPrice: number): Observable<any> {
try {
return this.http.put((this.apiUrl + '/pricealarms'), JSON.stringify({
alarm_id: alarmId,
defined_price: definedPrice
}));
} catch (exception) { } catch (exception) {
process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@ -104,6 +104,32 @@ label {
padding-right: 4px; padding-right: 4px;
} }
/* Main Container */
.container {
position: fixed;
left: 50%;
margin-left: -820px;
width: 1640px;
height: auto;
display: grid;
grid-template-rows: 70px auto 210px;
}
.header {
grid-row: 1/2;
}
.page-content {
grid-row: 2/3;
}
.footer {
grid-row: 3/4;
}
/* End of Main Container */
/* Top Bar */ /* Top Bar */
app-top-bar { app-top-bar {