Compare commits
No commits in common. "56bc0f2d69775054dd0f8c090308f820e26c5ebb" and "e6ed37ad5e4e1af760b194e80877e78128ca13f1" have entirely different histories.
56bc0f2d69
...
e6ed37ad5e
215
README.md
215
README.md
@ -1,215 +0,0 @@
|
|||||||
# SHOP API
|
|
||||||
Simple API (still WIP)
|
|
||||||
|
|
||||||
# Routes
|
|
||||||
|
|
||||||
## Hello World
|
|
||||||
### `GET /`
|
|
||||||
- **Description:** A simple route that returns a JSON message saying 'Hello, Flask!'.
|
|
||||||
- **Response:**
|
|
||||||
- JSON with the following structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Hello, Flask!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Status Code:**
|
|
||||||
- 200: Success.
|
|
||||||
|
|
||||||
|
|
||||||
## Users
|
|
||||||
### `POST /register`
|
|
||||||
- **Description:** Register a new user.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following fields:
|
|
||||||
- `username` (string): User's username.
|
|
||||||
- `displayname` (string): User's display name.
|
|
||||||
- `email` (string): User's email address.
|
|
||||||
- `password` (string): User's password.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the registration.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if any required field is missing).
|
|
||||||
|
|
||||||
### `POST /login`
|
|
||||||
- **Description:** Log in a user.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following fields:
|
|
||||||
- `username` (string): User's username.
|
|
||||||
- `password` (string): User's password.
|
|
||||||
- **Response:**
|
|
||||||
- JSON containing authentication token and user information.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if any required field is missing).
|
|
||||||
|
|
||||||
### `DELETE /logout`
|
|
||||||
- **Description:** Log out a user by invalidating the JWT token.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the logout.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /update/username`
|
|
||||||
- **Description:** Update the username of the authenticated user.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following field:
|
|
||||||
- `new_username` (string): New username.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the username update.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if new_username is missing).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /update/displayname`
|
|
||||||
- **Description:** Update the display name of the authenticated user.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following field:
|
|
||||||
- `new_displayname` (string): New display name.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the display name update.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if new_displayname is missing).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /update/email`
|
|
||||||
- **Description:** Update the email address of the authenticated user.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following field:
|
|
||||||
- `new_email` (string): New email address.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the email update.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if new_email is missing).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /update/password`
|
|
||||||
- **Description:** Update the password of the authenticated user.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following field:
|
|
||||||
- `new_password` (string): New password.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the password update.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if new_password is missing).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `DELETE /delete`
|
|
||||||
- **Description:** Delete the account of the authenticated user.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the account deletion.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
## Cart
|
|
||||||
### `GET /`
|
|
||||||
- **Description:** Retrieve the contents of the user's shopping cart.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Response:**
|
|
||||||
- JSON containing a list of dictionaries representing the cart contents.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /add/<int:product_id>`
|
|
||||||
- **Description:** Add a specified quantity of a product to the user's shopping cart.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Parameters:**
|
|
||||||
- `count` (optional, int): Quantity of the product to add. Defaults to 1.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of adding the product to the cart.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if count is less than 1).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `DELETE /remove/<int:product_id>`
|
|
||||||
- **Description:** Remove a specific product from the user's shopping cart.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of removing the product from the cart.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `PUT /update/<int:product_id>`
|
|
||||||
- **Description:** Update the quantity of a product in the user's shopping cart.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Parameters:**
|
|
||||||
- `count` (int): New quantity of the product.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of updating the product quantity in the cart.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if count is missing or not a valid integer).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
### `GET /purchase`
|
|
||||||
- **Description:** Complete a purchase, transferring items from the user's cart to the purchase history.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the purchase.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
||||||
|
|
||||||
## Products
|
|
||||||
### `GET /get`
|
|
||||||
- **Description:** Retrieve a paginated list of products.
|
|
||||||
- **Parameters:**
|
|
||||||
- `page` (optional, int): Page number for pagination. Defaults to 0.
|
|
||||||
- **Response:**
|
|
||||||
- JSON containing a list of products.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if the page is less than 0).
|
|
||||||
|
|
||||||
### `GET /<int:id>`
|
|
||||||
- **Description:** Retrieve information about a specific product.
|
|
||||||
- **Parameters:**
|
|
||||||
- `id` (int): Product identifier.
|
|
||||||
- `fields` (optional, string): Comma-separated list of fields to retrieve (e.g., 'name,price,image').
|
|
||||||
- **Response:**
|
|
||||||
- JSON containing information about the specified product.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if invalid fields are provided).
|
|
||||||
|
|
||||||
### `POST /create`
|
|
||||||
- **Description:** Create a new product listing.
|
|
||||||
- **Authentication:**
|
|
||||||
- Requires a valid JWT token.
|
|
||||||
- **Request Body:**
|
|
||||||
- JSON with the following fields:
|
|
||||||
- `name` (string): Product name.
|
|
||||||
- `price` (float): Product price.
|
|
||||||
- **Response:**
|
|
||||||
- JSON indicating the success of the product listing creation.
|
|
||||||
- **Status Codes:**
|
|
||||||
- 200: Success.
|
|
||||||
- 400: Bad Request (if name or price is missing or if price is not a valid float).
|
|
||||||
- 401: Unauthorized (if JWT token is missing or invalid).
|
|
@ -1,10 +1,8 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_jwt_extended import JWTManager
|
from flask_jwt_extended import JWTManager
|
||||||
from flask_mail import Mail
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
jwt_manager = JWTManager(app)
|
jwt_manager = JWTManager(app)
|
||||||
mail = Mail(app)
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
from app.api import bp, bp_errors, bp_product, bp_user, bp_cart
|
from app.api import bp, bp_errors, bp_product, bp_user, bp_cart
|
||||||
|
@ -2,7 +2,7 @@ from flask import Blueprint
|
|||||||
|
|
||||||
bp_errors = Blueprint('errors', __name__)
|
bp_errors = Blueprint('errors', __name__)
|
||||||
bp = Blueprint('api', __name__)
|
bp = Blueprint('api', __name__)
|
||||||
bp_product = Blueprint('products', __name__, url_prefix="/products")
|
bp_product = Blueprint('product', __name__, url_prefix="/product")
|
||||||
bp_user = Blueprint('user', __name__, url_prefix="/user")
|
bp_user = Blueprint('user', __name__, url_prefix="/user")
|
||||||
bp_cart = Blueprint('cart', __name__, url_prefix="/cart")
|
bp_cart = Blueprint('cart', __name__, url_prefix="/cart")
|
||||||
|
|
||||||
|
@ -1,52 +1,20 @@
|
|||||||
from flask import jsonify, abort, request
|
from flask import jsonify, abort, request
|
||||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
||||||
|
|
||||||
from app.api import bp_product
|
from app.api import bp_product
|
||||||
|
|
||||||
from app.services.product_service import ProductService
|
from app.services.product_service import ProductService
|
||||||
|
|
||||||
@bp_product.route('/get', methods=['GET'])
|
|
||||||
def get_products():
|
|
||||||
page = request.args.get('page', default=0, type=int)
|
|
||||||
|
|
||||||
if page < 0:
|
|
||||||
return abort(400)
|
|
||||||
|
|
||||||
result, status_code = ProductService.get_products(page)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
@bp_product.route('/<int:id>', methods=['GET'])
|
@bp_product.route('/<int:id>', methods=['GET'])
|
||||||
def get_product_info(id: int):
|
def get_product_info(id: int):
|
||||||
fields = ['name', 'price', 'image', 'image_name', 'seller']
|
fields = ['name', 'price', 'image', 'image_name', 'seller']
|
||||||
|
|
||||||
fields_param = request.args.get('fields')
|
fields_param = request.args.get('fields')
|
||||||
|
requested_fields = []
|
||||||
fields_param_list = fields_param.split(',') if fields_param else fields
|
|
||||||
|
|
||||||
common_fields = list(set(fields) & set(fields_param_list))
|
|
||||||
|
|
||||||
result, status_code = ProductService.get_product_info(common_fields, id)
|
return abort(501)
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp_product.route('/create', methods=['POST'])
|
@bp_product.route('/create', methods=['POST'])
|
||||||
@jwt_required()
|
|
||||||
def create_product_listing():
|
def create_product_listing():
|
||||||
user_id = get_jwt_identity()
|
|
||||||
name = request.json.get('name')
|
name = request.json.get('name')
|
||||||
price = request.json.get('price')
|
|
||||||
|
|
||||||
if name is None or price is None:
|
|
||||||
return abort(400)
|
|
||||||
|
|
||||||
float_price = float(price)
|
|
||||||
|
|
||||||
if not isinstance(float_price, float):
|
|
||||||
return abort(400)
|
|
||||||
|
|
||||||
result, status_code = ProductService.create_listing(user_id, name, float_price)
|
|
||||||
|
|
||||||
return result, status_code
|
|
@ -19,10 +19,4 @@ class FlaskProduction:
|
|||||||
class FlaskTesting:
|
class FlaskTesting:
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
|
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
|
||||||
SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT')
|
SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT')
|
||||||
|
|
||||||
MAIL_SERVER = os.environ.get('MAIL_SERVER')
|
|
||||||
MAIL_PORT = os.environ.get('MAIL_USERNAME')
|
|
||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
|
||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
|
||||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS')
|
|
@ -12,6 +12,8 @@ db_connection = mysql.connector.connect(
|
|||||||
database=MySqlConfig.MYSQL_DATABASE,
|
database=MySqlConfig.MYSQL_DATABASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
db_cursor = db_connection.cursor(dictionary=True)
|
||||||
|
|
||||||
jwt_redis_blocklist = redis.StrictRedis(
|
jwt_redis_blocklist = redis.StrictRedis(
|
||||||
host=RedisConfig.REDIS_HOST,
|
host=RedisConfig.REDIS_HOST,
|
||||||
port=RedisConfig.REDIS_PORT,
|
port=RedisConfig.REDIS_PORT,
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
from flask_mail import Message
|
|
||||||
|
|
||||||
from app import mail
|
|
||||||
|
|
||||||
|
|
||||||
def send_mail(subject: str, recipient: str, body: str):
|
|
||||||
msg = Message(subject, recipients=[recipient])
|
|
||||||
msg.body = body
|
|
||||||
|
|
||||||
try:
|
|
||||||
mail.send(msg)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to send email. Error: {e}")
|
|
||||||
return False
|
|
@ -7,17 +7,6 @@ class CartService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_to_cart(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
|
def add_to_cart(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
Adds a product to a user's cart.
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:param product_id: ID of product to be added.
|
|
||||||
:type product_id: int
|
|
||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
with db_connection.cursor(dictionary=True) as cursor:
|
||||||
cursor.execute("select count from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
cursor.execute("select count from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
||||||
@ -37,19 +26,6 @@ class CartService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_count(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
|
def update_count(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
Updates count of products in user's cart
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:param product_id: ID of product to be updated.
|
|
||||||
:type product_id: int
|
|
||||||
:param count: New count of products
|
|
||||||
:type count: int
|
|
||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if count <= 0:
|
if count <= 0:
|
||||||
return CartService.delete_from_cart(user_id, product_id)
|
return CartService.delete_from_cart(user_id, product_id)
|
||||||
@ -65,18 +41,6 @@ class CartService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_from_cart(user_id: str, product_id: int) -> Tuple[Union[dict, str], int]:
|
def delete_from_cart(user_id: str, product_id: int) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
Completely deletes an item from a user's cart
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:param product_id: ID of product to be updated.
|
|
||||||
:type product_id: int
|
|
||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_connection.cursor() as cursor:
|
with db_connection.cursor() as cursor:
|
||||||
cursor.execute("delete from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
cursor.execute("delete from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
||||||
@ -89,15 +53,6 @@ class CartService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_cart(user_id: str) -> Tuple[Union[dict, str], int]:
|
def show_cart(user_id: str) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
Gives the user the content of their cart
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
with db_connection.cursor(dictionary=True) as cursor:
|
||||||
cursor.execute("select product.name as product_name, count, price_subtotal, date_added from cart_item inner join product on cart_item.product_id = product.id where cart_item.cart_id = %s", (user_id,))
|
cursor.execute("select product.name as product_name, count, price_subtotal, date_added from cart_item inner join product on cart_item.product_id = product.id where cart_item.cart_id = %s", (user_id,))
|
||||||
@ -123,15 +78,6 @@ class CartService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def purchase(user_id: str) -> Tuple[Union[dict, str], int]:
|
def purchase(user_id: str) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
"Purchases" the contents of user's cart
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
with db_connection.cursor(dictionary=True) as cursor:
|
||||||
# get all cart items
|
# get all cart items
|
||||||
|
@ -3,125 +3,82 @@ import base64
|
|||||||
from flask import abort
|
from flask import abort
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from app.extensions import db_connection
|
from app.extensions import db_connection
|
||||||
|
|
||||||
class ProductService:
|
class ProductService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_products(page: int):
|
def get_name(product_id: int):
|
||||||
"""
|
db_cursor.execute(f"select name from product where product.id = {product_id}")
|
||||||
Fetches 10 products
|
|
||||||
|
|
||||||
:param page: Page, aka offset of fetching
|
if db_cursor.rowcount != 1:
|
||||||
:type page: int
|
return abort(404)
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
|
|
||||||
offset = 10 * page
|
|
||||||
cursor.execute("select product.id, user.displayname as seller, product.name, product.price_pc from product inner join user on user.id = product.seller_id order by product.id limit 10 offset %s", (offset,))
|
|
||||||
results = cursor.fetchall()
|
|
||||||
|
|
||||||
if len(results) < 1:
|
result = db_cursor.fetchone()
|
||||||
return {"Failed": "Failed to fetch products. You've probably selected too far with pages"}, 400
|
return result['name']
|
||||||
|
|
||||||
result_obj = []
|
|
||||||
for row in results:
|
|
||||||
mid_result = {
|
|
||||||
"id": row['id'],
|
|
||||||
"seller": row['seller'],
|
|
||||||
"name": row['name'],
|
|
||||||
"price": row['price_pc']
|
|
||||||
}
|
|
||||||
|
|
||||||
result_obj.append(mid_result)
|
|
||||||
|
|
||||||
return result_obj, 200
|
|
||||||
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to fetch products. Error: {e}"}, 500
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_product_info(fields: list[str], product_id: int):
|
def get_manufacturer(product_id: int):
|
||||||
"""
|
db_cursor.execute(f"select user.displayname as seller from product inner join user on product.seller_id = user.id where product.id = {product_id}")
|
||||||
Fetches specific product info
|
|
||||||
|
|
||||||
:param fields: array of fields that can be fetched
|
if db_cursor.rowcount != 1:
|
||||||
:type fields: list[str]
|
return abort(404)
|
||||||
:param product_id: ID of product to be updated.
|
|
||||||
:type product_id: int
|
result = db_cursor.fetchone()
|
||||||
"""
|
return result['seller']
|
||||||
|
|
||||||
try:
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
fields_to_sql = {
|
|
||||||
"name": "product.name",
|
|
||||||
"price": "product.price_pc as price",
|
|
||||||
"image": "product.image",
|
|
||||||
"image_name": "product.image_name",
|
|
||||||
"seller": "user.displayname as seller"
|
|
||||||
}
|
|
||||||
field_sql = []
|
|
||||||
|
|
||||||
for field in fields:
|
|
||||||
field_sql.append(fields_to_sql[field])
|
|
||||||
|
|
||||||
select_params = ", ".join(field_sql)
|
|
||||||
|
|
||||||
if "seller" in fields:
|
|
||||||
cursor.execute(f"select {select_params} from product inner join user on user.id = product.seller_id where product.id = %s", (product_id,))
|
|
||||||
else:
|
|
||||||
cursor.execute(f"select {select_params} from product where id = %s", (product_id,))
|
|
||||||
|
|
||||||
result = cursor.fetchone()
|
|
||||||
|
|
||||||
if cursor.rowcount != 1:
|
|
||||||
return {"Failed": "Failed to fetch product. Product likely doesn't exist"}, 400
|
|
||||||
|
|
||||||
result_obj = {}
|
|
||||||
|
|
||||||
for field in fields:
|
|
||||||
if field == "image": # Encode image into base64
|
|
||||||
result_obj[field] = base64.b64encode(result[field]).decode('utf-8')
|
|
||||||
else:
|
|
||||||
result_obj[field] = result[field]
|
|
||||||
|
|
||||||
return result_obj, 200
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to fetch product info. Error: {e}"}, 500
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_listing(seller_id: str, name: str, price: float):
|
def get_price(product_id: int):
|
||||||
"""
|
db_cursor.execute(f"select price_pc from product where product.id = {product_id}")
|
||||||
Creates a new product listing
|
|
||||||
|
|
||||||
:param seller_id: User ID
|
if db_cursor.rowcount != 1:
|
||||||
:type seller_id: str
|
return abort(404)
|
||||||
:param name: New product's name
|
|
||||||
:type name: str
|
result = db_cursor.fetchone()
|
||||||
:param price: New product's price
|
return result['price_pc']
|
||||||
:type price: float
|
|
||||||
"""
|
@staticmethod
|
||||||
|
def get_image(product_id: int):
|
||||||
|
db_cursor.execute(f"select image from product where product.id = {product_id}")
|
||||||
|
|
||||||
try:
|
if db_cursor.rowcount != 1:
|
||||||
with db_connection.cursor() as cursor:
|
return abort(404)
|
||||||
rounded_price = round(price, 2)
|
|
||||||
|
result = db_cursor.fetchone()
|
||||||
|
return base64.b64encode(result['image']).decode('utf-8')
|
||||||
|
|
||||||
cursor.execute("insert into product(seller_id, name, price_pc) values (%s, %s, %s)", (seller_id, name, Decimal(str(rounded_price))))
|
@staticmethod
|
||||||
db_connection.commit()
|
def get_image_name(product_id: int):
|
||||||
|
db_cursor.execute(f"select image_name from product where product.id = {product_id}")
|
||||||
|
|
||||||
|
if db_cursor.rowcount != 1:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
result = db_cursor.fetchone()
|
||||||
|
return result['image_name']
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_info(product_id: int):
|
||||||
|
db_cursor.execute(f"select name, user.displayname as seller, price_pc, image_name, image from product inner join user on product.seller_id = user.id where product.id = {product_id}")
|
||||||
|
|
||||||
|
if db_cursor.rowcount != 1:
|
||||||
|
return abort(404)
|
||||||
|
|
||||||
|
result = db_cursor.fetchone()
|
||||||
|
return {
|
||||||
|
"name": result['name'],
|
||||||
|
"seller": result['seller'],
|
||||||
|
"price": result['price_pc'],
|
||||||
|
"image_name": result['image_name'],
|
||||||
|
"image": base64.b64encode(result['image']).decode('utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_listing(name: str, seller_id: str, price: float, image_name: str, image):
|
||||||
|
try:
|
||||||
|
db_cursor.execute("insert into product(seller_id, name, price_pc, image, image_name) values (%s, %s, %s, %s, %s)", (seller_id, name, price, image, image_name))
|
||||||
|
db_connection.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
return {"Failed": f"Failed to create product. {e}"}, 400
|
return {"Failed": f"Failed to create product. {e}"}, 400
|
||||||
|
|
||||||
return {"Success": "Successfully created new product listing"}, 200
|
return {"Success", "Successfully created new product listing"}, 200
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __is_base64_jpg(decoded_string):
|
|
||||||
try:
|
|
||||||
# Checking if the decoded data represents a valid JPEG image
|
|
||||||
image_type = imghdr.what(None, decoded_data)
|
|
||||||
return image_type == 'jpeg'
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
@ -6,11 +6,9 @@ from typing import Tuple, Union
|
|||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
from flask_jwt_extended import create_access_token
|
from flask_jwt_extended import create_access_token
|
||||||
|
|
||||||
from app.extensions import db_connection
|
from app.extensions import db_cursor, db_connection
|
||||||
from app.extensions import jwt_redis_blocklist
|
from app.extensions import jwt_redis_blocklist
|
||||||
|
|
||||||
from app.mail_utils import send_mail
|
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
"""
|
"""
|
||||||
@ -40,33 +38,27 @@ class UserService:
|
|||||||
:rtype: Tuple[Union[dict, str], int]
|
:rtype: Tuple[Union[dict, str], int]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not UserService.__verify_username(username):
|
||||||
|
return {"Failed": "Failed to verify username. Try another username"}, 400
|
||||||
|
|
||||||
|
if not UserService.__verify_displayname(displayname):
|
||||||
|
return {"Failed": "Failed to verify display name. Try another name"}, 400
|
||||||
|
|
||||||
|
if not UserService.__verify_email(email):
|
||||||
|
return {"Failed": "Failed to verify email. Try another email"}, 400
|
||||||
|
|
||||||
|
if not UserService.__verify_password(password):
|
||||||
|
return {"Failed": "Failed to verify password. Try another (stronger) password"}, 400
|
||||||
|
|
||||||
|
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not UserService.__verify_username(username):
|
db_cursor.execute("insert into user (username, displayname, email, password) values (%s, %s, %s, %s)", (username, displayname, email, hashed_password))
|
||||||
return {"Failed": "Failed to verify username. Try another username"}, 400
|
db_connection.commit()
|
||||||
|
|
||||||
if not UserService.__verify_displayname(displayname):
|
|
||||||
return {"Failed": "Failed to verify display name. Try another name"}, 400
|
|
||||||
|
|
||||||
if not UserService.__verify_email(email):
|
|
||||||
return {"Failed": "Failed to verify email. Try another email"}, 400
|
|
||||||
|
|
||||||
if not UserService.__verify_password(password):
|
|
||||||
return {"Failed": "Failed to verify password. Try another (stronger) password"}, 400
|
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
|
||||||
|
|
||||||
with db_connection.cursor() as cursor:
|
|
||||||
cursor.execute("insert into user (username, displayname, email, password) values (%s, %s, %s, %s)", (username, displayname, email, hashed_password))
|
|
||||||
db_connection.commit()
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500
|
return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500
|
||||||
|
|
||||||
# TODO Implement mail sending
|
|
||||||
# Currently throws error - connection refused
|
|
||||||
|
|
||||||
# send_mail("Successfully registered!", email, "Congratulations! Your account has been successfully created.\nThis mail also serves as a test that the email address is correct")
|
|
||||||
|
|
||||||
return {"Success": "User created successfully"}, 200
|
return {"Success": "User created successfully"}, 200
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -81,29 +73,24 @@ class UserService:
|
|||||||
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
:rtype: Tuple[Union[dict, str], int]
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
|
|
||||||
cursor.execute("select id, password from user where username = %s", (username,))
|
db_cursor.execute("select id, password from user where username = %s", (username,))
|
||||||
result = db_cursor.fetchone()
|
result = db_cursor.fetchone()
|
||||||
|
|
||||||
user_id = result['id']
|
user_id = result['id']
|
||||||
password_hash = result['password']
|
password_hash = result['password']
|
||||||
|
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
return {"Failed": "Username not found"}, 400
|
return {"Failed": "Username not found"}, 400
|
||||||
|
|
||||||
if not bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')):
|
if not bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')):
|
||||||
return {"Failed": "Incorrect password"}, 401
|
return {"Failed": "Incorrect password"}, 401
|
||||||
|
|
||||||
expire = datetime.timedelta(hours=1)
|
expire = datetime.timedelta(hours=1)
|
||||||
|
|
||||||
token = create_access_token(identity=user_id, expires_delta=expire)
|
token = create_access_token(identity=user_id, expires_delta=expire)
|
||||||
|
|
||||||
return {"token": token}, 200
|
return {"token": token}, 200
|
||||||
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to login. Error: {e}"}, 500
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def logout(jti, exp) -> Tuple[Union[dict, str], int]:
|
def logout(jti, exp) -> Tuple[Union[dict, str], int]:
|
||||||
@ -123,19 +110,9 @@ class UserService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_user(user_id: str) -> Tuple[Union[dict, str], int]:
|
def delete_user(user_id: str) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
|
||||||
Deletes a user account.
|
|
||||||
|
|
||||||
:param user_id: User ID.
|
|
||||||
:type user_id: str
|
|
||||||
:return: Tuple containing a dictionary and an HTTP status code.
|
|
||||||
:rtype: Tuple[Union[dict, str], int]
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with db_connection.cursor() as cursor:
|
db_cursor.execute("delete from user where id = %s", (user_id,))
|
||||||
cursor.execute("delete from user where id = %s", (user_id,))
|
db_connection.commit()
|
||||||
db_connection.commit()
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
return {"Failed": f"Failed to delete user. {e}"}, 500
|
return {"Failed": f"Failed to delete user. {e}"}, 500
|
||||||
|
|
||||||
@ -154,13 +131,12 @@ class UserService:
|
|||||||
:rtype: Tuple[Union[dict, str], int]
|
:rtype: Tuple[Union[dict, str], int]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
if not UserService.__verify_email(new_email):
|
||||||
if not UserService.__verify_email(new_email):
|
return {"Failed": "Failed to verify email. Try another email"}, 400
|
||||||
return {"Failed": "Failed to verify email. Try another email"}, 400
|
|
||||||
|
|
||||||
with db_connection.cursor() as cursor:
|
try:
|
||||||
cursor.execute("update user set email = %s where id = %s", (new_email, user_id))
|
db_cursor.execute("update user set email = %s where id = %s", (new_email, user_id))
|
||||||
db_connection.commit()
|
db_connection.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
return {"Failed": f"Failed to update email. Email is likely in use already. Error: {e}"}, 500
|
return {"Failed": f"Failed to update email. Email is likely in use already. Error: {e}"}, 500
|
||||||
|
|
||||||
@ -179,13 +155,12 @@ class UserService:
|
|||||||
:rtype: Tuple[Union[dict, str], int]
|
:rtype: Tuple[Union[dict, str], int]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
if not UserService.__verify_name(new_username):
|
||||||
if not UserService.__verify_name(new_username):
|
return {"Failed": "Failed to verify username. Try another one"}, 400
|
||||||
return {"Failed": "Failed to verify username. Try another one"}, 400
|
|
||||||
|
|
||||||
with db_connection.cursor() as cursor:
|
try:
|
||||||
cursor.execute("update user set username = %s where id = %s", (new_username, user_id))
|
db_cursor.execute("update user set username = %s where id = %s", (new_username, user_id))
|
||||||
db_connection.commit()
|
db_connection.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
return {"Failed": f"Failed to update username. Username is likely in use already. Error: {e}"}, 500
|
return {"Failed": f"Failed to update username. Username is likely in use already. Error: {e}"}, 500
|
||||||
|
|
||||||
@ -204,15 +179,14 @@ class UserService:
|
|||||||
:rtype: Tuple[Union[dict, str], int]
|
:rtype: Tuple[Union[dict, str], int]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not UserService.__verify_password(new_password):
|
||||||
|
return {"Failed": "Failed to verify password. Try another (stronger) one"}, 400
|
||||||
|
|
||||||
|
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not UserService.__verify_password(new_password):
|
db_cursor.execute("update user set password = %s where id = %s", (new_username, user_id))
|
||||||
return {"Failed": "Failed to verify password. Try another (stronger) one"}, 400
|
db_connection.commit()
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
|
||||||
|
|
||||||
with db_connection.cursor() as cursor:
|
|
||||||
cursor.execute("update user set password = %s where id = %s", (new_username, user_id))
|
|
||||||
db_connection.commit()
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
return {"Failed": f"Failed to update password. Error: {e}"}, 500
|
return {"Failed": f"Failed to update password. Error: {e}"}, 500
|
||||||
|
|
||||||
|
@ -3,5 +3,4 @@ gunicorn==20.1.0
|
|||||||
mysql-connector-python==8.3.0
|
mysql-connector-python==8.3.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
Flask-JWT-Extended==4.5.3
|
Flask-JWT-Extended==4.5.3
|
||||||
PyJWT==2.8.0
|
PyJWT==2.8.0
|
||||||
Flask-Mail==0.9.1
|
|
2
shop.sql
2
shop.sql
@ -35,7 +35,7 @@ create table product(
|
|||||||
seller_id int not null,
|
seller_id int not null,
|
||||||
name varchar(32) not null,
|
name varchar(32) not null,
|
||||||
price_pc decimal(12,2) not null,
|
price_pc decimal(12,2) not null,
|
||||||
image blob,
|
image blob not null,
|
||||||
image_name varchar(64),
|
image_name varchar(64),
|
||||||
foreign key (seller_id) references user(id)
|
foreign key (seller_id) references user(id)
|
||||||
) auto_increment = 13001;
|
) auto_increment = 13001;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user