From b835653fb85a77b5712319edac9da8703b72bf7b Mon Sep 17 00:00:00 2001 From: Lucas Mathews Date: Sun, 26 May 2024 18:03:50 +0200 Subject: [PATCH] Started dashboard implementation --- api.yml | 27 ++++- application/app.ini | 4 +- application/connection.py | 102 ++++++++++++++---- application/dashboard.py | 184 ++++++++++++++++++++++++++++++-- application/login.py | 19 ++-- application/session_cookie.json | 2 +- application/session_data.json | 1 + manager.py | 21 +++- test_database.db | Bin 753664 -> 753664 bytes 9 files changed, 320 insertions(+), 40 deletions(-) create mode 100644 application/session_data.json diff --git a/api.yml b/api.yml index ab8ac75..061a0fb 100644 --- a/api.yml +++ b/api.yml @@ -134,7 +134,7 @@ paths: description: OTP not valid '404': description: client_id not found - /Client/Client: + /Client: put: tags: - client @@ -213,6 +213,31 @@ paths: description: Invalid Client ID supplied '404': description: Client not found + /Client/Accounts: + get: + tags: + - client + summary: Get all accounts for a client + description: Get all accounts for a client + operationId: manager.get_accounts + parameters: + - name: client_id + in: query + description: ID of client to return accounts for + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '400': + description: Invalid input + '404': + description: No accounts found /Account: post: tags: diff --git a/application/app.ini b/application/app.ini index f0864e5..a2afe2a 100644 --- a/application/app.ini +++ b/application/app.ini @@ -4,7 +4,7 @@ port=81 url=http://127.0.0.1:81 [preferences] -dark_theme=system -# Modes: system, light, dark +dark_theme=dark +# Modes: light, dark theme=dark-blue # Themes: blue, dark-blue, green diff --git a/application/connection.py b/application/connection.py index a0845bb..7cf54fd 100644 --- a/application/connection.py +++ b/application/connection.py @@ -9,16 +9,10 @@ import json def authenticate_client(client_id, client_password): try: - # Send a POST request to the /Client/Login endpoint with the client_id and password response = requests.post(CONFIG["server"]["url"] + "/Client/Login", params={'client_id': client_id, 'password': client_password}) - - # Return the response from the API return response except requests.exceptions.RequestException as e: - # If a RequestException is raised, print the exception message print(f"RequestException: {e}") - - # Create a new Response object with a status code of 500 and the error message in the JSON body response = Response() response.status_code = 500 response._content = b'{"success": false, "message": "Could not connect to the server. Please try again later."}' @@ -26,21 +20,93 @@ def authenticate_client(client_id, client_password): def logout_client(): try: - # Load the session cookie from the file - with open('application\\session_cookie.json', 'r') as f: - cookies = json.load(f) - - # Send a POST request to the /Client/Logout endpoint - response = requests.post(CONFIG["server"]["url"] + "/Client/Logout", cookies=cookies) - - # Return the response from the API + with open('application\\session_data.json', 'r') as f: # Open the session_data.json file in read mode + session_data = json.load(f) + response = requests.post(CONFIG["server"]["url"] + "/Client/Logout", cookies=session_data['session_cookie']) return response except requests.exceptions.RequestException as e: - # If a RequestException is raised, print the exception message print(f"RequestException: {e}") - - # Create a new Response object with a status code of 500 and the error message in the JSON body response = Response() response.status_code = 500 response._content = b'{"success": false, "message": "Could not connect to the server. Please try again later."}' - return response \ No newline at end of file + return response + +def get_client(client_id): + try: + with open('application\\session_data.json', 'r') as f: # Open the session_data.json file in read mode + session_data = json.load(f) + response = requests.get(CONFIG["server"]["url"] + "/Client", cookies=session_data['session_cookie'], params={'client_id': client_id}) + return response.json() + except requests.exceptions.RequestException as e: + print(f"RequestException: {e}") + response = Response() + response.status_code = 500 + response._content = b'{"success": false, "message": "Could not connect to the server. Please try again later."}' + return response.json() + +def update_client(client_id, email=None, phone_number=None, address=None): + try: + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + + # Create a dictionary of parameters to update + params = {'client_id': client_id} + if email is not None: + params['email'] = email + if phone_number is not None: + 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.raise_for_status() # Raise an exception if the request failed + return response.json() + except requests.exceptions.RequestException as e: + print(f"RequestException: {e}") + return {'success': False, 'message': "Could not connect to the server. Please try again later."} + +def get_accounts(client_id): + try: + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + + response = requests.get( + CONFIG["server"]["url"] + "/Client/Accounts", + cookies=session_data['session_cookie'], + params={'client_id': client_id} + ) + response.raise_for_status() # Raise an exception if the request failed + accounts = response.json() + if isinstance(accounts, str): # If the response is a string, convert it to a list of dictionaries + accounts = json.loads(accounts) + return accounts + except requests.exceptions.RequestException as e: + print(f"RequestException: {e}") + return {'success': False, 'message': "Could not connect to the server. Please try again later."} + +def format_balance(balance): # Formats the balance as a currency string with comma seperator + return f"€{balance:,.2f}" + +def get_transactions(account_id): + try: + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + + response = requests.get( + CONFIG["server"]["url"] + "/Account/Transactions", + cookies=session_data['session_cookie'], + params={'account_id': account_id} + ) + response.raise_for_status() # Raise an exception if the request failed + transactions = response.json() + if isinstance(transactions, str): # If the response is a string, convert it to a list of dictionaries + transactions = json.loads(transactions) + return transactions + except requests.exceptions.RequestException as e: + print(f"RequestException: {e}") + return {'success': False, 'message': "Could not connect to the server. Please try again later."} + diff --git a/application/dashboard.py b/application/dashboard.py index 61fcf55..3d20ebc 100644 --- a/application/dashboard.py +++ b/application/dashboard.py @@ -2,11 +2,26 @@ # Banking System Dashboard Page import tkinter as tk -from tkinter import messagebox +from tkinter import messagebox, ttk import customtkinter +import json +from config import CONFIG -from connection import logout_client +from connection import logout_client, get_client, update_client, get_accounts, format_balance + + +email_entry = None +phone_entry = None +address_entry = None +frame = None + + + + +################# +### Functions ### +################# def logout(): response = logout_client() # Call the logout_client function @@ -17,21 +32,174 @@ def logout(): else: messagebox.showerror("Logout failed", json_response['message']) -# Create the main window +def display_client_info(): + global frame # Declare frame as global inside the function + if frame is not None: + for widget in frame.winfo_children(): # Destroy all widgets in the frame + widget.destroy() + else: + frame = customtkinter.CTkFrame(root) + frame.pack(anchor='w', side='left', padx=20, pady=20) + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + client_id = session_data['client_id'] + client_info = get_client(client_id) + if 'success' in client_info and client_info['success']: + client = client_info['data'] + fields = [('Name', 'name'), + ('Client ID', 'client_id'), + ('Email', 'email'), + ('Phone', 'phone_number'), + ('Address', 'address'), + ('Account Opened', 'opening_timestamp')] + for i, (display_name, key) in enumerate(fields): + value = client.get(key, 'N/A') # Use 'N/A' as the default value if the key is not found + label_key = customtkinter.CTkLabel(frame, text=f"{display_name}: ", font=("Helvetica", 14)) + label_value = customtkinter.CTkLabel(frame, text=value, font=("Helvetica", 14)) + label_key.grid(row=i, column=0, sticky='e') + label_value.grid(row=i, column=1, sticky='w') + else: + error_label = customtkinter.CTkLabel(root, text="Error: Could not retrieve client information", font=("Helvetica", 14)) + error_label.pack(pady=20) + + edit_button = customtkinter.CTkButton(frame, text="Edit Details", command=edit_details) + edit_button.grid(row=len(fields), column=0, columnspan=2) + + +def edit_details(): + global edit_window, email_entry, phone_entry, address_entry # Declare the variables as global inside the function + edit_window = customtkinter.CTkToplevel(root) + edit_window.title("Edit Details") + edit_window.geometry("300x200") + edit_window.attributes('-topmost', True) + + + email_label = customtkinter.CTkLabel(edit_window, text="Email: ") + email_entry = customtkinter.CTkEntry(edit_window) + email_label.pack() + email_entry.pack() + + phone_label = customtkinter.CTkLabel(edit_window, text="Phone: ") + phone_entry = customtkinter.CTkEntry(edit_window) + phone_label.pack() + phone_entry.pack() + + address_label = customtkinter.CTkLabel(edit_window, text="Address: ") + address_entry = customtkinter.CTkEntry(edit_window) + address_label.pack() + address_entry.pack() + + save_button = customtkinter.CTkButton(edit_window, text="Save", command=save_details) + save_button.pack() + edit_window.lift() + +def save_details(): + global edit_window, email_entry, phone_entry, address_entry + new_email = email_entry.get() if email_entry.get() != '' else None + new_phone = phone_entry.get() if phone_entry.get() != '' else None + new_address = address_entry.get() if address_entry.get() != '' else None + + # Get the client_id from the session data + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + client_id = session_data['client_id'] + + # Display a confirmation dialog box + if not messagebox.askyesno("Confirmation", "Are you sure you want to update the details?"): + return # If the user clicked 'No', exit the function + + # Update the client details + result = update_client(client_id, new_email, new_phone, new_address) + # If the request was successful, update the client info displayed in the main window + if result['success']: + display_client_info() + + # Destroy the window after updating the client info + edit_window.destroy() + +def populate_table(): + # Get the client_id from the session data + with open('application\\session_data.json', 'r') as f: + session_data = json.load(f) + client_id = session_data['client_id'] + + # Get the accounts for the client + response = get_accounts(client_id) + accounts = response['data'] if 'data' in response else [] + + # Check if accounts is a list + if not isinstance(accounts, list): + print(f"Error: Expected a list of accounts, but got {type(accounts)}") + return + + # Populate the table with the accounts + for account in accounts: + formatted_balance = format_balance(account['balance']) + table.insert('', 'end', values=(account['description'], account['account_id'], formatted_balance, account['account_type'])) + +def on_account_double_click(event): + # Get the selected account + selected_account = table.item(table.selection()) + + # Open a new window + account_window = tk.Toplevel(root) + + # Display the account details + display_account_details(account_window, selected_account) + +############## +### Layout ### +############## + root = customtkinter.CTk() -# Set the window title, icon, and size root.title("Luxbank Dashboard") root.iconbitmap("application/luxbank.ico") -root.geometry("800x600") +root.geometry("800x350") -# Create a label with a welcome message +# Check if dark mode is enabled +if CONFIG["preferences"]["dark_theme"] == "dark": + # Set the style for dark mode + customtkinter.set_appearance_mode("dark") +else: + # Set the style for light mode + customtkinter.set_appearance_mode("light") + +# Create a label for the title welcome_label = customtkinter.CTkLabel(root, text="Welcome to the Luxbank Dashboard!", font=("Helvetica", 24)) welcome_label.pack(pady=20) -# Create a Logout button +display_client_info() + logout_button = customtkinter.CTkButton(root, text="Logout", command=logout) logout_button.pack(pady=15) -# Start the main loop +# Create a frame for the table +table_frame = ttk.Frame(root) +table_frame.pack(side='right', fill='both', expand=True) + +# Create the table +table = ttk.Treeview(table_frame, columns=('Description', 'Account ID', 'Balance', 'Account Type'), show='headings') +table.heading('Description', text='Description') +table.heading('Account ID', text='Account ID') +table.heading('Balance', text='Balance') +table.heading('Account Type', text='Account Type') + +# Set the column widths +table.column('Description', width=200) +table.column('Account ID', width=100) +table.column('Balance', width=100) +table.column('Account Type', width=100) + +table.pack(fill='both', expand=True) + +populate_table() + +# Create a scrollbar for the table +scrollbar = ttk.Scrollbar(table_frame, orient='vertical', command=table.yview) +scrollbar.pack(side='right', fill='y') +table.configure(yscrollcommand=scrollbar.set) + +table.bind("", on_account_double_click) + root.mainloop() \ No newline at end of file diff --git a/application/login.py b/application/login.py index 9ce315f..f00d5e0 100644 --- a/application/login.py +++ b/application/login.py @@ -5,7 +5,9 @@ from tkinter import messagebox import customtkinter import os import json -from connection import * +import requests +from connection import authenticate_client +from config import CONFIG ################# ### Functions ### @@ -18,18 +20,19 @@ def login(): response = authenticate_client(client_id, client_password) # Authenticate the client json_response = response.json() # Convert the response content to JSON if json_response["success"] == True: # If the authentication is successful, open the dashboard - # Save the session cookie to a file - with open('application\\session_cookie.json', 'w') as f: - json.dump(response.cookies.get_dict(), f) + session_data = { + 'session_cookie': response.cookies.get_dict(), + 'client_id': client_id + } + with open('application\\session_data.json', 'w') as f: # Save the session data to a file + json.dump(session_data, f) root.destroy() os.system("python application\\dashboard.py") else: - messagebox.showerror("Login failed", json_response["message"]) # If the authentication fails, show an error message + messagebox.showerror("Login failed", json_response["message"]) except requests.exceptions.RequestException as e: - # If a RequestException is raised, show an error message that includes the exception message messagebox.showerror("Login failed", "Could not connect to the server. Please try again later. Error: " + str(e)) - ############## ### Layout ### ############## @@ -54,6 +57,8 @@ entry_password.pack(pady=10) login_button= customtkinter.CTkButton(root, text="Login", command=login) login_button.pack(pady=15) +root.bind('', lambda event=None: login()) + ########### ### Run ### ########### diff --git a/application/session_cookie.json b/application/session_cookie.json index 7df7fc8..55cd6c2 100644 --- a/application/session_cookie.json +++ b/application/session_cookie.json @@ -1 +1 @@ -{"session": "rZS5tQOS4nXGJu-WXEg6_Ls5q8njy3GNiZ3s8N3YHEA"} \ No newline at end of file +{"session": "ECxyI0rswhPqvD9bW2KiRsWLmRsVY8bhxDbK40X1_Ok"} \ No newline at end of file diff --git a/application/session_data.json b/application/session_data.json new file mode 100644 index 0000000..93dd9c7 --- /dev/null +++ b/application/session_data.json @@ -0,0 +1 @@ +{"session_cookie": {"session": "xC02SzGKn_a4_R2fhKuj8qNWFk1MIx9zjatqzyzpRBM"}, "client_id": "31d90aad"} \ No newline at end of file diff --git a/manager.py b/manager.py index a7bd808..73ad0d9 100644 --- a/manager.py +++ b/manager.py @@ -13,6 +13,8 @@ import time # For OTP generation 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 flask import session as flask_session +from database import session otps = {} # Temporary dictionary to store OTPs and their creation time @@ -47,10 +49,12 @@ def format_response(success: bool, message: str = '', data: dict = None): # Form response['data'] = data return jsonify(response) -def get_current_client(): # Returns the current client and if they are an administrator +def get_current_client(): client = flask_session['client_id'] - is_admin = session.query(Client).filter_by(client_id=client).one_or_none().administrator - return client, is_admin + client_obj = session.query(Client).filter_by(client_id=client).one_or_none() + if client_obj is None: + return None, None + return client_obj.client_id, client_obj.administrator def verify_otp(client_id:str, otp:int): # Verifies a one time password for a client if client_id in otps and otps[client_id][0] == otp: @@ -186,6 +190,17 @@ def change_password(client_id:str, password:str, new_password:str, otp:int): # C return format_response(False, "Invalid password."), 400 return format_response(False, "Client not found."), 404 +@login_required +def get_accounts(client_id: str): + current_client_id, is_admin = get_current_client() + if current_client_id is None: + # return an appropriate response or raise an exception + raise Exception("No current client found") + if not is_admin and client_id != current_client_id: + return format_response(False, "You can only view your own client information."), 403 + accounts = session.query(Account).filter(Account.client_id == client_id) + return format_response(True, "", [account.to_dict() for account in accounts]), 200 + ############### ### Account ### ############### diff --git a/test_database.db b/test_database.db index e2b8de417e3e1b6ed8fdfe5166f440d97c61eef8..a9cfd260edb90109a61fedf2e8747505950660dc 100644 GIT binary patch delta 289 zcmZo@&}(SWn;^}|Gf~Ewm4`vEtb1e1+LQU+9yt3lr#*Ot5 z+E{V2w7j;gQ&E0uc1C_#YF=?_d1|_Xp^>?PnW4G4g}I4^#pHnc4V%pcZ}GA)FfhEI zEGlotYGiC;YP>n1exm_5p96y+g9iiuVt%L1JR3~-fQlHICcD^O0IBYuEMc#u8PCKi zE32s}&BPgQV31;HV3}lQY?xwckeHYfoLH8bm#*NLoSa{pS2FpIorQKZBT%)jqAU}q zXR1M}X-bN*0bF%KYF-MG`sQ={+t2N11Y#y2W(HywAZ7((HXvpPVh$kY+n{@?_@K4q#