diff --git a/manager.py b/manager.py index 4f16143..75a90a0 100644 --- a/manager.py +++ b/manager.py @@ -4,13 +4,11 @@ from class_client import Client from class_account import Account from class_transaction import Transaction -from flask import Flask, jsonify, session as flask_session, request # Imports the Flask modules +from flask import jsonify, session as flask_session # Imports the Flask modules import hashlib # hashlib for password hashing import datetime # datetime for timestamps import uuid # uuid for unique identifiers from functools import wraps # functools for decorators / user login - - from database import * # Importing the database connection ############## @@ -70,16 +68,24 @@ def admin_required(f): if client.client_id == flask_session['client_id']: if client.administrator == 1: return f(*args, **kwargs) - return jsonify({"error": "Not authorized"}), 403 + return jsonify({"error": "Not authorised"}), 403 return decorated_function +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 ### ############## @login_required def get_client(client_id:str): # Returns a specific client in the database - client = session.query(Client).filter_by(client_id=client_id).one_or_none() + current_client_id, is_admin = get_current_client() + if not is_admin and client_id != current_client_id: + return jsonify({"error": "You can only view your own client information."}), 403 for client in session.query(Client).all(): if client.client_id == client_id: return jsonify({"name": client.name, "birthdate": client.birthdate, "opening_timestamp": client.opening_timestamp, "address": client.address, "phone_number": client.phone_number, "email": client.email}), 200 @@ -94,20 +100,11 @@ def add_client(name:str, birthdate:str, address:str, phone_number:str, email:str session.commit() return f"New client has been added: name: {name}, uuid: {client_id} ", 200 -@login_required -def delete_client(client_id:str): # Deletes a client from the database - for client in session.query(Client).all(): - if client.client_id == client_id: - if client.accounts == None: - session.delete(client) - session.commit() - return f"client_id: {client_id} has been removed.", 200 - else: - return f"client_id: {client_id} has active accounts and can not be removed.", 400 - return f"client_id: {client_id} is not found.", 404 - @login_required def update_client(client_id:str, **kwargs): # Updates a client in the database + current_client_id, is_admin = get_current_client() + if not is_admin and client_id != current_client_id: + return jsonify({"error": "You can only update your own client information."}), 403 for client in session.query(Client).all(): if client.client_id == client_id: name = kwargs.get("name", None) @@ -134,6 +131,9 @@ def update_client(client_id:str, **kwargs): # Updates a client in the database @login_required def change_password(client_id:str, password:str, new_password:str): # Changes the password of a client + current_client_id, is_admin = get_current_client() + if not is_admin and client_id != current_client_id: + return jsonify({"error": "You can only update your own password."}), 403 old_hash = hash_password(password) new_hash = hash_password(new_password) for client in session.query(Client).all(): @@ -151,6 +151,10 @@ def change_password(client_id:str, password:str, new_password:str): # Changes th @login_required def get_account(account_id:str): # Returns a specific account in the database + current_client_id, is_admin = get_current_client() + account_owner = session.query(Account).filter_by(account_id=account_id).one_or_none().client_id + if not is_admin and account_owner != current_client_id: + return jsonify({"error": "You can only view your own account information."}), 403 account = session.query(Account).filter_by(account_id=account_id).one_or_none() for account in session.query(Account).all(): if account.account_id == account_id: @@ -159,6 +163,9 @@ def get_account(account_id:str): # Returns a specific account in the database @login_required def add_account(client_id:str, description:str, account_type:str, **kwargs): # Adds a new account to the database + current_client_id, is_admin = get_current_client() + if not is_admin and client_id != current_client_id: + return jsonify({"error": "You can only add accounts your own client account."}), 403 account_id = generate_uuid_short() notes = kwargs.get("notes", None) client_found = None @@ -176,20 +183,12 @@ def add_account(client_id:str, description:str, account_type:str, **kwargs): # A session.commit() return f"New account has been added: description: {description}, uuid: {account_id} ", 200 -@login_required -def delete_account(account_id:str): # Deletes an account from the database - for account in session.query(Account).all(): - if account.account_id == account_id: - if account.balance == 0: - session.delete(account) - session.commit() - return f"account_id: {account_id} has been removed.", 200 - else: - return f"account_id: {account_id} has a balance and can not be removed.", 400 - return f"account_id: {account_id} is not found.", 404 - @login_required def update_account(account_id:str, **kwargs): # Updates an account in the database + current_client_id, is_admin = get_current_client() + account_owner = session.query(Account).filter_by(account_id=account_id).one_or_none().client_id + if not is_admin and account_owner != current_client_id: + return jsonify({"error": "You can only view your own account information."}), 403 for account in session.query(Account).all(): if account.account_id == account_id: description = kwargs.get("description", None) @@ -217,14 +216,21 @@ def update_account(account_id:str, **kwargs): # Updates an account in the databa @login_required def get_transaction(transaction_id:int): # Returns a specific transaction in the database + current_client_id, is_admin = get_current_client() transaction = session.query(Transaction).filter_by(transaction_id=transaction_id).one_or_none() - for transaction in session.query(Transaction).all(): - if transaction.transaction_id == transaction_id: - return jsonify({"transaction_type": transaction.transaction_type, "amount": transaction.amount, "timestamp": transaction.timestamp, "description": transaction.description, "account_id": transaction.account_id, "recipient_account_id": transaction.recipient_account_id}), 200 - return jsonify({"error": "Transaction not found"}), 404 + if not transaction: + return jsonify({"error": "Transaction not found"}), 404 + account = session.query(Account).filter_by(account_id=transaction.account_id).one_or_none() + recipient_account = session.query(Account).filter_by(account_id=transaction.recipient_account_id).one_or_none() + if not is_admin and (account.client_id != current_client_id and recipient_account.client_id != current_client_id): + return jsonify({"error": "You can only view your own transaction information."}), 403 + return jsonify({"transaction_type": transaction.transaction_type, "amount": transaction.amount, "timestamp": transaction.timestamp, "description": transaction.description, "account_id": transaction.account_id, "recipient_account_id": transaction.recipient_account_id}), 200 @login_required def add_transaction(amount:int, account_id, recipient_account_id, **kwargs): # Adds a new transaction to the database + current_client_id, is_admin = get_current_client() + if not is_admin and account_id != current_client_id: + return jsonify({"error": "You can only add transactions to your own account."}), 403 transaction_id = generate_uuid() for account in session.query(Account).all(): if account.account_id == account_id: @@ -248,6 +254,12 @@ def add_transaction(amount:int, account_id, recipient_account_id, **kwargs): # A @login_required def transaction_history(account_id:int): # Returns all transactions for a specific account + current_client_id, is_admin = get_current_client() + account = session.query(Account).filter_by(account_id=account_id).one_or_none() + if not account: + return jsonify({"error": "Account not found."}), 404 + if not is_admin and account.client_id != current_client_id: + return jsonify({"error": "You can only view your own transaction history."}), 403 result = session.query(Transaction).filter(Transaction.account_id == account_id) return jsonify([{"transaction_id": transaction.transaction_id, "transaction_type": transaction.transaction_type, "amount": transaction.amount, "timestamp": transaction.timestamp, "description": transaction.description, "account_number": transaction.account_id, "recipient_account_number": transaction.recipient_account_id} for transaction in result]), 200 @@ -255,6 +267,33 @@ def transaction_history(account_id:int): # Returns all transactions for a specif ### Administrator ### ##################### +@admin_required +def delete_client(client_id:str): # Deletes a client from the database + if client_id == flask_session['client_id']: + return "You can not delete yourself.", 400 + + for client in session.query(Client).all(): + if client.client_id == client_id: + if client.accounts == None: + session.delete(client) + session.commit() + return f"client_id: {client_id} has been removed.", 200 + else: + return f"client_id: {client_id} has active accounts and can not be removed.", 400 + return f"client_id: {client_id} is not found.", 404 + +@admin_required +def delete_account(account_id:str): # Deletes an account from the database + for account in session.query(Account).all(): + if account.account_id == account_id: + if account.balance == 0: + session.delete(account) + session.commit() + return f"account_id: {account_id} has been removed.", 200 + else: + return f"account_id: {account_id} has a balance and can not be removed.", 400 + return f"account_id: {account_id} is not found.", 404 + @admin_required def get_all_clients(): # Returns all clients in the database clients = session.query(Client).all() @@ -304,5 +343,21 @@ def delete_transaction(transaction_id:int): return return f"Transaction ID: {transaction_id} is not found." +@admin_required +def test_account_balances(): + # Get all accounts + all_accounts = get_all_accounts() + # Go through each account + for account in all_accounts: + # Calculate the balance based on the transactions + calculated_balance = 0 + for transaction in account.get_transactions(): + if transaction.transaction_type == 'Deposit': + calculated_balance += transaction.amount + elif transaction.transaction_type == 'Withdrawal': + calculated_balance -= transaction.amount + # Check if the calculated balance matches the stored balance + if calculated_balance != account.balance: + print(f"Alert: Account {account.account_id} has a balance discrepancy. Stored balance is {account.balance}, but calculated balance is {calculated_balance}.") diff --git a/requirements.txt b/requirements.txt index 8113c4a..415e17a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ flask connexion[swagger-ui]==2.14.2 requests sqlalchemy -flask-session \ No newline at end of file +flask-session +faker \ No newline at end of file diff --git a/test_database.db b/test_database.db new file mode 100644 index 0000000..73917f3 Binary files /dev/null and b/test_database.db differ diff --git a/test_database_generator.py b/test_database_generator.py new file mode 100644 index 0000000..09c4fa1 --- /dev/null +++ b/test_database_generator.py @@ -0,0 +1,91 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from class_base import Base +import hashlib # hashlib for password hashing +import uuid # uuid for unique identifiers +from class_account import Account +from class_client import Client +from class_transaction import Transaction +from faker import Faker +import random + +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 + return str(uuid.uuid4())[:8] + +# Create a new engine for the test database +engine = create_engine('sqlite:///test_database.db') + +# Create all tables in the test database +Base.metadata.create_all(engine) + +# Create a new sessionmaker bound to the test engine +Session = sessionmaker(bind=engine) + +# Create a new session +session = Session() + +# Create a Faker instance +fake = Faker() + +# List to store all account IDs +all_account_ids = [] + +# Generate 50 clients +for i in range(50): + client_id = generate_uuid_short() + client = Client( + client_id=client_id, + name=fake.name(), + birthdate=fake.date_of_birth(minimum_age=18, maximum_age=90), + opening_timestamp=fake.date_this_century(), + address=fake.address(), + phone_number=fake.phone_number(), + email=fake.email(), + administrator=random.choice([0, 1]), + hash=generate_hash(), # Replace with appropriate value + notes=fake.text(max_nb_chars=50), # Generate fake notes + enabled=1, + accounts=[]) # Empty list for accounts, you can add accounts later) + session.add(client) + + # Each client has 2 accounts + for j in range(2): + account_id = generate_uuid_short() + account = Account( + account_id=account_id, + client_id=client_id, + description=fake.text(max_nb_chars=200), + open_timestamp=fake.date_this_century(), + account_type=random.choice(['Spending', 'Savings']), + balance=random.randint(100, 10000), + enabled=1, + notes=fake.text(max_nb_chars=50), + transactions=[]) + session.add(account) + all_account_ids.append(account_id) + + # Each account has 40 transactions + for k in range(40): + transaction = Transaction( + transaction_id=generate_uuid(), # Call the function here + account_id=account_id, + recipient_account_id=random.choice(all_account_ids), + transaction_type=random.choice(['Deposit', 'Withdrawal']), + amount=random.randint(1, 20), + timestamp=fake.date_this_year(), + description=fake.text(max_nb_chars=50) + ) + session.add(transaction) + +# Commit the session to write the test data to the database +session.commit() + +# Close the session +session.close() \ No newline at end of file