From 1c614f23b6bb5007c2d0f45755f1ec7846e08084 Mon Sep 17 00:00:00 2001 From: Lucas Mathews Date: Sat, 15 Jun 2024 20:11:04 +0200 Subject: [PATCH] Before move to Gunicorn --- Dockerfile | 12 +++--- application/login.py | 2 +- docker-compose.yml | 17 +++++--- server/requirements.txt => requirements.txt | 3 +- server/api.py | 9 ++--- server/api.yml | 2 +- server/database.py | 35 +++++++++++++--- server/emailer.py | 8 ++-- server/logger.py | 15 +++++++ server/manager.py | 45 +++++++++------------ server/scheduler.py | 4 +- 11 files changed, 96 insertions(+), 56 deletions(-) rename server/requirements.txt => requirements.txt (80%) create mode 100644 server/logger.py diff --git a/Dockerfile b/Dockerfile index ed0694c..ebbb1a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ # Lucas Mathews - Fontys Student ID: 5023572 # Banking System DockerFile -FROM python:3.12.3 - -LABEL maintainer="522499@student.fontys.nl" +FROM python:3.12.3-slim as base WORKDIR /server COPY server/ /server/ - -EXPOSE 80 +COPY requirements.txt /server/ RUN pip install --no-cache-dir --upgrade -r /server/requirements.txt -ENTRYPOINT ["python", "/server/api.py"] +EXPOSE 8066 + +ENTRYPOINT ["python"] +CMD ["api.py"] \ No newline at end of file diff --git a/application/login.py b/application/login.py index a85cfcf..0ca7db6 100644 --- a/application/login.py +++ b/application/login.py @@ -40,7 +40,7 @@ def login(): except requests.exceptions.HTTPError: messagebox.showerror("Login failed", "Invalid client ID or password.") except requests.exceptions.ConnectionError: - messagebox.showerror("Connection Error", "Could not connect to the server.") + messagebox.showerror("Connection Error", f"Could not connect to the server on {CONFIG['server']['url']}") def change_dark_theme(): """Change the theme between dark and light.""" diff --git a/docker-compose.yml b/docker-compose.yml index 5a83188..4868555 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: db: image: postgres:latest @@ -12,16 +10,25 @@ services: volumes: - db_data:/var/lib/postgresql/data ports: - - "5432:5432" + - 5432:5432 + networks: + - bank api: - build: . + build: + context: . + dockerfile: Dockerfile container_name: banking_system_api restart: unless-stopped depends_on: - db ports: - - "8000:80" + - 8066:8066 + networks: + - bank volumes: db_data: + +networks: + bank: \ No newline at end of file diff --git a/server/requirements.txt b/requirements.txt similarity index 80% rename from server/requirements.txt rename to requirements.txt index 626acf7..88baeb9 100644 --- a/server/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ flask-session faker customtkinter schedule -psycopg2 \ No newline at end of file +psycopg2-binary +gunicorn \ No newline at end of file diff --git a/server/api.py b/server/api.py index bddade5..137d454 100644 --- a/server/api.py +++ b/server/api.py @@ -11,7 +11,6 @@ from config import CONFIG # Imports the configuration file from manager import * # Imports the Manager file that contains the functions for the API from flask_session import Session # Imports the session module from scheduler import run_schedule # Imports the scheduler module -from manager import log_event # Imports the log_event function from the manager module ################# ### Connexion ### @@ -34,21 +33,21 @@ def API(): app = create_app() debug_value = CONFIG["server"]["debug"] debug = False if debug_value.lower() == 'false' else True - app.run(host=CONFIG["server"]["url"], debug=debug) + app.run(host=CONFIG["server"]["host"], port=CONFIG["server"]["port"], debug=debug) ################ ### Run Code ### ################ if __name__ == "__main__": - log_event("Starting API...") # Create a thread that will run the run_schedule function in the background + event_logger("Starting API...") # Create a thread that will run the run_schedule function in the background scheduler = CONFIG["server"]["scheduler"] scheduler = False if scheduler.lower() == 'false' else True if scheduler: thread = threading.Thread(target=run_schedule) thread.daemon = True # Set the thread as a daemon thread thread.start() - log_event("Scheduler started.") + event_logger("Scheduler started.") API() - log_event("API stopped.") # This line will only be reached if the API is stopped + event_logger("API stopped.") # This line will only be reached if the API is stopped \ No newline at end of file diff --git a/server/api.yml b/server/api.yml index aee0763..03cc0dc 100644 --- a/server/api.yml +++ b/server/api.yml @@ -7,7 +7,7 @@ info: email: 522499@student.fontys.nl version: 3.0.0 servers: - - url: http://127.0.0.1:81 + - url: / tags: - name: client description: Operations for Client Accounts diff --git a/server/database.py b/server/database.py index ccd4c56..e7b198b 100644 --- a/server/database.py +++ b/server/database.py @@ -1,9 +1,12 @@ # Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Manager File +# Banking System Databas File from config import CONFIG # Import Config from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import OperationalError +import time +from logger import event_logger db_type = CONFIG.get('database', 'type') db_user = CONFIG.get('database', 'user') @@ -14,11 +17,31 @@ db_name = CONFIG.get('database', 'name') db_url : str = f"{db_type}://{db_user}:{db_password}@{db_ip}:{db_port}/{db_name}" -engine = create_engine(db_url, echo=True) # Creates the database engine +try: # Retry connecting to the database with a retry mechanism + max_retries = 10 + retries = 0 + engine = None + while retries < max_retries: + try: + engine = create_engine(db_url, echo=True) + break + except OperationalError as e: + event_logger(f"Failed to connect to database: {e}") + retries += 1 + event_logger(f"Retrying ({retries}/{max_retries})...") + time.sleep(10) # Wait 10 seconds before retrying -from class_base import Base # Imports the base class required by SQLAlchemy + if 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 -Base.metadata.create_all(bind=engine) # Creates the tables in the database from the classes +except Exception as e: + event_logger(f"Error: {e}") -Session = sessionmaker(bind=engine) # Creates a session to interact with the database -session = Session() # Creates a session object \ No newline at end of file +finally: + if 'session' in locals(): + session.close() # Close the session when done + +event_logger("Database operations completed.") \ No newline at end of file diff --git a/server/emailer.py b/server/emailer.py index 774eda7..965dd1b 100644 --- a/server/emailer.py +++ b/server/emailer.py @@ -33,12 +33,12 @@ def send_email(receiver_email, subject, body): with smtplib.SMTP_SSL(CONFIG["smtp"]["host"], CONFIG["smtp"]["port"], context=context) as server: server.login(CONFIG["smtp"]["username"], CONFIG["smtp"]["password"]) server.sendmail(sender_email, receiver_email, text) - from manager import log_event - log_event(f"Email '{subject}' sent to {receiver_email}") # Log the message + from manager import event_logger + event_logger(f"Email '{subject}' sent to {receiver_email}") # Log the message except Exception as e: error_message = f"Failed to send email to {receiver_email}: {e}" - from manager import log_event - log_event(error_message) # Log the error + from manager import event_logger + event_logger(error_message) # Log the error raise EmailSendingError(error_message) diff --git a/server/logger.py b/server/logger.py new file mode 100644 index 0000000..fbf4250 --- /dev/null +++ b/server/logger.py @@ -0,0 +1,15 @@ +# Lucas Mathews - Fontys Student ID: 5023572 +# Banking System Logger + +import datetime + +def timestamp(): + """Returns the current timestamp in the format 'YYYY-MM-DD HH:MM:SS'.""" + return (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + +def event_logger(data_to_log:str): + """Logs an event to the log file.""" + import os + print(os.getcwd()) + with open("log.txt", "a") as log_file: + log_file.write(f"{timestamp()} - {data_to_log}\n") \ No newline at end of file diff --git a/server/manager.py b/server/manager.py index 70cc5d1..135dbfd 100644 --- a/server/manager.py +++ b/server/manager.py @@ -9,6 +9,7 @@ from flask import jsonify, session as flask_session # Imports the Flask modules from functools import wraps # For decorators / user login from database import * # Importing the database connection from emailer import send_email # Importing the emailer function +from logger import event_logger # Importing the event_logger function from flask import session as flask_session from flask import request from database import session @@ -89,13 +90,7 @@ def clean_expired_otps(): for client_id in expired_otps: delete_otp(client_id) otps_removed += 1 - log_event(f"Cleaned {otps_removed} expired OTPs.") - -def log_event(data_to_log:str): - """Logs an event to the log file.""" - with open("log.txt", "a") as log_file: - log_file.write(f"{timestamp()} - {data_to_log}\n") - + event_logger(f"Cleaned {otps_removed} expired OTPs.") ###################### ### Authentication ### @@ -109,7 +104,7 @@ def login(): client = session.query(Client).filter_by(client_id=client_id).first() if client and client.hash == client_hash: flask_session['client_id'] = client_id - log_event(f"{client_id} logged in successfully.") + event_logger(f"{client_id} logged in successfully.") return format_response(True, f"{flask_session['client_id']} logged in successfully."), 200 return format_response(False, "Invalid client_id or password."), 401 @@ -163,11 +158,11 @@ def generate_otp(client_id: str): try: send_email(email, "Luxbank One Time Password", f"Your one-time password is: {password}") otps[client_id] = (password, time.time()) # Store the OTP and the current time - log_event(f"OTP Code {password} emailed to {email}") + event_logger(f"OTP Code {password} emailed to {email}") print(f"OTP Code {password} emailed to {email}") return format_response(True, "OTP generated and sent successfully."), 200 except EmailSendingError as e: - log_event(f"Error sending email: {str(e)}") + event_logger(f"Error sending email: {str(e)}") error_message = "Error sending email. Please try again later." if e.original_error: error_message += f" Original error: {str(e.original_error)}" @@ -232,7 +227,7 @@ def change_password(): client.hash = hash_new_password session.commit() delete_otp(current_client_id) - log_event(f"Password for client_id {client_id} has been updated by {current_client_id}.") + event_logger(f"Password for client_id {client_id} has been updated by {current_client_id}.") return format_response(True, f"Password for client_id {client_id} has been updated."), 200 else: return format_response(False, "Invalid old password."), 400 @@ -406,7 +401,7 @@ def delete_client(client_id:str): if client.accounts == None: session.delete(client) session.commit() - log_event(f"Client ID: {client_id} has been removed by {flask_session['client_id']}.") + event_logger(f"Client ID: {client_id} has been removed by {flask_session['client_id']}.") return format_response(True, f"client_id: {client_id} has been removed."), 200 else: return format_response(False, "Client has accounts and can not be removed."), 400 @@ -420,7 +415,7 @@ def delete_account(account_id:str): if account.balance == 0: session.delete(account) session.commit() - log_event(f"Account ID: {account_id} has been removed by {flask_session['client_id']}.") + event_logger(f"Account ID: {account_id} has been removed by {flask_session['client_id']}.") return format_response(True, f"account_id: {account_id} has been removed."), 200 else: return format_response(False, "Account has a balance and can not be removed."), 400 @@ -430,21 +425,21 @@ def delete_account(account_id:str): def get_all_clients(): """Returns all clients in the database.""" clients = session.query(Client).all() - log_event(f"All clients have been retrieved by {flask_session['client_id']}.") + event_logger(f"All clients have been retrieved by {flask_session['client_id']}.") return format_response(True, "", [client.to_dict() for client in clients]), 200 @admin_required def get_all_accounts(): """Returns all accounts in the database.""" accounts = session.query(Account).all() - log_event(f"All accounts have been retrieved by {flask_session['client_id']}.") + event_logger(f"All accounts have been retrieved by {flask_session['client_id']}.") return format_response(True, "", [account.to_dict() for account in accounts]), 200 @admin_required def get_all_transactions(): """Returns all transactions in the database.""" transactions = session.query(Transaction).all() - log_event(f"All transactions have been retrieved by {flask_session['client_id']}.") + event_logger(f"All transactions have been retrieved by {flask_session['client_id']}.") return format_response(True, "", [transaction.to_dict() for transaction in transactions]), 200 @admin_required @@ -455,7 +450,7 @@ def apply_interest(account_id:int, interest_rate:float): interest = account.balance * interest_rate account.balance += interest session.commit() - log_event(f"Interest of €{interest} has been applied to Account ID: {account_id} by {flask_session['client_id']}.") + event_logger(f"Interest of €{interest} has been applied to Account ID: {account_id} by {flask_session['client_id']}.") return format_response(True, f"€{interest} in interest has been applied to Account ID: {account_id}."), 200 return format_response(False, "Account not found."), 404 @@ -466,7 +461,7 @@ def apply_fee(account_id:int, fee:float): if account.account_id == account_id: account.balance -= fee session.commit() - log_event(f"Fee of €{fee} has been applied to Account ID: {account_id} by {flask_session['client_id']}.") + event_logger(f"Fee of €{fee} has been applied to Account ID: {account_id} by {flask_session['client_id']}.") return format_response(True, f"€{fee} in fees has been applied to Account ID: {account_id}."), 200 return format_response(False, "Account not found."), 404 @@ -477,7 +472,7 @@ def delete_transaction(transaction_id:int): if transaction.transaction_id == transaction_id: session.delete(transaction) session.commit() - log_event(f"Transaction ID: {transaction_id} has been removed by {flask_session['client_id']}.") + event_logger(f"Transaction ID: {transaction_id} has been removed by {flask_session['client_id']}.") return format_response(True, f"Transaction ID: {transaction_id} has been removed."), 200 return format_response(False, "Transaction not found."), 404 @@ -488,14 +483,14 @@ def modify_balance(transaction_id:int, amount:int): if transaction.transaction_id == transaction_id: transaction.amount = amount session.commit() - log_event(f"Transaction ID: {transaction_id} has been modified by {flask_session['client_id']}.") + event_logger(f"Transaction ID: {transaction_id} has been modified by {flask_session['client_id']}.") return format_response(True, f"Transaction ID: {transaction_id} has been modified."), 200 return format_response(False, "Transaction not found."), 404 @admin_required def test_account_balances(): """Checks all account balances in the database and returns a list of discrepancies.""" - log_event(f"Account balances have been checked by {flask_session['client_id']}.") + event_logger(f"Account balances have been checked by {flask_session['client_id']}.") all_transactions = session.query(Transaction).all()# Get all transactions from the database calculated_balances = {} # Initialize a dictionary to store the calculated balance for each account for transaction in all_transactions: # Go through each transaction @@ -520,7 +515,7 @@ def add_client(name:str, birthdate:str, address:str, phone_number:str, email:str new_client = Client(client_id, name, birthdate, timestamp(), address, phone_number, email, hash_password(password), notes, 1, 0, None) session.add(new_client) session.commit() - log_event(f"New client has been added: client_id: {client_id} by {flask_session['client_id']}.") + event_logger(f"New client has been added: client_id: {client_id} by {flask_session['client_id']}.") return format_response(True, f"New client has been added: client_id: {client_id}"), 200 def initialise_database(password:str, email:str): @@ -533,7 +528,7 @@ def initialise_database(password:str, email:str): admin_client = session.query(Client).filter_by(name='ADMIN').one() # Retrieve the administrator client admin_client.administrator = 1 # Set the new client as an administrator session.commit() - log_event(f"Database initialised with administrator account with client_id {admin_client.client_id}.") + event_logger(f"Database initialised with administrator account with client_id {admin_client.client_id}.") return format_response(True, f"Database initialised with administrator account with client_id {admin_client.client_id}"), 200 return format_response(False, "Database not empty."), 400 @@ -544,7 +539,7 @@ def promote_to_admin(client_id:str): if client.client_id == client_id: client.administrator = 1 session.commit() - log_event(f"Client ID: {client_id} has been promoted to administrator by {flask_session['client_id']}.") + event_logger(f"Client ID: {client_id} has been promoted to administrator by {flask_session['client_id']}.") return format_response(True, f"client_id: {client_id} has been promoted to administrator."), 200 return format_response(False, f"client_id: {client_id} is not found."), 404 @@ -555,7 +550,7 @@ def demote_from_admin(client_id:str): if client.client_id == client_id: client.administrator = 0 session.commit() - log_event(f"Client ID: {client_id} has been demoted from administrator by {flask_session['client_id']}.") + event_logger(f"Client ID: {client_id} has been demoted from administrator by {flask_session['client_id']}.") return format_response(True, f"client_id: {client_id} has been demoted from administrator."), 200 return format_response(False, f"client_id: {client_id} is not found."), 404 diff --git a/server/scheduler.py b/server/scheduler.py index 21aaecf..465761d 100644 --- a/server/scheduler.py +++ b/server/scheduler.py @@ -4,7 +4,7 @@ import threading import schedule import time -from manager import log_event +from manager import event_logger stop_event = threading.Event() @@ -18,7 +18,7 @@ def clean_otp(): print("Cleaning OTPs...") from manager import clean_expired_otps removed_otps = clean_expired_otps() - log_event(f"Removed {removed_otps} expired OTPs.") + event_logger(f"Removed {removed_otps} expired OTPs.") schedule.every(300).seconds.do(clean_otp)