implement OTP

This commit is contained in:
Lucas Mathews
2024-05-25 12:32:43 +02:00
parent 93c3c6f59b
commit a9278514aa
3 changed files with 110 additions and 21 deletions

51
api.yml
View File

@@ -15,8 +15,8 @@ tags:
description: Operations for Bank Accounts description: Operations for Bank Accounts
- name: transaction - name: transaction
description: Operations for Transactions description: Operations for Transactions
- name: login - name: auth
description: Operations for Logging in description: Operations for Authentication
- name: system - name: system
description: Operations for System description: Operations for System
- name: admin - name: admin
@@ -25,7 +25,7 @@ paths:
/Client/Login: /Client/Login:
post: post:
tags: tags:
- login - auth
summary: Log in to the system summary: Log in to the system
description: Log in to the system description: Log in to the system
operationId: manager.login operationId: manager.login
@@ -50,7 +50,7 @@ paths:
/Client/Logout: /Client/Logout:
post: post:
tags: tags:
- login - auth
summary: Log out from the system summary: Log out from the system
description: Log out from the system description: Log out from the system
operationId: manager.logout operationId: manager.logout
@@ -62,7 +62,7 @@ paths:
/Client/Status: /Client/Status:
get: get:
tags: tags:
- login - auth
summary: Get login status summary: Get login status
description: Get login status description: Get login status
operationId: manager.status operationId: manager.status
@@ -74,7 +74,7 @@ paths:
/Client/Password: /Client/Password:
put: put:
tags: tags:
- client - auth
summary: Change password summary: Change password
description: Change password description: Change password
operationId: manager.change_password operationId: manager.change_password
@@ -84,8 +84,7 @@ paths:
description: ID of client to change password description: ID of client to change password
required: true required: true
schema: schema:
type: integer type: string
format: int32
- name: password - name: password
in: query in: query
description: New password description: New password
@@ -98,6 +97,13 @@ paths:
required: true required: true
schema: schema:
type: string type: string
- name: otp
in: query
description: OTP to verify
required: true
schema:
type: integer
format: int32
responses: responses:
'200': '200':
description: Password changed successfully description: Password changed successfully
@@ -105,6 +111,29 @@ paths:
description: Old password incorrect description: Old password incorrect
'404': '404':
description: client_id not found description: client_id not found
/OTP/Generate:
get:
tags:
- auth
summary: Generate OTP
description: Generate OTP
operationId: manager.generate_otp
parameters:
- name: client_id
in: query
description: ID of client to generate OTP
required: true
schema:
type: string
responses:
'200':
description: OTP generated
'401':
description: Unauthorised
'400':
description: OTP not valid
'404':
description: client_id not found
/Client/Client: /Client/Client:
put: put:
tags: tags:
@@ -551,6 +580,12 @@ paths:
required: true required: true
schema: schema:
type: string type: string
- name: email
in: query
description: Email to initialise the system
required: true
schema:
type: string
responses: responses:
'200': '200':
description: Successful operation description: Successful operation

BIN
bank.db

Binary file not shown.

View File

