From a8f4a061ca1cde7f82e7df3c2dc69baba3ba4073 Mon Sep 17 00:00:00 2001 From: Lucas Mathews Date: Thu, 20 Jun 2024 22:25:26 +0200 Subject: [PATCH] Working on CLI Implementation --- .gitignore | 1 + cli/class_account.py | 43 +++++++++++ cli/class_base.py | 6 ++ cli/class_client.py | 50 +++++++++++++ cli/class_transaction.py | 38 ++++++++++ cli/cli.ini | 8 +-- cli/cli.py | 10 ++- cli/config.py | 2 +- cli/connection.py | 52 +++++++++++--- cli/template_cli.ini | 8 +++ cli/test_database_generator.py | 128 +++++++++++++++++++++++++++++++++ server/manager.py | 2 +- server/scheduler.py | 2 +- 13 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 cli/class_account.py create mode 100644 cli/class_base.py create mode 100644 cli/class_client.py create mode 100644 cli/class_transaction.py create mode 100644 cli/template_cli.ini create mode 100644 cli/test_database_generator.py diff --git a/.gitignore b/.gitignore index 4b1caa0..dadddb2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ bank.db log.txt bank.ini app.ini +cli.ini # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/cli/class_account.py b/cli/class_account.py new file mode 100644 index 0000000..f8ee9a4 --- /dev/null +++ b/cli/class_account.py @@ -0,0 +1,43 @@ +# 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/cli/class_base.py b/cli/class_base.py new file mode 100644 index 0000000..0439352 --- /dev/null +++ b/cli/class_base.py @@ -0,0 +1,6 @@ +# 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/cli/class_client.py b/cli/class_client.py new file mode 100644 index 0000000..8c7da78 --- /dev/null +++ b/cli/class_client.py @@ -0,0 +1,50 @@ +# 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/cli/class_transaction.py b/cli/class_transaction.py new file mode 100644 index 0000000..eddc3fa --- /dev/null +++ b/cli/class_transaction.py @@ -0,0 +1,38 @@ +# 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/cli/cli.ini b/cli/cli.ini index 9549bef..56e8070 100644 --- a/cli/cli.ini +++ b/cli/cli.ini @@ -1,8 +1,8 @@ [server] ip = 0.0.0.0 -port = 81 -url = http://127.0.0.1:81 +port = 8066 +url = http://127.0.0.1:8066 [client] -default_id = -default_password = \ No newline at end of file +default_id = a9fa4899 +default_password = Happymeal1 \ No newline at end of file diff --git a/cli/cli.py b/cli/cli.py index cdeaec6..6d6e27d 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -22,10 +22,16 @@ def main(): print("Username and password are required for login.") sys.exit(1) response = login(args.username, args.password) - print(f"{response.status_code}: {response.content}") + if response['success']: + print(f"Login successful: {response['message']}") + else: + print(f"Login failed: {response['message']}") elif args.command == 'logout': response = logout() - print(f"{response.status_code}: {response.content}") + if response['success']: + print(f"Logout successful: {response['message']}") + else: + print(f"Check Credentials: {response['message']}") else: print("Invalid command. Use 'login' or 'logout'.") diff --git a/cli/config.py b/cli/config.py index cd8fe0e..3d292f4 100644 --- a/cli/config.py +++ b/cli/config.py @@ -4,4 +4,4 @@ import configparser CONFIG = configparser.ConfigParser() -CONFIG.read("cli/cli.ini") +CONFIG.read("cli.ini") diff --git a/cli/connection.py b/cli/connection.py index a89f8e7..78b7acf 100644 --- a/cli/connection.py +++ b/cli/connection.py @@ -1,14 +1,50 @@ -# api_client.py +# Lucas Mathews - Fontys Student ID: 5023572 +# Banking System CLI Utility Connection File + import requests from config import CONFIG +import hashlib +import json -def login(username, password): - url = f"{CONFIG['server']['url']}/login" - payload = {'username': username, 'password': password} - response = requests.get(url, params=payload) - return response +############## +### System ### +############## + +def format_balance(balance): + """Formats the balance as a currency string with comma separators.""" + return f"€{balance:,.2f}" + +def hash_password(password:str): + """Hashes a password using the SHA-512 algorithm and returns the hexadecimal representation of the hash.""" + return hashlib.sha512(password.encode()).hexdigest() + +##################### +### API Functions ### +##################### + +def login(client_id, password): + """Authenticates a client with the given client_id and client_hash.""" + try: + client_hash = hash_password(password) + response = requests.post(CONFIG["server"]["url"] + "/Client/Login", json={'client_id': client_id, 'client_hash': client_hash}) + response.raise_for_status() + response_content = json.loads(response.content) # Parse the JSON response + if response.status_code == 200 and response_content.get('success'): + return {'success': True, 'message': response_content.get('message')} + else: + return {'success': False, 'message': response_content.get('message')} + except requests.exceptions.RequestException as e: + return {'success': False, 'message': str(e)} def logout(): url = f"{CONFIG['server']['url']}/logout" - response = requests.get(url) - return response + try: + response = requests.get(url) + response.raise_for_status() + response_content = json.loads(response.content) # Parse the JSON response + if response.status_code == 200 and response_content.get('success'): + return {'success': True, 'message': response_content.get('message')} + else: + return {'success': False, 'message': response_content.get('message')} + except requests.exceptions.RequestException as e: + return {'success': False, 'message': str(e)} diff --git a/cli/template_cli.ini b/cli/template_cli.ini new file mode 100644 index 0000000..04ba943 --- /dev/null +++ b/cli/template_cli.ini @@ -0,0 +1,8 @@ +[server] +ip = 0.0.0.0 +port = 8066 +url = http://127.0.0.1:8066 + +[client] +default_id = +default_password = \ No newline at end of file diff --git a/cli/test_database_generator.py b/cli/test_database_generator.py new file mode 100644 index 0000000..7fe2d8f --- /dev/null +++ b/cli/test_database_generator.py @@ -0,0 +1,128 @@ +# 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 + + +from faker import Faker +import class_account +import class_client +import class_transaction +from connection import login, add_client, add_account, add_transaction, logout +import argparse +import random +import datetime +import hashlib +import uuid + +def generate_hash(): # Creates a hash for a password + seed = str(random.random()).encode('utf-8') + return hashlib.sha512(seed).hexdigest() + +def generate_uuid(): # Generates a unique identifier for transactions + return str(uuid.uuid4()) + +def generate_uuid_short(): # Generates a short uuid for accounts and clients + return str(uuid.uuid4())[:8] + +def timestamp(): # Returns the current timestamp + return (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + +fake = Faker() # Create a Faker instance + +all_account_ids = [] # List to store all account IDs + +# Set up argument parsing +parser = argparse.ArgumentParser(description="Generate test database for the banking system.") +parser.add_argument('-u', '--username', required=True, help="Username for admin login") +parser.add_argument('-p', '--password', required=True, help="Password for admin login") + +args = parser.parse_args() + +# Log in as the admin using provided username and password +client_id = args.username +client_hash = hashlib.sha512(args.password.encode()).hexdigest() +client = login(client_id, client_hash) + +if client is not None: # Check if login was successful + print("Admin logged in successfully") + + 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" + password = "Happymeal1" if i == 0 else generate_hash() + client_id = generate_uuid_short() + client_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() + notes = fake.text(max_nb_chars=50) + + # Add client using add_client function + client_response = add_client( + name=client_name, + birthdate=birthdate, + address=address, + phone_number=phone_number, + email=email, + password=password, + notes=notes + ) + print(client_response[1]) # Print the response message + + for j in range(2): # Each client has 2 accounts + account_id = generate_uuid_short() + balance = 1000 # Initialize balance to 1000 + account_type = random.choice(['Spending', 'Savings']) + account_notes = fake.text(max_nb_chars=50) + + # Add account using add_account function + account_response = add_account( + client_id=client_id, + description=fake.text(max_nb_chars=200), + account_type=account_type, + notes=account_notes + ) + print(account_response[1]) # Print the response message + + 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 + + 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 + continue + + if transaction_type == 'Deposit': # Update balance based on transaction type + balance += amount + elif transaction_type == 'Withdrawal': + balance -= amount + + transaction_description = fake.text(max_nb_chars=50) + recipient_account_id = random.choice(all_account_ids) + + # Add transaction using add_transaction function + transaction_response = add_transaction( + amount=amount, + account_id=account_id, + recipient_account_id=recipient_account_id, + otp_code=123456, # Replace with actual OTP verification code + description=transaction_description + ) + print(transaction_response[1]) # Print the response message + + all_account_ids.append(account_id) + logout() # Log out of the admin account + print(f"The client_id of the administrator account of this test database is: {all_account_ids[0]}. The password is: Happymeal1") +else: + print("Admin login failed.") \ No newline at end of file diff --git a/server/manager.py b/server/manager.py index 3a14107..6791987 100644 --- a/server/manager.py +++ b/server/manager.py @@ -5,13 +5,13 @@ 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 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 flask import jsonify # Imports the Flask modules from database import session import hashlib # For password hashing import datetime # For timestamps diff --git a/server/scheduler.py b/server/scheduler.py index fe5def1..0c99fa6 100644 --- a/server/scheduler.py +++ b/server/scheduler.py @@ -22,7 +22,7 @@ def clean_otp(): event_logger(f"Removed {removed_otps} expired OTPs.") event_logger("Finished cleaning OTPs.") -schedule.every(300).seconds.do(clean_otp) +schedule.every(60).seconds.do(clean_otp) thread = threading.Thread(target=run_schedule) thread.daemon = True # Set the thread as a daemon thread