diff --git a/Backend/src/models/contact_persons/contact_persons.router.ts b/Backend/src/models/contact_persons/contact_persons.router.ts index 85c8035..b06f62d 100644 --- a/Backend/src/models/contact_persons/contact_persons.router.ts +++ b/Backend/src/models/contact_persons/contact_persons.router.ts @@ -89,9 +89,9 @@ contactpersonsRouter.post('/', async (req: Request, res: Response) => { const success = await ContactPersonService.createContactEntry(user.user_id, vendor_id, first_name, last_name, gender, email, phone); if (success) { - res.sendStatus(201); + res.status(201).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); @@ -118,9 +118,9 @@ contactpersonsRouter.put('/:id', async (req: Request, res: Response) => { 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); + res.status(200).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); diff --git a/Backend/src/models/crawling_status/crawling_status.router.ts b/Backend/src/models/crawling_status/crawling_status.router.ts index b442c26..4ca0b89 100644 --- a/Backend/src/models/crawling_status/crawling_status.router.ts +++ b/Backend/src/models/crawling_status/crawling_status.router.ts @@ -28,7 +28,7 @@ crawlingstatusRouter.get('/', async (req: Request, res: Response) => { const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip); if (!user.is_admin) { - res.sendStatus(403); + res.status(403).send({}); return; } diff --git a/Backend/src/models/prices/prices.router.ts b/Backend/src/models/prices/prices.router.ts index 6d3d389..16bc5ba 100644 --- a/Backend/src/models/prices/prices.router.ts +++ b/Backend/src/models/prices/prices.router.ts @@ -117,9 +117,9 @@ pricesRouter.post('/', async (req: Request, res: Response) => { const success = await PriceService.createPriceEntry(user.user_id, vendor_id, product_id, price_in_cents); if (success) { - res.sendStatus(201); + res.status(201).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); diff --git a/Backend/src/models/products/products.router.ts b/Backend/src/models/products/products.router.ts index 115d655..a30e8ef 100644 --- a/Backend/src/models/products/products.router.ts +++ b/Backend/src/models/products/products.router.ts @@ -120,7 +120,7 @@ productsRouter.post('/', async (req: Request, res: Response) => { const result: boolean = await ProductService.addNewProduct(asin); if (result) { - res.sendStatus(201); + res.status(201).send({}); } else { res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); } diff --git a/Backend/src/models/users/users.router.ts b/Backend/src/models/users/users.router.ts index fe67f1b..79c3034 100644 --- a/Backend/src/models/users/users.router.ts +++ b/Backend/src/models/users/users.router.ts @@ -50,7 +50,7 @@ usersRouter.post('/register', async (req: Request, res: Response) => { 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); + }), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).status(201).send({}); } catch (e) { console.log('Error handling a request: ' + e.message); res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); @@ -83,7 +83,7 @@ usersRouter.post('/login', async (req: Request, res: Response) => { 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); + }), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).status(200).send({}); } catch (e) { console.log('Error handling a request: ' + e.message); res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'})); diff --git a/Backend/src/models/vendors/vendors.router.ts b/Backend/src/models/vendors/vendors.router.ts index 20d2086..7888cae 100644 --- a/Backend/src/models/vendors/vendors.router.ts +++ b/Backend/src/models/vendors/vendors.router.ts @@ -100,9 +100,9 @@ vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Respons const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id); if (success) { - res.sendStatus(200); + res.status(200).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); @@ -123,9 +123,9 @@ vendorsRouter.put('/manage/shop/deactivate/:id', async (req: Request, res: Respo const success = await VendorService.setShopStatus(user.user_id, vendor_id, false); if (success) { - res.sendStatus(200); + res.status(200).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); @@ -146,9 +146,9 @@ vendorsRouter.put('/manage/shop/activate/:id', async (req: Request, res: Respons const success = await VendorService.setShopStatus(user.user_id, vendor_id, true); if (success) { - res.sendStatus(200); + res.status(200).send({}); } else { - res.sendStatus(500); + res.status(500).send({}); } } catch (e) { console.log('Error handling a request: ' + e.message); diff --git a/Frontend/angular.json b/Frontend/angular.json index 8940e9f..58d0b5a 100644 --- a/Frontend/angular.json +++ b/Frontend/angular.json @@ -92,7 +92,8 @@ "karmaConfig": "karma.conf.js", "codeCoverage": true, "codeCoverageExclude": [ - "src/app/mocks/mock.service.ts" + "src/app/mocks/mock.service.ts", + "src/app/services/api.service.ts" ], "assets": [ "src/favicon.ico", diff --git a/Frontend/src/app/components/product-details/product-details.component.ts b/Frontend/src/app/components/product-details/product-details.component.ts index 9f0fe14..54b9c00 100644 --- a/Frontend/src/app/components/product-details/product-details.component.ts +++ b/Frontend/src/app/components/product-details/product-details.component.ts @@ -47,7 +47,9 @@ export class ProductDetailsComponent implements OnInit { } getProduct(): void { - this.apiService.getProduct(this.productId).subscribe(product => {this.product = product}); + this.apiService.getProduct(this.productId).subscribe(product => { + this.product = product; + }); } getPrices(): void { diff --git a/Frontend/src/app/models/price.ts b/Frontend/src/app/models/price.ts index 49030e8..9a09e80 100644 --- a/Frontend/src/app/models/price.ts +++ b/Frontend/src/app/models/price.ts @@ -5,3 +5,24 @@ export interface Price { price_in_cents: number; timestamp: Date; } + +export class Deal implements Price { + price_id: number; + product_id: number; + vendor_id: number; + price_in_cents: number; + timestamp: Date; + amazonDifference: number; + amazonDifferencePercent: number; + + constructor(price_id: number, product_id: number, vendor_id: number, price_in_cents: number, timestamp: Date, amazonDifference: number, + amazonDifferencePercent: number) { + this.price_id = price_id; + this.product_id = product_id; + this.vendor_id = vendor_id; + this.price_in_cents = price_in_cents; + this.timestamp = timestamp; + this.amazonDifference = amazonDifference; + this.amazonDifferencePercent = amazonDifferencePercent; + } +} diff --git a/Frontend/src/app/services/api.service.ts b/Frontend/src/app/services/api.service.ts index dbaddb0..0ac907f 100644 --- a/Frontend/src/app/services/api.service.ts +++ b/Frontend/src/app/services/api.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import process from 'process'; import {Product} from '../models/product'; -import {Price} from '../models/price'; +import {Deal, Price} from '../models/price'; import {Observable, of} from 'rxjs'; import {Vendor} from '../models/vendor'; import {PriceAlarm} from '../models/pricealarm'; @@ -71,6 +71,60 @@ export class ApiService { } } + /** + * Gets a list of all specified products + * @param ids The ids of the products to get + * @return Observable An observable list of products + */ + getProductsByIds(ids: number[]): Observable { + try { + return this.http.get((this.apiUrl + '/products/list/[' + ids.toString() + ']')); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Gets a list of all products that are available at the specified vendor + * @param vendor The vendor to get the products for + * @return Observable An observable list of products + */ + getProductsByVendor(vendor: number): Observable { + try { + return this.http.get((this.apiUrl + '/products/vendor/' + vendor)); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Creates a new product entry + * @param asinOrLink The amazon link or asin of the product + * @return Observable The observable response of the api + */ + addNewProduct(asinOrLink: string): Observable { + let asin = ''; + + // Check if the parameter is a link or an asin + const linkRegex: RegExp = /^http[s]{0,1}:\/\/.*\/dp\/(.[^\/]*)\/{0,1}.*$/; + const matches = linkRegex.exec(asinOrLink); + if (matches) { + // param is a link, extract asin + asin = matches[1] ?? ''; + } else { + // param is not a link, suspect it is an asin + asin = asinOrLink; + } + + try { + return this.http.post((this.apiUrl + '/products'), JSON.stringify({ + asin + })); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + /* ____ _ / __ \_____(_)_______ _____ @@ -79,6 +133,19 @@ export class ApiService { /_/ /_/ /_/\___/\___/____/ */ + /** + * Gets the specified price from the API + * @param id The id of the price to get + * @return Observable An observable containing a single price + */ + getPrice(id: number): Observable { + try { + return this.http.get((this.apiUrl + '/prices/' + id)); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + /** * Gets a list of all prices * @return Observable An observable list of prices @@ -140,6 +207,51 @@ export class ApiService { } } + /** + * Gets the currently best deals + * @param amount The amount of deals to get + * @return Observable An observable list of deals + */ + getBestDeals(amount: number): Observable { + try { + return this.http.get((this.apiUrl + '/prices/bestDeals/' + amount)); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Gets a list of all prices for the specified product + * @param product The product to get prices for + * @return Observable An observable list of prices + */ + getPricesByProduct(products: number[]): Observable { + try { + console.log('IDs: ' + products.toString()); + return this.http.get((this.apiUrl + '/prices/byProduct/list/[' + products.toString() + ']')); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } + + /** + * Creates a new price entry + * @param vendorId The vendor to add the price for + * @param productId The product to add the price to + * @param price The price in cents to add + * @return Observable The observable response of the api + */ + addNewPrice(vendorId: number, productId: number, price: number): Observable { + try { + return this.http.post((this.apiUrl + '/prices'), JSON.stringify({ + vendor_id: vendorId, + product_id: productId, + price_in_cents: price + })); + } catch (exception) { + process.stderr.write(`ERROR received from ${this.apiUrl}: ${exception}\n`); + } + } /* _ __ __ | | / /__ ____ ____/ /___ __________ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2698d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Patrick Müller, Georg Reichert, Henning Sextro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doku/betterzon_sql_script.txt b/doku/betterzon_sql_script.txt new file mode 100644 index 0000000..5c0562c --- /dev/null +++ b/doku/betterzon_sql_script.txt @@ -0,0 +1,185 @@ +CREATE DATABASE `Betterzon`; + +USE `Betterzon`; + +create table categories +( + category_id int auto_increment + primary key, + name text null +); + +create table crawling_processes +( + process_id int auto_increment + primary key, + started_timestamp datetime default current_timestamp() null, + combinations_to_crawl int null +); + +create table manufacturers +( + manufacturer_id int auto_increment + primary key, + name text null +); + +create table products +( + product_id int auto_increment + primary key, + asin text null, + is_active tinyint null, + name text null, + short_description text null, + long_description text null, + image_guid text null, + date_added date null, + last_modified datetime null, + manufacturer_id int null, + selling_rank text null, + category_id int null, + constraint FK_products_categories + foreign key (category_id) references categories (category_id), + constraint FK_products_manufacturers + foreign key (manufacturer_id) references manufacturers (manufacturer_id) +); + +create table users +( + user_id int auto_increment + primary key, + username text not null, + email text null, + bcrypt_password_hash text null, + registration_date datetime default current_timestamp() null, + last_login_date datetime default current_timestamp() null, + is_admin tinyint(1) default 0 null, + constraint users_username_uindex + unique (username) using hash +); + +create table price_alarms +( + alarm_id int auto_increment + primary key, + user_id int not null, + product_id int not null, + defined_price int null, + constraint price_alarms_products_product_id_fk + foreign key (product_id) references products (product_id) + on update cascade on delete cascade, + constraint price_alarms_users_user_id_fk + foreign key (user_id) references users (user_id) + on update cascade on delete cascade +); + +create table sessions +( + session_id int auto_increment + primary key, + user_id int not null, + session_key_hash text null, + createdDate datetime default current_timestamp() null, + lastLogin datetime null, + validUntil datetime null, + validDays int null, + last_IP text null, + constraint sessions_users_user_id_fk + foreign key (user_id) references users (user_id) + on update cascade on delete cascade +); + +create table vendors +( + vendor_id int auto_increment + primary key, + admin_id int null, + name text null, + streetname text null, + zip_code int null, + city text null, + country_code text null, + phone text null, + website text null, + isActive tinyint(1) default 1 not null, + constraint vendors_users_user_id_fk + foreign key (admin_id) references users (user_id) + on update set null on delete set null +); + +create table contact_persons +( + contact_person_id int auto_increment + primary key, + first_name text default '0' not null, + last_name text default '0' not null, + gender text default '0' not null, + email text default '0' not null, + phone text default '0' not null, + vendor_id int default 0 not null, + constraint FK_contact_persons_vendors + foreign key (vendor_id) references vendors (vendor_id) +); + +create table crawling_status +( + status_id int auto_increment + primary key, + process_id int not null, + instance_url text null, + product_id int not null, + vendor_id int not null, + success tinyint(1) not null, + constraint crawling_status_crawling_processes_process_id_fk + foreign key (process_id) references crawling_processes (process_id) + on update cascade on delete cascade, + constraint crawling_status_products_product_id_fk + foreign key (product_id) references products (product_id) + on update cascade on delete cascade, + constraint crawling_status_vendors_vendor_id_fk + foreign key (vendor_id) references vendors (vendor_id) + on update cascade on delete cascade +); + +create table favorite_shops +( + favorite_id int auto_increment + primary key, + vendor_id int not null, + user_id int not null, + constraint favorite_shops_users_user_id_fk + foreign key (user_id) references users (user_id) + on update cascade on delete cascade, + constraint favorite_shops_vendors_vendor_id_fk + foreign key (vendor_id) references vendors (vendor_id) + on update cascade on delete cascade +); + +create table prices +( + price_id int auto_increment + primary key, + product_id int default 0 null, + vendor_id int null, + price_in_cents int null, + timestamp datetime default current_timestamp() null, + active_listing tinyint(1) default 1 not null, + constraint FK_prices_products + foreign key (product_id) references products (product_id), + constraint FK_prices_vendors + foreign key (vendor_id) references vendors (vendor_id) +); + +create table product_links +( + product_link_id int auto_increment + primary key, + product_id int default 0 not null, + vendor_id int default 0 not null, + url text default '0' not null, + constraint FK__products + foreign key (product_id) references products (product_id), + constraint FK__vendors + foreign key (vendor_id) references vendors (vendor_id) +);