@@ -5,11 +5,14 @@ from class_client import Client
from class_account import Account from class_account import Account
from class_transaction import Transaction from class_transaction import Transaction
from flask import jsonify, session as flask_session # Imports the Flask modules from flask import jsonify, session as flask_session # Imports the Flask modules
import hashlib # hashlib for password hashing import hashlib # For password hashing
import datetime # datetime for timestamps import datetime # For timestamps
import uuid # uuid for unique identifiers import uuid # For unique identifiers
from functools import wraps # functools for decorators / user login import random # For OTP generation
import time # For OTP generation
from functools import wraps # For decorators / user login
from database import * # Importing the database connection from database import * # Importing the database connection
from emailer import send_email # Importing the emailer function
############## ##############
### System ### ### System ###
@@ -27,9 +30,15 @@ def generate_uuid(): # Generates a unique identifier for transactions
def generate_uuid_short(): # Generates a short uuid def generate_uuid_short(): # Generates a short uuid
return str(uuid.uuid4())[:8] return str(uuid.uuid4())[:8]
############# def get_email(client_id:str):
### Login ### for client in session.query(Client).all():
############# if client.client_id == client_id:
return client.email
return None
######################
### Authentication ###
######################
def login(client_id:str, password:str): # Logs in a user def login(client_id:str, password:str): # Logs in a user
password_hash = hash_password(password) password_hash = hash_password(password)
@@ -76,6 +85,35 @@ def get_current_client():
is_admin = session.query(Client).filter_by(client_id=client).one_or_none().administrator is_admin = session.query(Client).filter_by(client_id=client).one_or_none().administrator
return client, is_admin return client, is_admin
otps = {} # Dictionary to store OTPs and their creation time
@login_required
def generate_otp(client_id:str): # Generates a one time password for 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 view your own client information."}), 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 jsonify({"error": "Client not found"}), 404
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:
return True
return False
def delete_otp(client_id:str): # Deletes a one time password for a client
if client_id in otps:
del otps[client_id]
def check_expired_otps(): # Checks and deletes expired OTPs
current_time = time.time()
expired_otps = [client_id for client_id, (otp, creation_time) in otps.items() if current_time - creation_time > 300] # Find OTPs older than 5 minutes
for client_id in expired_otps:
delete_otp(client_id)
############## ##############
### Client ### ### Client ###
@@ -121,10 +159,12 @@ def update_client(client_id:str, **kwargs): # Updates a client in the database
return f"Client ID: {client_id} is not found." , 400 return f"Client ID: {client_id} is not found." , 400
@login_required @login_required
def change_password(client_id:str, password:str, new_password:str): # Changes the password of a client def change_password(client_id:str, password:str, new_password:str, otp:int): # Changes the password of a client
current_client_id, is_admin = get_current_client() current_client_id, is_admin = get_current_client()
if not is_admin and client_id != current_client_id: if not is_admin and client_id != current_client_id:
return jsonify({"error": "You can only update your own password."}), 403 return jsonify({"error": "You can only update your own password."}), 403
if not verify_otp(client_id, otp):
return jsonify({"error": "Invalid OTP"}), 400
old_hash = hash_password(password) old_hash = hash_password(password)
new_hash = hash_password(new_password) new_hash = hash_password(new_password)
for client in session.query(Client).all(): for client in session.query(Client).all():
@@ -132,6 +172,7 @@ def change_password(client_id:str, password:str, new_password:str): # Changes th
if client.hash == old_hash: if client.hash == old_hash:
client.hash = new_hash client.hash = new_hash
session.commit() session.commit()
delete_otp(client_id)
return "Password changed successfully.", 200 return "Password changed successfully.", 200
return "Incorrect old password.", 400 return "Incorrect old password.", 400
return f"client_id: {client_id} is not found.", 404 return f"client_id: {client_id} is not found.", 404
@@ -321,8 +362,21 @@ def apply_fee(account_id:int, fee:float):
@admin_required @admin_required
def delete_transaction(transaction_id:int): def delete_transaction(transaction_id:int):
DELETE_TRANSACTION = "DELETE FROM transaction WHERE transaction_id=?" for transaction in session.query(Transaction).all():
return if transaction.transaction_id == transaction_id:
session.delete(transaction)
session.commit()
return f"Transaction ID: {transaction_id} has been removed.", 400
return f"Transaction ID: {transaction_id} is not found.", 404
@admin_required
def modify_balance(transaction_id:int, amount:int):
for transaction in session.query(Transaction).all():
if transaction.transaction_id == transaction_id:
transaction.amount = amount
session.commit()
return f"Transaction ID: {transaction_id} has been modified.", 200
return f"Transaction ID: {transaction_id} is not found.", 404
@admin_required @admin_required
def test_account_balances(): def test_account_balances():
@@ -369,10 +423,10 @@ def add_client(name:str, birthdate:str, address:str, phone_number:str, email:str
session.commit() session.commit()
return client_id, 200 return client_id, 200
def initialise_database(password:str): def initialise_database(password:str, email:str):
existing_clients = session.query(Client).all() # Check if any clients exist in the database existing_clients = session.query(Client).all() # Check if any clients exist in the database
if not existing_clients: # If no clients exist, create an administrator client if not existing_clients: # If no clients exist, create an administrator client
add_client('ADMINISTRATOR', 'ADMINISTRATOR', 'ADMINISTRATOR', 'ADMINISTRATOR', 'ADMINISTRATOR', password) # Add the administrator client add_client('ADMINISTRATOR', 'ADMINISTRATOR', 'ADMINISTRATOR', 'ADMINISTRATOR', email, password) # Add the administrator client
session.commit() session.commit()
admin_client = session.query(Client).filter_by(name='ADMINISTRATOR').one() # Retrieve the administrator client admin_client = session.query(Client).filter_by(name='ADMINISTRATOR').one() # Retrieve the administrator client
admin_client.administrator = 1 # Set the new client as an administrator admin_client.administrator = 1 # Set the new client as an administrator