From c79f54eda755a10e8168886c9dff5d76debb73a3 Mon Sep 17 00:00:00 2001 From: Lucas Mathews Date: Sat, 8 Jun 2024 20:59:00 +0200 Subject: [PATCH] Start migration to postgresql and other minor fixes and improvements --- Dockerfile | 6 +- application/login.py | 3 + database/class_account.py | 43 ---------- database/class_base.py | 6 -- database/class_client.py | 50 ----------- database/class_transaction.py | 38 --------- database/config.py | 7 -- database/database.ini | 5 -- database/database.py | 26 ------ database/test_database_generator.py | 125 ---------------------------- docker-compose.yml | 29 +++++++ requirements.txt | 3 +- server/api.py | 4 +- server/database.py | 18 ++-- server/manager.py | 5 +- server/template_bank.ini | 21 ++--- 16 files changed, 59 insertions(+), 330 deletions(-) delete mode 100644 database/class_account.py delete mode 100644 database/class_base.py delete mode 100644 database/class_client.py delete mode 100644 database/class_transaction.py delete mode 100644 database/config.py delete mode 100644 database/database.ini delete mode 100644 database/database.py delete mode 100644 database/test_database_generator.py create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index af9405e..53cb616 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,12 +5,12 @@ FROM python:3.12.3 LABEL maintainer="522499@student.fontys.nl" -WORKDIR /banking-system +WORKDIR /server -COPY / /banking-system/ +COPY . /server EXPOSE 81 -RUN pip install --no-cache-dir --upgrade -r /banking-system/requirements.txt +RUN pip install --no-cache-dir --upgrade -r /requirements.txt ENTRYPOINT ["python", "./api.py", "--host", "0.0.0.0", "--port", "81"] diff --git a/application/login.py b/application/login.py index 9c3f1e0..a85cfcf 100644 --- a/application/login.py +++ b/application/login.py @@ -1,3 +1,6 @@ +# Lucas Mathews - Fontys Student ID: 5023572 +# Banking System Login Page + import tkinter as tk from tkinter import messagebox import customtkinter diff --git a/database/class_account.py b/database/class_account.py deleted file mode 100644 index f8ee9a4..0000000 --- a/database/class_account.py +++ /dev/null @@ -1,43 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Account Class - -from sqlalchemy import ForeignKey, Column, String, Integer, Boolean -from sqlalchemy.orm import relationship - -from class_base import Base - -class Account(Base): - __tablename__ = 'accounts' - account_id = Column("account_id", String, primary_key=True) - client_id = Column(String, ForeignKey('clients.client_id')) - description = Column("description", String) - open_timestamp = Column("open_timestamp", String) - account_type = Column("account_type", String) - balance = Column("balance", Integer) - enabled = Column("enabled", Boolean) - notes = Column("notes", String) - transactions = relationship("Transaction", foreign_keys='Transaction.account_id', backref="account") - - def __init__(self, account_id, client_id, description, open_timestamp, account_type, balance, enabled, notes, transactions): - """Initialises the account object.""" - self.account_id = account_id - self.client_id = client_id - self.description = description - self.open_timestamp = open_timestamp - self.account_type = account_type - self.balance = balance - self.enabled = enabled - self.notes = notes - self.transactions = transactions if transactions is not None else [] - - def to_dict(self): - """Returns the account as a dictionary.""" - return { - "account_id": self.account_id, - "client_id": self.client_id, - "description": self.description, - "open_timestamp": self.open_timestamp, - "account_type": self.account_type, - "balance": self.balance, - "notes": self.notes - } \ No newline at end of file diff --git a/database/class_base.py b/database/class_base.py deleted file mode 100644 index 0439352..0000000 --- a/database/class_base.py +++ /dev/null @@ -1,6 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Base Class - -from sqlalchemy.orm import declarative_base - -Base = declarative_base() \ No newline at end of file diff --git a/database/class_client.py b/database/class_client.py deleted file mode 100644 index 8c7da78..0000000 --- a/database/class_client.py +++ /dev/null @@ -1,50 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Client Class - -from sqlalchemy import Column, String, Boolean -from sqlalchemy.orm import relationship - -from class_base import Base - -class Client(Base): - __tablename__ = 'clients' - client_id = Column("client_id", String, primary_key=True) - name = Column("name", String) - birthdate = Column("birthdate", String) - opening_timestamp = Column("opening_timestamp", String) - address = Column("address", String) - phone_number = Column("phone_number", String) - email = Column("email", String) - hash = Column("hash", String) - notes = Column("notes", String) - enabled = Column("enabled", Boolean) - administrator = Column("administrator", Boolean) - accounts = relationship("Account", backref="client") - - def __init__(self, client_id, name, birthdate, opening_timestamp, address, phone_number, email, hash, notes, enabled, administrator, accounts): - """Initialises the client object.""" - self.client_id = client_id - self.name = name - self.birthdate = birthdate - self.opening_timestamp = opening_timestamp - self.address = address - self.phone_number = phone_number - self.email = email - self.hash = hash - self.notes = notes - self.enabled = enabled - self.administrator = administrator - self.accounts = accounts if accounts is not None else [] - - def to_dict(self): - """Returns the client as a dictionary.""" - return { - "client_id": self.client_id, - "name": self.name, - "birthdate": self.birthdate, - "opening_timestamp": self.opening_timestamp, - "address": self.address, - "phone_number": self.phone_number, - "email": self.email, - } - \ No newline at end of file diff --git a/database/class_transaction.py b/database/class_transaction.py deleted file mode 100644 index eddc3fa..0000000 --- a/database/class_transaction.py +++ /dev/null @@ -1,38 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Transaction Class - -from sqlalchemy import Column, String, Integer, ForeignKey - -from class_base import Base - -class Transaction(Base): - __tablename__ = 'transactions' - transaction_id = Column("transaction_id", String, primary_key=True) - transaction_type = Column("transaction_type", String) - amount = Column("amount", Integer) - timestamp = Column("timestamp", String) - description = Column("description", String) - account_id = Column(String, ForeignKey('accounts.account_id')) - recipient_account_id = Column(String, ForeignKey('accounts.account_id')) - - def __init__(self, transaction_id, transaction_type, amount, timestamp, description, account_id, recipient_account_id = None): - """Initialises the Transaction object.""" - self.transaction_id = transaction_id - self.transaction_type = transaction_type - self.amount = amount - self.timestamp = timestamp - self.description = description - self.account_id = account_id - self.recipient_account_id = recipient_account_id - - def to_dict(self): - """Converts the Transaction object to a dictionary.""" - return { - "transaction_id": self.transaction_id, - "transaction_type": self.transaction_type, - "amount": self.amount, - "timestamp": self.timestamp, - "description": self.description, - "account_id": self.account_id, - "recipient_account_id": self.recipient_account_id - } \ No newline at end of file diff --git a/database/config.py b/database/config.py deleted file mode 100644 index 922ebe5..0000000 --- a/database/config.py +++ /dev/null @@ -1,7 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Config Parser - -import configparser - -CONFIG = configparser.ConfigParser() -CONFIG.read("database.ini") diff --git a/database/database.ini b/database/database.ini deleted file mode 100644 index 8e7fedf..0000000 --- a/database/database.ini +++ /dev/null @@ -1,5 +0,0 @@ -[database] -name=bank.db - -[server] -url=http://0.0.0.0:80 \ No newline at end of file diff --git a/database/database.py b/database/database.py deleted file mode 100644 index f4f30fe..0000000 --- a/database/database.py +++ /dev/null @@ -1,26 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Manager File - -import os.path - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - -from config import CONFIG # Import Config - -if os.path.exists(CONFIG["database"]["name"]): # Check if the database exists - print(f"Database {CONFIG["database"]["name"]} already exists.") -else: - print(f"Database {CONFIG["database"]["name"]} does not exist. Creating it now.") - -db_url : str = "sqlite:///" + CONFIG["database"]["name"] # Sets the database file to be used from the configuration file -print(f"Database file set to: {db_url}") - -engine = create_engine(db_url, echo=True) # Creates the database engine (does not create the database file if it already exists) - -from class_base import Base # Imports the base class required by SQLAlchemy - -Base.metadata.create_all(bind=engine) # Creates the tables in the database from the classes - -Session = sessionmaker(bind=engine) # Creates a session to interact with the database -session = Session() # Creates a session object diff --git a/database/test_database_generator.py b/database/test_database_generator.py deleted file mode 100644 index 4b0a4f3..0000000 --- a/database/test_database_generator.py +++ /dev/null @@ -1,125 +0,0 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Test Database Generator - -ADMIN_EMAIL = "lmath56@hotmail.com" - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from server.class_base import Base -from class_account import Account -from class_client import Client -from class_transaction import Transaction -from faker import Faker -import random -import datetime -import hashlib -import uuid -from datetime import datetime, timedelta - - -def generate_hash(): - seed = str(random.random()).encode('utf-8') - return hashlib.sha512(seed).hexdigest() - -def generate_uuid(): - return str(uuid.uuid4()) - -def generate_uuid_short(): - return str(uuid.uuid4())[:8] - -def current_timestamp(): - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - -def random_date(start, end): - return start + timedelta(seconds=random.randint(0, int((end - start).total_seconds()))) - -def timestamp_this_year(): - start = datetime(datetime.now().year, 1, 1) - end = datetime(datetime.now().year, 12, 31, 23, 59, 59) - return random_date(start, end).strftime("%Y-%m-%d %H:%M:%S") - -def timestamp_this_century(): - start = datetime(2000, 1, 1) - end = datetime(2099, 12, 31, 23, 59, 59) - return random_date(start, end).strftime("%Y-%m-%d %H:%M:%S") - - -engine = create_engine('sqlite:///bank.db') -Base.metadata.create_all(engine) -Session = sessionmaker(bind=engine) -session = Session() - -fake = Faker() -all_account_ids = [] - -for i in range(50): - is_administrator = 1 if i == 0 else 0 - password_hash = "7835062ec36ed529fe22cc63baf3ec18d347dacb21c9801da8ba0848cc18efdf1e51717dd5b1240f7556aca3947aa0722452858be6002c1d46b1f1c311b0e9d8" if i == 0 else generate_hash() - client_id = generate_uuid_short() - client = Client( - client_id=client_id, - name="ADMIN" if i == 0 else fake.name(), - birthdate="ADMIN" if i == 0 else timestamp_this_century(), - opening_timestamp=current_timestamp() if i == 0 else timestamp_this_century(), - address="ADMIN" if i == 0 else fake.address(), - phone_number="ADMIN" if i == 0 else fake.phone_number(), - email=ADMIN_EMAIL if i == 0 else fake.email(), - administrator=is_administrator, - hash=password_hash, - notes=fake.text(max_nb_chars=50), - enabled=1, - accounts=[] - ) - session.add(client) - - for j in range(2): - account_id = generate_uuid_short() - balance = 1000 - - for k in range(40): - transaction_type = random.choice(['Deposit', 'Withdrawal']) - amount = random.randint(1, 200) - - if transaction_type == 'Withdrawal' and balance - amount < 0: - continue - - if transaction_type == 'Deposit': - balance += amount - else: - balance -= amount - - transaction = Transaction( - transaction_id=generate_uuid(), - account_id=account_id, - recipient_account_id=random.choice(all_account_ids) if all_account_ids else account_id, - transaction_type=transaction_type, - amount=amount, - timestamp=timestamp_this_year(), - description=fake.text(max_nb_chars=20) - ) - session.add(transaction) - - account = Account( - account_id=account_id, - client_id=client_id, - description=fake.text(max_nb_chars=20), - open_timestamp=timestamp_this_year(), - account_type=random.choice(['Spending', 'Savings']), - balance=balance, - enabled=1, - notes=fake.text(max_nb_chars=50), - transactions=[] - ) - session.add(account) - all_account_ids.append(account_id) - -session.commit() - -# Retrieve the client_id of the administrator account from the session -admin_client_id = session.query(Client.client_id).filter(Client.administrator == 1).first()[0] - -# Print the client_id of the administrator account -print(f"The client_id of the administrator account of this test database is: {admin_client_id}") -print("The password is: Happymeal1") - -session.close() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f981050 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + db: + image: postgres:latest + container_name: banking_system_db + restart: unless-stopped + environment: + POSTGRES_DB: banking_system + POSTGRES_USER: dbadmin + POSTGRES_PASSWORD: your_db_password + volumes: + - db_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + api: + build: . + container_name: banking_system_api + restart: unless-stopped + environment: + DATABASE_URL: postgres://dbadmin:your_db_password@db/banking_system + depends_on: + - db + ports: + - "8000:80" + +volumes: + db_data: diff --git a/requirements.txt b/requirements.txt index 398565d..626acf7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ sqlalchemy flask-session faker customtkinter -schedule \ No newline at end of file +schedule +psycopg2 \ No newline at end of file diff --git a/server/api.py b/server/api.py index be8c2d4..511cd7f 100644 --- a/server/api.py +++ b/server/api.py @@ -20,7 +20,7 @@ from manager import log_event # Imports the log_event function from the manager def create_app(): """Creates the API using Connexion.""" app = connexion.FlaskApp(__name__) - app.add_api(CONFIG["api_file"]["name"]) + app.add_api(CONFIG["server"]["api_file"]) flask_app = app.app flask_app.config['SECRET_KEY'] = CONFIG["sessions"]["secret_key"] @@ -34,7 +34,7 @@ def API(): app = create_app() debug_value = CONFIG["server"]["debug"] debug = False if debug_value.lower() == 'false' else True - app.run(host=CONFIG["server"]["listen_ip"], port=CONFIG["server"]["port"], debug=debug) + app.run(host=CONFIG["server"]["url"], debug=debug) ################ ### Run Code ### diff --git a/server/database.py b/server/database.py index f4f30fe..ebcf8f5 100644 --- a/server/database.py +++ b/server/database.py @@ -1,26 +1,18 @@ # Lucas Mathews - Fontys Student ID: 5023572 # Banking System Manager File -import os.path - +from config import CONFIG # Import Config from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from config import CONFIG # Import Config +db_url : str = f"{CONFIG['database']['type']}://{CONFIG['database']['user']}:{CONFIG['database']['password']}@{CONFIG['database']['ip']}:{CONFIG['database']['port']}/{CONFIG['database']['name']}" +print(f"Database URL set to: {db_url}") -if os.path.exists(CONFIG["database"]["name"]): # Check if the database exists - print(f"Database {CONFIG["database"]["name"]} already exists.") -else: - print(f"Database {CONFIG["database"]["name"]} does not exist. Creating it now.") - -db_url : str = "sqlite:///" + CONFIG["database"]["name"] # Sets the database file to be used from the configuration file -print(f"Database file set to: {db_url}") - -engine = create_engine(db_url, echo=True) # Creates the database engine (does not create the database file if it already exists) +engine = create_engine(db_url, echo=True) # Creates the database engine from class_base import Base # Imports the base class required by SQLAlchemy Base.metadata.create_all(bind=engine) # Creates the tables in the database from the classes Session = sessionmaker(bind=engine) # Creates a session to interact with the database -session = Session() # Creates a session object +session = Session() # Creates a session object \ No newline at end of file diff --git a/server/manager.py b/server/manager.py index be00771..70cc5d1 100644 --- a/server/manager.py +++ b/server/manager.py @@ -68,6 +68,8 @@ def get_current_client(): def verify_otp(client_id:str, otp:int): """Verifies a one time password for a client. Returns True if the OTP is correct and False otherwise.""" + if CONFIG["smtp"]["true"] == "False": + return True if client_id in otps: stored_otp, creation_time = otps[client_id] if stored_otp == otp and time.time() - creation_time <= 300: # Check if OTP is within 5 minutes @@ -150,10 +152,11 @@ def admin_required(f): @login_required def generate_otp(client_id: str): """Generates a one-time password for a client and sends it to their email address. Returns a success message if the OTP is generated and an error message otherwise.""" + if CONFIG["smtp"]["true"] == "False": + return format_response(True, "OTP generation disabled as SMTP is not enabled."), 200 current_client_id, is_admin = get_current_client() if not is_admin and client_id != current_client_id: return format_response(False, "You can only generate OTPs for your own client account."), 403 - email = get_email(client_id) if email: password = int(random.randint(100000, 999999)) # Generate a 6-digit OTP diff --git a/server/template_bank.ini b/server/template_bank.ini index c80aa2a..aa0517f 100644 --- a/server/template_bank.ini +++ b/server/template_bank.ini @@ -1,15 +1,16 @@ -[database] -name=bank.db - -[api_file] -name=api.yml - [server] +url=http://0.0.0.0:80/ +api_file=api.yml debug=False scheduler=True -[api] -url=http://0.0.0.0:81/ +[database] +type=postgresql +ip=0.0.0.0 +port=5432 +name=bank +user=root +password= [sessions] secret_key= @@ -17,8 +18,8 @@ secret_key= [smtp] enabled=True host= -port= +port=465 username= password= sender_name=Luxbank -sender_email= \ No newline at end of file +sender_email=bank@domain.com \ No newline at end of file