From f0c378b20fbb36b1d6a453a681b495c73a0c6041 Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Sun, 23 Feb 2025 15:56:39 +0100 Subject: [PATCH] [rewrite] Updated models a bit, preparing to make first functional routes --- backend/app/__init__.py | 29 -------- backend/app/database/__init__.py | 0 backend/app/database/exceptions.py | 20 ++++++ backend/app/database/manager.py | 68 +++++++++++++++++++ backend/app/dependencies.py | 36 ++++++++++ backend/app/extensions.py | 21 ------ backend/app/models/__init__.py | 5 -- .../app/models/{ => disabled}/cart_model.py | 0 .../models/{ => disabled}/purchase_model.py | 0 .../models/disabled/user_preferences_model.py | 0 .../models/{ => disabled}/wishlist_model.py | 0 backend/app/models/shop_model.py | 8 ++- backend/app/models/user_model.py | 37 +++++----- backend/app/models/user_preferences.py | 11 +++ backend/app/models/user_role_model.py | 16 +++++ backend/app/models/user_statistics_model.py | 5 +- .../app/utils/database_exception_catcher.py | 13 ++++ backend/app/utils/route_exception_catcher.py | 0 backend/poetry.lock | 12 +++- backend/pyproject.toml | 1 + main.py | 11 --- 21 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 backend/app/database/__init__.py create mode 100644 backend/app/database/exceptions.py create mode 100644 backend/app/database/manager.py create mode 100644 backend/app/dependencies.py delete mode 100644 backend/app/extensions.py rename backend/app/models/{ => disabled}/cart_model.py (100%) rename backend/app/models/{ => disabled}/purchase_model.py (100%) create mode 100644 backend/app/models/disabled/user_preferences_model.py rename backend/app/models/{ => disabled}/wishlist_model.py (100%) create mode 100644 backend/app/models/user_preferences.py create mode 100644 backend/app/models/user_role_model.py create mode 100644 backend/app/utils/database_exception_catcher.py create mode 100644 backend/app/utils/route_exception_catcher.py delete mode 100644 main.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 7f84074..e69de29 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,29 +0,0 @@ -# from flask import Flask -# from flask_jwt_extended import JWTManager -# from flask_mail import Mail -# from flasgger import Swagger - -# from app.doc.main_swag import main_swagger - -# app = Flask(__name__) -# from app.config import FlaskTesting, FlaskProduction - -# app.config.from_object(FlaskTesting) - -# flask_mail = Mail(app) -# jwt_manager = JWTManager(app) -# swag = Swagger(app, template=main_swagger) - - -# def create_app(): -# from app.api import bp, bp_errors, bp_product, bp_user, bp_cart - -# app.register_blueprint(bp) -# app.register_blueprint(bp_errors) -# app.register_blueprint(bp_product) -# app.register_blueprint(bp_user) -# app.register_blueprint(bp_cart) - -# from . import jwt_utils - -# return app diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database/exceptions.py b/backend/app/database/exceptions.py new file mode 100644 index 0000000..1ffe94c --- /dev/null +++ b/backend/app/database/exceptions.py @@ -0,0 +1,20 @@ +class DatabaseError(Exception): + # Inspired by OSError which also uses errno's + # It's a better approach than using a class for each error + UNKNOWN_ERROR = -1 + + CONNECTION_ERROR = 1 + EMPTY_CONFIG = 2 + DUPLICATE_ENTRY = 3 + + def __init__(self, message: str, errno: int, **kwargs): + + super().__init__(message) + self.message = message + self.errno = errno + + for key, value in kwargs.items(): + setattr(self, key, value) + + +__all__ = ["DatabaseError"] diff --git a/backend/app/database/manager.py b/backend/app/database/manager.py new file mode 100644 index 0000000..dba4aa4 --- /dev/null +++ b/backend/app/database/manager.py @@ -0,0 +1,68 @@ +import logging +from typing import Generator +from contextlib import contextmanager + +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy import create_engine, text +from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError + +from app.database.exceptions import DatabaseError +from app.models.base_model import Base + + +class DatabaseManager(): + + _instance: 'DatabaseManager' = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self) -> None: + if hasattr(self, "engine"): + return + + self.logger = logging.getLogger(__name__) + self.logger.info("Initializing Database") + + self.engine = create_engine('sqlite:///bank.db') + + self.Session = sessionmaker(bind=self.engine) + self.create_tables() + + def create_tables(self): + self.logger.debug("Creating tables") + Base.metadata.create_all(self.engine) + + def cleanup(self) -> None: + self.engine.dispose() + + def test_connection(self) -> bool: + self.logger.debug("Testing database connection") + try: + with self.engine.connect() as connection: + connection.execute(text("select 1")) + self.logger.debug("Database connection successful") + return True + except SqlAlchemyDatabaseError as e: + self.logger.critical("Database connection failed: %s", e) + raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e + + return False + + @classmethod + @contextmanager + def get_session(cls) -> Generator[Session, None, None]: + session = cls._instance.Session() + try: + yield session + except Exception as e: + session.rollback() + cls._instance.logger.error("Transaction failed: %s", e) + raise + finally: + session.close() + + +__all__ = ["DatabaseManager"] diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..8716ea3 --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,36 @@ +from typing import Annotated + +from pydantic import BaseModel + +from fastapi import Header, HTTPException, Request, Body + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +class LoginRequest(BaseModel): + username: str + password: str + +async def get_jsession_id(request: Request): + jsessionid = ( + request.headers.get("JSESSIONID") + or request.cookies.get("JSESSIONID") + or request.query_params.get("jsessionid") + ) + + if not jsessionid: + raise HTTPException(status_code=400, detail="JSESSIONID is required for this operation") + + return jsessionid + +async def get_credentials(credentials: LoginRequest = Body(...)): + return credentials.dict() + + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") \ No newline at end of file diff --git a/backend/app/extensions.py b/backend/app/extensions.py deleted file mode 100644 index dd4d11b..0000000 --- a/backend/app/extensions.py +++ /dev/null @@ -1,21 +0,0 @@ -import mysql.connector -import redis -import os - -from app.config import RedisConfig -from app.config import MySqlConfig - -db_connection = mysql.connector.connect( - host=MySqlConfig.MYSQL_HOST, - user=MySqlConfig.MYSQL_USER, - password=MySqlConfig.MYSQL_PASSWORD, - database=MySqlConfig.MYSQL_DATABASE, -) - -jwt_redis_blocklist = redis.StrictRedis( - host=RedisConfig.REDIS_HOST, - port=RedisConfig.REDIS_PORT, - password=RedisConfig.REDIS_PASSWORD, - db=0, - decode_responses=True, -) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e3b2b2e..e69de29 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +0,0 @@ -from .user_model import * - -__all__ = [ - *user_model.__all__ -] diff --git a/backend/app/models/cart_model.py b/backend/app/models/disabled/cart_model.py similarity index 100% rename from backend/app/models/cart_model.py rename to backend/app/models/disabled/cart_model.py diff --git a/backend/app/models/purchase_model.py b/backend/app/models/disabled/purchase_model.py similarity index 100% rename from backend/app/models/purchase_model.py rename to backend/app/models/disabled/purchase_model.py diff --git a/backend/app/models/disabled/user_preferences_model.py b/backend/app/models/disabled/user_preferences_model.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/wishlist_model.py b/backend/app/models/disabled/wishlist_model.py similarity index 100% rename from backend/app/models/wishlist_model.py rename to backend/app/models/disabled/wishlist_model.py diff --git a/backend/app/models/shop_model.py b/backend/app/models/shop_model.py index bc3b65e..f784c4c 100644 --- a/backend/app/models/shop_model.py +++ b/backend/app/models/shop_model.py @@ -22,5 +22,9 @@ class Shop(Base): business_hours = Column(JSON, nullable=False) links = Column(JSON, nullable=False) - users = relationship('User', back_populates='shop') - products = relationship('Product', back_populates='shop') + owner = relationship('User', back_populates='owned_shops') + registered_users = relationship('User', back_populates='registered_shop') + # products = relationship('Product', back_populates='shop') + + +__all__ = ["Shop"] diff --git a/backend/app/models/user_model.py b/backend/app/models/user_model.py index 904b800..f54411b 100644 --- a/backend/app/models/user_model.py +++ b/backend/app/models/user_model.py @@ -1,33 +1,32 @@ -from sqlalchemy import ForeignKey, Column, Integer, JSON, TIMESTAMP, String, Enum -from sqlalchemy.sql import func +from sqlalchemy import Column, String, Integer, ForeignKey, TIMESTAMP +from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.orm import relationship - from .base_model import Base class User(Base): - __tablename__ = "user" + __tablename__ = 'user' - id = Column(Integer, primary_key=True) - shop_id = Column(Integer, ForeignKey("shop.id"), nullable=True) - username = Column(String(64), unique=True, nullable=False) - email = Column(String(128), unique=True, nullable=False) + id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True) + user_role_id = Column(INTEGER(unsigned=True), ForeignKey('user_role.id'), nullable=False) + shop_id = Column(Integer, ForeignKey('shop.id'), nullable=True) + username = Column(String(64), nullable=False, unique=True) + email = Column(String(128), nullable=False, unique=True) password = Column(String(60), nullable=False) - role = Column(Enum("customer", "employee", "manager", "owner", "admin", name="user_role"), nullable=False, default="customer") first_name = Column(String(64), nullable=True) last_name = Column(String(64), nullable=True) - phone_number = Column(String(15), nullable=True) - created_at = Column(TIMESTAMP, default=func.now, nullable=True) - updated_at = Column(TIMESTAMP, default=func.now, onupdate=func.now, nullable=True) - last_login = Column(TIMESTAMP, nullable=True) + phone_number = Column(String(15), nullable=False) profile_picture = Column(String(100), nullable=True) - preferences = Column(JSON, nullable=True) + created_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP") + updated_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + last_login = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP") - shop = relationship("Shop", back_populates="users") - carts = relationship("Cart", back_populates="user") - purchases = relationship("Purchase", back_populates="user") - statistics = relationship("UserStatistics", back_populates="user") - wishlists = relationship("Wishlist", back_populates="user") + owned_shops = relationship("Shop", back_populates="owner") + registered_shop = relationship("Shop", back_populates="registered_users") + + role = relationship("UserRole", back_populates="users") + preferences = relationship("UserPreferences", uselist=False, back_populates="user") + statistics = relationship("UserStatistics", uselist=False, back_populates="user_statistics") __all__ = ["User"] diff --git a/backend/app/models/user_preferences.py b/backend/app/models/user_preferences.py new file mode 100644 index 0000000..50df982 --- /dev/null +++ b/backend/app/models/user_preferences.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, ForeignKey +from sqlalchemy.dialects.mysql import INTEGER +from sqlalchemy.orm import relationship +from .base_model import Base + +class UserPreferences(Base): + __tablename__ = 'user_preferences' + + user_id = Column(INTEGER(unsigned=True), ForeignKey('user.id'), primary_key=True) + + user = relationship("User", back_populates="preferences") \ No newline at end of file diff --git a/backend/app/models/user_role_model.py b/backend/app/models/user_role_model.py new file mode 100644 index 0000000..4a1e01c --- /dev/null +++ b/backend/app/models/user_role_model.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, String +from sqlalchemy.dialects.mysql import INTEGER +from sqlalchemy.orm import relationship +from .base_model import Base + + +class UserRole(Base): + __tablename__ = 'user_role' + + id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True) + name = Column(String(45), nullable=False, unique=True) + + users = relationship("User", back_populates="role") + + +__all__ = ["UserRole"] diff --git a/backend/app/models/user_statistics_model.py b/backend/app/models/user_statistics_model.py index 13e7b2f..d1980b2 100644 --- a/backend/app/models/user_statistics_model.py +++ b/backend/app/models/user_statistics_model.py @@ -1,11 +1,12 @@ -from sqlalchemy import Column, Integer, Float, ForeignKey +from sqlalchemy import Column, Float, ForeignKey +from sqlalchemy.dialects.mysql import INTEGER from sqlalchemy.orm import relationship from .base_model import Base class UserStatistics(Base): __tablename__ = "user_statistics" - user_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True) + user_id = Column(INTEGER(unsigned=True), ForeignKey("user.id", ondelete="CASCADE"), primary_key=True) total_spend = Column(Float, nullable=True) user = relationship("User", back_populates="user_statistics", foreign_keys=[user_id]) diff --git a/backend/app/utils/database_exception_catcher.py b/backend/app/utils/database_exception_catcher.py new file mode 100644 index 0000000..3d85954 --- /dev/null +++ b/backend/app/utils/database_exception_catcher.py @@ -0,0 +1,13 @@ +from functools import wraps +from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError +from app.database.exceptions import DatabaseError + + +def handle_database_errors(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except SqlAlchemyDatabaseError as e: + raise DatabaseError(str(e), -1) from e + return wrapper diff --git a/backend/app/utils/route_exception_catcher.py b/backend/app/utils/route_exception_catcher.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/poetry.lock b/backend/poetry.lock index a8c000b..f300d75 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -204,6 +204,16 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "mysql-connector" +version = "2.2.9" +description = "MySQL driver written in Python" +optional = false +python-versions = "*" +files = [ + {file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -614,4 +624,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "250084c97cedad74f83b3a3420c71f338eb85d309bf30aed8e84743a3c2b26e3" +content-hash = "96176c45f5a989a912f901370d6680231d9be0ba6a6e12a753699f17bed8ca93" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 573e297..ccfca9f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.12" fastapi = "^0.115.6" sqlalchemy = "^2.0.37" python-dotenv = "^1.0.1" +mysql-connector = "^2.2.9" [tool.poetry.group.dev.dependencies] diff --git a/main.py b/main.py deleted file mode 100644 index d778a18..0000000 --- a/main.py +++ /dev/null @@ -1,11 +0,0 @@ -from dotenv import load_dotenv - -from app import create_app - -load_dotenv() - -app = create_app() - -if __name__ == "__main__": - print("Hello, Flask") - app.run(use_reloader=False)