diff --git a/api.yml b/api.yml index 1153eaa..fd0b010 100644 --- a/api.yml +++ b/api.yml @@ -135,7 +135,7 @@ paths: '404': description: client_id not found /Client: - put: + post: tags: - client summary: Update an existing client diff --git a/application/__pycache__/config.cpython-312.pyc b/application/__pycache__/config.cpython-312.pyc index fb7b6f6..642e9e3 100644 Binary files a/application/__pycache__/config.cpython-312.pyc and b/application/__pycache__/config.cpython-312.pyc differ diff --git a/application/__pycache__/connection.cpython-312.pyc b/application/__pycache__/connection.cpython-312.pyc index e78b922..c0cd5f8 100644 Binary files a/application/__pycache__/connection.cpython-312.pyc and b/application/__pycache__/connection.cpython-312.pyc differ diff --git a/application/account.py b/application/account.py index a9cfba7..1aa245c 100644 --- a/application/account.py +++ b/application/account.py @@ -57,9 +57,6 @@ def display_account_info(account_id): label_key.grid(row=0, column=i*2, sticky='w', padx=10) label_value.grid(row=0, column=i*2+1, sticky='w', padx=10) - - - ############## ### Layout ### ############## @@ -87,6 +84,16 @@ display_account_info(account_id) table_frame = customtkinter.CTkFrame(root) table_frame.pack(fill=tk.BOTH, expand=True) +# Add buttons for adding a new transaction, requesting the OTP, and editing the account details +button_frame = customtkinter.CTkFrame(root) +button_frame.pack(fill=tk.X, pady=10) +add_transaction_button = customtkinter.CTkButton(button_frame, text="Add Transaction", command=add_transaction) +add_transaction_button.grid(row=0, column=0, padx=10) +request_otp_button = customtkinter.CTkButton(button_frame, text="Request OTP", command=request_otp) +request_otp_button.grid(row=0, column=1, padx=10) +edit_account_details_button = customtkinter.CTkButton(button_frame, text="Edit Account Details", command=edit_account_details) +edit_account_details_button.grid(row=0, column=2, padx=10) + # Create the transactions table transactions_table = ttk.Treeview(table_frame, columns=("Transaction ID", "Transaction Type", "Amount", "Timestamp", "Description", "Account ID", "Recipient Account ID"), show="headings") transactions_table.pack(fill=tk.BOTH, expand=True) diff --git a/application/app.ini b/application/app.ini index 114f21c..00e8b3a 100644 --- a/application/app.ini +++ b/application/app.ini @@ -8,6 +8,6 @@ dark_theme = dark theme = dark-blue [client] -default_id = 31d90aad +default_id = d18e5ae0 default_password = Happymeal1 diff --git a/application/connection.py b/application/connection.py index 155a70e..6ee20f6 100644 --- a/application/connection.py +++ b/application/connection.py @@ -2,6 +2,7 @@ import requests from requests.models import Response from config import CONFIG import json +from tkinter import messagebox ############## ### System ### @@ -65,9 +66,14 @@ def update_client(client_id, otp_code, email=None, phone_number=None, address=No params['phone_number'] = phone_number if address is not None: params['address'] = address - response = requests.put(CONFIG["server"]["url"] + "/Client", cookies=session_data['session_cookie'], params=params) + response = requests.post(CONFIG["server"]["url"] + "/Client", cookies=session_data['session_cookie'], params=params) response.raise_for_status() return response.json() + except requests.exceptions.HTTPError as e: + if response.status_code == 400: + return {'success': False, 'message': "Invalid OTP."} + print(f"HTTPError: {e}") + return {'success': False, 'message': "Could not connect to the server. Please try again later."} except requests.exceptions.RequestException as e: print(f"RequestException: {e}") return {'success': False, 'message': "Could not connect to the server. Please try again later."} @@ -153,10 +159,12 @@ def generate_otp(): try: with open('application\\session_data.json', 'r') as f: session_data = json.load(f) - client_id = session_data['client_id'] - response = requests.post(CONFIG["server"]["url"] + "/OTP/Generate", cookies=session_data['session_cookie'], params={'client_id': client_id}) - response.raise_for_status() - return response.json() + client_id = session_data['client_id'] + response = requests.post(f"{CONFIG['server']['url']}/OTP/Generate", cookies=session_data['session_cookie'], params={'client_id': client_id}) + if response.status_code == 200: + messagebox.showinfo("OTP", "OTP has been sent to your email.") + else: + response.raise_for_status() except requests.exceptions.RequestException as e: print(f"RequestException: {e}") - return {'success': False, 'message': "Could not connect to the server. Please try again later."} \ No newline at end of file + messagebox.showerror("Error", f"Could not generate OTP: {e}") \ No newline at end of file diff --git a/application/dashboard.py b/application/dashboard.py index 3bce439..1e8b507 100644 --- a/application/dashboard.py +++ b/application/dashboard.py @@ -5,6 +5,7 @@ import os from config import CONFIG from connection import logout_client, get_client, update_client, get_accounts, format_balance, generate_otp + # Global variables email_entry = None phone_entry = None @@ -31,6 +32,16 @@ def logout(): else: messagebox.showerror("Logout failed", json_response['message']) +def exit_application(): + """Logs out the client and exits the application.""" + response = logout_client() + json_response = response.json() + if json_response['success']: + messagebox.showinfo("Logout", "You have been logged out.") + root.quit() + else: + messagebox.showerror("Logout failed", json_response['message']) + def display_client_info(): """Displays the client's information on the dashboard.""" global frame @@ -70,7 +81,7 @@ def edit_details(): global edit_window, email_entry, phone_entry, address_entry, otp_entry edit_window = customtkinter.CTkToplevel(root) edit_window.title("Edit Details") - edit_window.geometry("300x300") + edit_window.geometry("300x350") edit_window.iconbitmap("application/luxbank.ico") edit_window.attributes('-topmost', True) @@ -89,16 +100,17 @@ def edit_details(): address_label.pack() address_entry.pack() - # Add MFA verify button and text box - mfa_button = customtkinter.CTkButton(edit_window, text="Get OTP Code", command=generate_otp) - mfa_button.pack() + customtkinter.CTkLabel(edit_window, text=" ").pack() # Add space under the address box - mfa_label = customtkinter.CTkLabel(edit_window, text="OTP Code: ") + otp_button = customtkinter.CTkButton(edit_window, text="Get OTP Code", command=generate_otp) + otp_button.pack() + + otp_label = customtkinter.CTkLabel(edit_window, text="OTP Code: ") otp_entry = customtkinter.CTkEntry(edit_window) - mfa_label.pack() + otp_label.pack() otp_entry.pack() - save_button = customtkinter.CTkButton(edit_window, text="Verify MFA and Save", command=save_details) + save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=save_details) save_button.pack() edit_window.lift() @@ -109,18 +121,33 @@ def save_details(): new_phone = phone_entry.get() if phone_entry.get() != '' else None new_address = address_entry.get() if address_entry.get() != '' else None otp_code = otp_entry.get() + + if not otp_code: + messagebox.showerror("Error", "OTP code must be entered.") + return + with open('application\\session_data.json', 'r') as f: session_data = json.load(f) client_id = session_data['client_id'] + if not messagebox.askyesno("Confirmation", "Are you sure you want to update the details?"): return - result = update_client(client_id, otp_code, new_email, new_phone, new_address) - if result['success']: - display_client_info() - else: - messagebox.showerror("Update Failed", result.get('message', 'Unknown error')) - edit_window.destroy() - + + try: + result = update_client(client_id, otp_code, new_email, new_phone, new_address) + if result['success']: + display_client_info() + messagebox.showinfo("Success", "Details updated successfully.") + edit_window.destroy() + else: + if result['message'] == "Invalid OTP.": + messagebox.showerror("Error", "MFA details not correct. Please try again.") + else: + messagebox.showerror("Update Failed", result.get('message', 'Unknown error')) + except Exception as e: + messagebox.showerror("Error", f"An error occurred: {e}") + + def populate_table(): """Populates the accounts table with client accounts.""" try: @@ -169,13 +196,21 @@ welcome_label.pack(pady=20) display_client_info() -# Create a logout button -logout_button = customtkinter.CTkButton(root, text="Logout", command=logout) -logout_button.pack(pady=15) +# Create a frame for buttons +button_frame = customtkinter.CTkFrame(root) +button_frame.pack(pady=15, side='top') -# Create the MFA button -mfa_button = customtkinter.CTkButton(root, text="MFA", command=generate_otp) -mfa_button.pack(pady=15, side='left') +# Create the OTP button +otp_button = customtkinter.CTkButton(button_frame, text="Get OTP Code", command=generate_otp) +otp_button.pack(side='left', padx=5) + +# Create the logout button +logout_button = customtkinter.CTkButton(button_frame, text="Logout", command=logout) +logout_button.pack(side='left', padx=5) + +# Create the exit button +exit_button = customtkinter.CTkButton(button_frame, text="Exit", command=exit_application) +exit_button.pack(side='left', padx=5) # Create a frame for the table table_frame = ttk.Frame(root) diff --git a/application/session_data.json b/application/session_data.json index 084acf3..148fffb 100644 --- a/application/session_data.json +++ b/application/session_data.json @@ -1 +1 @@ -{"session_cookie": {"session": "mMTYW-c_n0BE-l7MENT8A1h2Rg4UUrNRJYl7NvXTcS4"}, "client_id": "31d90aad"} \ No newline at end of file +{"session_cookie": {"session": "CjjpiVUxx009emp27lUSxMpJ4Dt1sUi3fV-_VILXFrw"}, "client_id": "d18e5ae0"} \ No newline at end of file diff --git a/emailer.py b/emailer.py index 64adc85..3ba6806 100644 --- a/emailer.py +++ b/emailer.py @@ -1,35 +1,47 @@ -# Lucas Mathews - Fontys Student ID: 5023572 -# Banking System Emailer - -import smtplib, ssl -from config import CONFIG # Import Config +import smtplib +import ssl from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +from config import CONFIG -context = ssl.create_default_context() # Create a secure SSL context -email = "l.mathews@student.fontys.nl" # These three lines are for testing purposes -subject = "Test Email" -body = "This is a test email." +class EmailSendingError(Exception): + """Custom exception raised when an error occurs during email sending.""" + def __init__(self, message, original_error=None): + super().__init__(message) + self.original_error = original_error + def send_email(receiver_email, subject, body): """Sends an email to the specified receiver email address.""" sender_email = CONFIG["smtp"]["sender_email"] - message = MIMEMultipart() # Create a multipart message and set headers - message["From"] = f"{CONFIG["smtp"]["sender_name"]} <{CONFIG["smtp"]["sender_email"]}>" + message = MIMEMultipart() + message["From"] = f"{CONFIG['smtp']['sender_name']} <{sender_email}>" message["To"] = receiver_email message["Subject"] = subject - message.attach(MIMEText(body, "plain")) # Add body to email + message.attach(MIMEText(body, "plain")) text = message.as_string() - 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) - print(f"Email sent to {receiver_email}.") - server.quit() + context = ssl.create_default_context() + try: + 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) + print(f"Email sent to {receiver_email}.") + except Exception as e: + error_message = f"Failed to send email to {receiver_email}: {e}" + print(error_message) + raise EmailSendingError(error_message) + if __name__ == "__main__": - send_email(email, subject, body) \ No newline at end of file + email = "l.mathews@student.fontys.nl" + subject = "Test Email" + body = "This is a test email." + try: + send_email(email, subject, body) + except EmailSendingError: + print("Email sending failed.") diff --git a/manager.py b/manager.py index c870691..9bd63eb 100644 --- a/manager.py +++ b/manager.py @@ -4,6 +4,7 @@ from class_client import Client from class_account import Account from class_transaction import Transaction +from emailer import EmailSendingError # Import the EmailSendingError class to handle email sending errors from flask import jsonify, session as flask_session # Imports the Flask modules import hashlib # For password hashing import datetime # For timestamps @@ -133,17 +134,27 @@ def admin_required(f): return decorated_function @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.""" +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.""" 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 + 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 - send_email(email, "Luxbank One Time Password", f"Your one time password is: {password}"), 200 - otps[client_id] = (password, time.time()) # Store the OTP and the current time - return format_response(True, "Client not found."), 404 + 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 + return format_response(True, "OTP generated and sent successfully."), 200 + except EmailSendingError as e: + print(f"Error sending email: {e}") + error_message = "Error sending email. Please try again later." + if e.original_error: + error_message += f" Original error: {str(e.original_error)}" + return format_response(False, error_message), 500 + else: + return format_response(False, "Email address not found for the client."), 404 ############## @@ -162,37 +173,25 @@ def get_client(client_id:str): return format_response(False, "Client not found."), 404 @login_required -def update_client(client_id:str, otp_code:int, **kwargs): +def update_client(client_id: str, otp_code: int, **kwargs): """Updates a client in the database. If the client is not found, returns an error message.""" current_client_id, is_admin = get_current_client() if not verify_otp(current_client_id, otp_code): - return format_response(False, "Invalid OTP."), 405 + return format_response(False, "Invalid OTP."), 400 # Changed to 400 Bad Request if not is_admin and client_id != current_client_id: return format_response(False, "You can only view your own client information."), 403 - for client in session.query(Client).all(): - if client.client_id == client_id: - name = kwargs.get("name", None) - birthdate = kwargs.get("birthdate", None) - address = kwargs.get("address", None) - phone_number = kwargs.get("phone_number", None) - email = kwargs.get("email", None) - notes = kwargs.get("notes", None) - if name: - client.name = name - if birthdate: - client.birthdate = birthdate - if address: - client.address = address - if phone_number: - client.phone_number = phone_number - if email: - client.email = email - if notes: - client.notes = notes - session.commit() - return format_response(True, f"client_id: {client_id} has been updated."), 200 + + client = session.query(Client).filter_by(client_id=client_id).first() + if client: + for field in ['name', 'birthdate', 'address', 'phone_number', 'email', 'notes']: + if field in kwargs and kwargs[field] is not None: + setattr(client, field, kwargs[field]) + session.commit() + return format_response(True, f"Client ID: {client_id} has been updated."), 200 + return format_response(False, "Client not found."), 404 + @login_required def change_password(client_id:str, password:str, new_password:str, otp:int): """Changes the password for a client in the database. If the client is not found, returns an error message.""" diff --git a/test_database.db b/test_database.db index 4129f7e..9be2bfb 100644 Binary files a/test_database.db and b/test_database.db differ diff --git a/test_database_generator.py b/test_database_generator.py index 5ab4702..42cb917 100644 --- a/test_database_generator.py +++ b/test_database_generator.py @@ -1,14 +1,7 @@ # Lucas Mathews - Fontys Student ID: 5023572 # Banking System Test Database Generator -# This program generates a test database for the banking system. The database contains 50 clients, each with 2 accounts. Each account has 40 transactions. -# The first client is an administrator. The password for the administrator account is "Happymeal1". The program uses the Faker library to generate fake -# data for the clients, accounts, and transactions. The random library is used to generate random data for the accounts and transactions. The program -# creates a new SQLite database called test_database.db and writes the test data to the database. The client ID of the administrator account and the -# password for the administrator account. - -ADMIN_EMAIL = "lmath56@hotmail.com" # Email address of the administrator account - +ADMIN_EMAIL = "lmath56@hotmail.com" from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -19,97 +12,113 @@ from class_transaction import Transaction from faker import Faker import random import datetime -import hashlib +import hashlib import uuid +from datetime import datetime, timedelta -def generate_hash(): # Creates a hash for a password + +def generate_hash(): seed = str(random.random()).encode('utf-8') return hashlib.sha512(seed).hexdigest() -def generate_uuid(): # Generates a unique identifier for transactions +def generate_uuid(): return str(uuid.uuid4()) -def generate_uuid_short(): # Generates a short uuid for accounts and clients +def generate_uuid_short(): return str(uuid.uuid4())[:8] -def timestamp(): # Returns the current timestamp - return (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) +def current_timestamp(): + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") -engine = create_engine('sqlite:///test_database.db')# Create a new engine for the test database -Base.metadata.create_all(engine) # Create all tables in the test database -Session = sessionmaker(bind=engine) # Create a new sessionmaker bound to the test engine -session = Session() # Create a new session +def random_date(start, end): + return start + timedelta(seconds=random.randint(0, int((end - start).total_seconds()))) -fake = Faker() # Create a Faker instance +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") -all_account_ids = [] # List to store all account IDs +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") -for i in range(50): # Generate 50 clients - is_administrator = 1 if i == 0 else 0 # Set the first client as an administrator - # Set the password hash for the first account so that the password is "Happymeal1" + +engine = create_engine('sqlite:///test_database.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() - all_account_ids.append(client_id) # Add the client ID to the list of account IDs client = Client( client_id=client_id, - name="ADMIN" if i == 0 else fake.name(), - birthdate="ADMIN" if i == 0 else fake.date_of_birth(minimum_age=18, maximum_age=90), - opening_timestamp=timestamp() if i == 0 else fake.date_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, + 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), # Generate fake notes - enabled=1, - accounts=[]) # Empty list for accounts, you can add accounts later) + notes=fake.text(max_nb_chars=50), + enabled=1, + accounts=[] + ) session.add(client) - for j in range(2):# Each client has 2 accounts + for j in range(2): account_id = generate_uuid_short() - balance = 1000 # Initialize balance to 1000 - - for k in range(40): # Each account has 40 transactions - if not all_account_ids: # Skip creating a transaction if there are no accounts yet - continue + 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: # Skip withdrawal if it would make balance negative + if transaction_type == 'Withdrawal' and balance - amount < 0: continue - if transaction_type == 'Deposit': # Update balance based on transaction type + if transaction_type == 'Deposit': balance += amount - elif transaction_type == 'Withdrawal': + else: balance -= amount transaction = Transaction( transaction_id=generate_uuid(), account_id=account_id, - recipient_account_id=random.choice(all_account_ids), + recipient_account_id=random.choice(all_account_ids) if all_account_ids else account_id, transaction_type=transaction_type, amount=amount, - timestamp=fake.date_this_year(), - description=fake.text(max_nb_chars=50) + 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=200), - open_timestamp=fake.date_this_century(), + description=fake.text(max_nb_chars=20), + open_timestamp=timestamp_this_year(), account_type=random.choice(['Spending', 'Savings']), - balance=balance, # Set balance to calculated balance + balance=balance, enabled=1, notes=fake.text(max_nb_chars=50), - transactions=[]) + transactions=[] + ) session.add(account) all_account_ids.append(account_id) -session.commit() # Commit the session to write the test data to the database +session.commit() -print(f"The client_id of the administrator account of this test database is: {all_account_ids[0]}. The password is: Happymeal1") +# 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] -session.close() # Close the session \ No newline at end of file +# 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}. The password is: Happymeal1") + +session.close() \ No newline at end of file