client side hashing, shared via JSON, password reset working
This commit is contained in:
105
api.yml
105
api.yml
@@ -29,24 +29,46 @@ paths:
|
|||||||
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
|
||||||
parameters:
|
requestBody:
|
||||||
- name: client_id
|
description: Login credentials
|
||||||
in: query
|
required: true
|
||||||
description: client_id
|
content:
|
||||||
required: true
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: object
|
||||||
- name: password
|
properties:
|
||||||
in: query
|
client_id:
|
||||||
description: Password
|
type: string
|
||||||
required: true
|
description: Client ID
|
||||||
schema:
|
client_hash:
|
||||||
type: string
|
type: string
|
||||||
|
description: Hashed password
|
||||||
|
required:
|
||||||
|
- client_id
|
||||||
|
- client_hash
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
'401':
|
'401':
|
||||||
description: Invalid Client ID/password supplied
|
description: Invalid Client ID/password supplied
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
/Client/Logout:
|
/Client/Logout:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -78,39 +100,40 @@ paths:
|
|||||||
summary: Change password
|
summary: Change password
|
||||||
description: Change password
|
description: Change password
|
||||||
operationId: manager.change_password
|
operationId: manager.change_password
|
||||||
parameters:
|
requestBody:
|
||||||
- name: client_id
|
required: true
|
||||||
in: query
|
content:
|
||||||
description: ID of client to change password
|
application/json:
|
||||||
required: true
|
schema:
|
||||||
schema:
|
type: object
|
||||||
type: string
|
properties:
|
||||||
- name: password
|
client_id:
|
||||||
in: query
|
type: string
|
||||||
description: New password
|
description: ID of client to change password
|
||||||
required: true
|
hash_old_password:
|
||||||
schema:
|
type: string
|
||||||
type: string
|
description: Old password as a hash
|
||||||
- name: new_password
|
hash_new_password:
|
||||||
in: query
|
type: string
|
||||||
description: New password
|
description: New password as a hash
|
||||||
required: true
|
otp_code:
|
||||||
schema:
|
type: integer
|
||||||
type: string
|
format: int32
|
||||||
- name: otp
|
description: OTP to verify
|
||||||
in: query
|
required:
|
||||||
description: OTP to verify
|
- client_id
|
||||||
required: true
|
- hash_old_password
|
||||||
schema:
|
- hash_new_password
|
||||||
type: integer
|
- otp_code
|
||||||
format: int32
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Password changed successfully
|
description: Password changed successfully
|
||||||
'400':
|
'400':
|
||||||
description: Old password incorrect
|
description: Validation error
|
||||||
|
'401':
|
||||||
|
description: Unauthorised
|
||||||
'404':
|
'404':
|
||||||
description: client_id not found
|
description: Client not found
|
||||||
/OTP/Generate:
|
/OTP/Generate:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
Binary file not shown.
@@ -12,6 +12,7 @@ from connection import get_transactions, format_balance, get_account, generate_o
|
|||||||
description_entry = None
|
description_entry = None
|
||||||
notes_entry = None
|
notes_entry = None
|
||||||
otp_entry = None
|
otp_entry = None
|
||||||
|
notes_text = None
|
||||||
|
|
||||||
#################
|
#################
|
||||||
### Functions ###
|
### Functions ###
|
||||||
@@ -64,6 +65,8 @@ def display_account_info(account_id):
|
|||||||
label_value = customtkinter.CTkLabel(info_frame, text=value, font=("Helvetica", 14))
|
label_value = customtkinter.CTkLabel(info_frame, text=value, font=("Helvetica", 14))
|
||||||
label_key.grid(row=0, column=i*2, sticky='w', padx=10)
|
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)
|
label_value.grid(row=0, column=i*2+1, sticky='w', padx=10)
|
||||||
|
global notes_text
|
||||||
|
notes_text.configure(text=account.get('notes', '')) # Use config instead of configuration
|
||||||
|
|
||||||
def on_transaction_double_click(event):
|
def on_transaction_double_click(event):
|
||||||
"""Handles double-click event on a transaction in the table."""
|
"""Handles double-click event on a transaction in the table."""
|
||||||
@@ -157,7 +160,7 @@ def save_details():
|
|||||||
root = customtkinter.CTk()
|
root = customtkinter.CTk()
|
||||||
root.title(f"Transactions for: {account_description}")
|
root.title(f"Transactions for: {account_description}")
|
||||||
root.iconbitmap("application/luxbank.ico")
|
root.iconbitmap("application/luxbank.ico")
|
||||||
root.geometry("800x400")
|
root.geometry("800x450")
|
||||||
|
|
||||||
if CONFIG["preferences"]["dark_theme"] == "dark": # Check if dark mode is enabled
|
if CONFIG["preferences"]["dark_theme"] == "dark": # Check if dark mode is enabled
|
||||||
customtkinter.set_appearance_mode("dark") # Set the style for dark mode
|
customtkinter.set_appearance_mode("dark") # Set the style for dark mode
|
||||||
@@ -168,6 +171,12 @@ else:
|
|||||||
welcome_label = customtkinter.CTkLabel(root, text=f"Transactions for: {account_description}", font=("Helvetica", 24))
|
welcome_label = customtkinter.CTkLabel(root, text=f"Transactions for: {account_description}", font=("Helvetica", 24))
|
||||||
welcome_label.pack(pady=10)
|
welcome_label.pack(pady=10)
|
||||||
|
|
||||||
|
# Create the notes label and text box
|
||||||
|
notes_label = customtkinter.CTkLabel(root, text="Notes:", font=("Helvetica", 14))
|
||||||
|
notes_label.pack(pady=10)
|
||||||
|
notes_text = customtkinter.CTkLabel(root, height=4, width=50, wraplength=400) # Use CTkLabel instead of CTkTextbox
|
||||||
|
notes_text.pack(pady=10, fill=tk.BOTH, expand=True) # Add fill and expand options
|
||||||
|
|
||||||
# Display account information
|
# Display account information
|
||||||
info_frame = customtkinter.CTkFrame(root)
|
info_frame = customtkinter.CTkFrame(root)
|
||||||
info_frame.pack(fill=tk.X)
|
info_frame.pack(fill=tk.X)
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ theme = dark-blue
|
|||||||
|
|
||||||
[client]
|
[client]
|
||||||
default_id = d18e5ae0
|
default_id = d18e5ae0
|
||||||
default_password = Happymeal1
|
default_password = KFCKrusher1
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
# Lucas Mathews - Fontys Student ID: 5023572
|
||||||
# Banking System Connection Page
|
# Banking System Connection Page
|
||||||
|
|
||||||
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
import hashlib
|
||||||
from requests.models import Response
|
from requests.models import Response
|
||||||
from config import CONFIG
|
from config import CONFIG
|
||||||
import json
|
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
##############
|
##############
|
||||||
@@ -15,22 +16,25 @@ def format_balance(balance):
|
|||||||
"""Formats the balance as a currency string with comma separators."""
|
"""Formats the balance as a currency string with comma separators."""
|
||||||
return f"€{balance:,.2f}"
|
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 ###
|
### API Functions ###
|
||||||
#####################
|
#####################
|
||||||
|
|
||||||
def authenticate_client(client_id, client_password):
|
def authenticate_client(client_id, client_hash):
|
||||||
"""Authenticates a client with the given client_id and client_password."""
|
"""Authenticates a client with the given client_id and client_hash."""
|
||||||
try:
|
try:
|
||||||
response = requests.post(CONFIG["server"]["url"] + "/Client/Login", params={'client_id': client_id, 'password': client_password})
|
response = requests.post(CONFIG["server"]["url"] + "/Client/Login", json={'client_id': client_id, 'client_hash': client_hash})
|
||||||
|
response.raise_for_status()
|
||||||
|
if response.status_code == 401:
|
||||||
|
return {'success': False, 'message': "Incorrect password."}
|
||||||
return response
|
return response
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"RequestException: {e}")
|
raise e # Re-raise the exception to handle it in the login function
|
||||||
response = Response()
|
|
||||||
response.status_code = 500
|
|
||||||
response._content = b'{"success": false, "message": "Could not connect to the server. Please try again later."}'
|
|
||||||
return response
|
|
||||||
|
|
||||||
def logout_client():
|
def logout_client():
|
||||||
"""Logs out the current client."""
|
"""Logs out the current client."""
|
||||||
try:
|
try:
|
||||||
@@ -175,4 +179,41 @@ def generate_otp():
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"RequestException: {e}")
|
print(f"RequestException: {e}")
|
||||||
messagebox.showerror("Error", f"Could not generate OTP: {e}")
|
messagebox.showerror("Error", f"Could not generate OTP: {e}")
|
||||||
|
|
||||||
|
def change_password(client_id, old_password, new_password, otp_code):
|
||||||
|
"""Changes the password for the given client_id."""
|
||||||
|
hash_old_password = hash_password(old_password)
|
||||||
|
hash_new_password = hash_password(new_password)
|
||||||
|
try:
|
||||||
|
otp_code = int(otp_code) # Ensure otp_code is an integer
|
||||||
|
except ValueError:
|
||||||
|
return {'success': False, 'message': "Invalid OTP code format: must be an integer."}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('application\\session_data.json', 'r') as f:
|
||||||
|
session_data = json.load(f)
|
||||||
|
payload = { # Prepare the payload to be sent in the request body
|
||||||
|
'client_id': client_id,
|
||||||
|
'hash_old_password': hash_old_password,
|
||||||
|
'hash_new_password': hash_new_password,
|
||||||
|
'otp_code': otp_code
|
||||||
|
}
|
||||||
|
response = requests.put( # Send the PUT request with the payload in the body
|
||||||
|
CONFIG["server"]["url"] + "/Client/Password",
|
||||||
|
cookies=session_data['session_cookie'],
|
||||||
|
json=payload # use json to send the data in the request body
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if response.status_code == 400:
|
||||||
|
return {'success': False, 'message': response.json().get('message', 'Invalid request.')}
|
||||||
|
elif response.status_code == 401:
|
||||||
|
return {'success': False, 'message': response.json().get('message', 'Unauthorised action.')}
|
||||||
|
elif response.status_code == 404:
|
||||||
|
return {'success': False, 'message': response.json().get('message', 'Client not found.')}
|
||||||
|
else:
|
||||||
|
return {'success': False, 'message': "An error occurred. Please try again later."}
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return {'success': False, 'message': "Could not connect to the server. Please try again later."}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import customtkinter
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from config import CONFIG
|
from config import CONFIG
|
||||||
from connection import logout_client, get_client, update_client, get_accounts, format_balance, generate_otp
|
from connection import logout_client, get_client, update_client, get_accounts, format_balance, generate_otp, change_password
|
||||||
|
|
||||||
|
|
||||||
# Global variables
|
# Global variables
|
||||||
@@ -112,7 +112,7 @@ def edit_details():
|
|||||||
otp_label.pack()
|
otp_label.pack()
|
||||||
otp_entry.pack()
|
otp_entry.pack()
|
||||||
|
|
||||||
save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=save_details)
|
save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=change_password_save)
|
||||||
save_button.pack()
|
save_button.pack()
|
||||||
edit_window.lift()
|
edit_window.lift()
|
||||||
|
|
||||||
@@ -186,6 +186,82 @@ def reload_info_and_accounts():
|
|||||||
display_client_info()
|
display_client_info()
|
||||||
populate_table()
|
populate_table()
|
||||||
|
|
||||||
|
def change_password_box():
|
||||||
|
"""Opens a new window for changing the client's password."""
|
||||||
|
global edit_window,password_entry, old_password_entry, confirm_password_entry, otp_entry
|
||||||
|
edit_window = customtkinter.CTkToplevel(root)
|
||||||
|
edit_window.title("Change Password")
|
||||||
|
edit_window.iconbitmap("application/luxbank.ico")
|
||||||
|
edit_window.geometry("300x350")
|
||||||
|
edit_window.attributes('-topmost', True)
|
||||||
|
|
||||||
|
old_password_label = customtkinter.CTkLabel(edit_window, text="Old Password: ")
|
||||||
|
old_password_entry = customtkinter.CTkEntry(edit_window, show="*")
|
||||||
|
old_password_label.pack()
|
||||||
|
old_password_entry.pack()
|
||||||
|
|
||||||
|
customtkinter.CTkLabel(edit_window, text=" ").pack() # Add space under the old password box
|
||||||
|
|
||||||
|
password_label = customtkinter.CTkLabel(edit_window, text="New Password: ")
|
||||||
|
password_entry = customtkinter.CTkEntry(edit_window, show="*")
|
||||||
|
password_label.pack()
|
||||||
|
password_entry.pack()
|
||||||
|
|
||||||
|
confirm_password_label = customtkinter.CTkLabel(edit_window, text="Confirm Password: ")
|
||||||
|
confirm_password_entry = customtkinter.CTkEntry(edit_window, show="*")
|
||||||
|
confirm_password_label.pack()
|
||||||
|
confirm_password_entry.pack()
|
||||||
|
|
||||||
|
customtkinter.CTkLabel(edit_window, text=" ").pack() # Add space under the confirm password box
|
||||||
|
|
||||||
|
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)
|
||||||
|
otp_label.pack()
|
||||||
|
otp_entry.pack()
|
||||||
|
|
||||||
|
save_button = customtkinter.CTkButton(edit_window, text="Verify OTP and Save", command=change_password_save)
|
||||||
|
save_button.pack()
|
||||||
|
edit_window.lift()
|
||||||
|
|
||||||
|
def change_password_save():
|
||||||
|
"""Saves the updated client password."""
|
||||||
|
global edit_window, otp_entry, password_entry, old_password_entry, confirm_password_entry
|
||||||
|
old_password = old_password_entry.get()
|
||||||
|
new_password = password_entry.get()
|
||||||
|
confirm_password = confirm_password_entry.get()
|
||||||
|
otp_code = otp_entry.get()
|
||||||
|
|
||||||
|
if not otp_code:
|
||||||
|
messagebox.showerror("Error", "OTP code must be entered.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not new_password or not confirm_password:
|
||||||
|
messagebox.showerror("Error", "New password and confirm password must be entered.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
messagebox.showerror("Error", "New password and confirm password do not match.")
|
||||||
|
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 change the password?"):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = change_password(client_id, old_password, new_password, otp_code)
|
||||||
|
if response['success']:
|
||||||
|
messagebox.showinfo("Success", "Password changed successfully.")
|
||||||
|
edit_window.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Error", f"Could not change password: {response['message']}")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Error", f"Could not change password: {str(e)}")
|
||||||
|
|
||||||
##############
|
##############
|
||||||
### Layout ###
|
### Layout ###
|
||||||
@@ -215,13 +291,17 @@ otp_button.grid(row=0, column=0, padx=5)
|
|||||||
reload_button = customtkinter.CTkButton(button_frame, text="Reload", command=reload_info_and_accounts)
|
reload_button = customtkinter.CTkButton(button_frame, text="Reload", command=reload_info_and_accounts)
|
||||||
reload_button.grid(row=0, column=1, padx=5)
|
reload_button.grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
# Create reset password button
|
||||||
|
password_button = customtkinter.CTkButton(button_frame, text="Reset Password", command=change_password_box)
|
||||||
|
password_button.grid(row=0, column=2, padx=5)
|
||||||
|
|
||||||
# Create the logout button
|
# Create the logout button
|
||||||
logout_button = customtkinter.CTkButton(button_frame, text="Logout", command=logout)
|
logout_button = customtkinter.CTkButton(button_frame, text="Logout", command=logout)
|
||||||
logout_button.grid(row=0, column=2, padx=5)
|
logout_button.grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
# Create the exit button
|
# Create the exit button
|
||||||
exit_button = customtkinter.CTkButton(button_frame, text="Exit", command=exit_application)
|
exit_button = customtkinter.CTkButton(button_frame, text="Exit", command=exit_application)
|
||||||
exit_button.grid(row=0, column=3, padx=5)
|
exit_button.grid(row=0, column=4, padx=5)
|
||||||
|
|
||||||
# Display client info after creating the buttons
|
# Display client info after creating the buttons
|
||||||
frame = customtkinter.CTkFrame(root)
|
frame = customtkinter.CTkFrame(root)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import customtkinter
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from connection import authenticate_client
|
from connection import authenticate_client, hash_password
|
||||||
from config import CONFIG
|
from config import CONFIG
|
||||||
import configparser, sys
|
import configparser, sys
|
||||||
|
|
||||||
@@ -17,10 +17,11 @@ def login():
|
|||||||
"""Authenticate the client and open the dashboard if successful."""
|
"""Authenticate the client and open the dashboard if successful."""
|
||||||
client_id = entry_username.get() if entry_username.get() else CONFIG["client"]["default_id"]
|
client_id = entry_username.get() if entry_username.get() else CONFIG["client"]["default_id"]
|
||||||
client_password = entry_password.get() if entry_password.get() else CONFIG["client"]["default_password"]
|
client_password = entry_password.get() if entry_password.get() else CONFIG["client"]["default_password"]
|
||||||
|
client_hash = hash_password(client_password) # Hash the password on the client-side
|
||||||
try:
|
try:
|
||||||
response = authenticate_client(client_id, client_password) # Authenticate the client
|
response = authenticate_client(client_id, client_hash) # Authenticate the client
|
||||||
json_response = response.json() # Convert the response content to JSON
|
json_response = response.json() # Convert the response content to JSON
|
||||||
if json_response["success"]: # If the authentication is successful, open the dashboard
|
if response.status_code == 200 and json_response.get("success"): # If the authentication is successful, open the dashboard
|
||||||
session_data = {
|
session_data = {
|
||||||
'session_cookie': response.cookies.get_dict(),
|
'session_cookie': response.cookies.get_dict(),
|
||||||
'client_id': client_id
|
'client_id': client_id
|
||||||
@@ -29,10 +30,14 @@ def login():
|
|||||||
json.dump(session_data, f)
|
json.dump(session_data, f)
|
||||||
root.destroy()
|
root.destroy()
|
||||||
os.system("python application/dashboard.py")
|
os.system("python application/dashboard.py")
|
||||||
|
elif response.status_code == 401:
|
||||||
|
messagebox.showerror("Login failed", "Invalid client ID or password.")
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("Login failed", json_response["message"])
|
messagebox.showerror("Login failed", json_response.get("message", "Unknown error"))
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.HTTPError:
|
||||||
messagebox.showerror("Login failed", f"Could not connect to the server. Please try again later. Error: {str(e)}")
|
messagebox.showerror("Login failed", "Invalid client ID or password.")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
messagebox.showerror("Connection Error", "Could not connect to the server.")
|
||||||
|
|
||||||
def change_dark_theme():
|
def change_dark_theme():
|
||||||
"""Change the theme between dark and light."""
|
"""Change the theme between dark and light."""
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"session_cookie": {"session": "nwHUYOr9vg2nOaZmrYNmWgjMgJ47QLIz71_dX_kFH_o"}, "client_id": "d18e5ae0"}
|
{"session_cookie": {"session": "HHymBLOCpW9YTcajilxYA_B9aVPJ1FyS75TAz2995jk"}, "client_id": "d18e5ae0"}
|
||||||
@@ -14,8 +14,6 @@ import sys
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##############
|
##############
|
||||||
### Layout ###
|
### Layout ###
|
||||||
##############
|
##############
|
||||||
|
|||||||
107
manager.py
107
manager.py
@@ -6,16 +6,18 @@ from class_account import Account
|
|||||||
from class_transaction import Transaction
|
from class_transaction import Transaction
|
||||||
from emailer import EmailSendingError # Import the EmailSendingError class to handle email sending errors
|
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 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 flask import session as flask_session
|
||||||
|
from flask import request
|
||||||
|
from database import session
|
||||||
import hashlib # For password hashing
|
import hashlib # For password hashing
|
||||||
import datetime # For timestamps
|
import datetime # For timestamps
|
||||||
import uuid # For unique identifiers
|
import uuid # For unique identifiers
|
||||||
import random # For OTP generation
|
import random # For OTP generation
|
||||||
import time # For OTP generation
|
import time # For OTP generation
|
||||||
from functools import wraps # For decorators / user login
|
import re # For password hash validation
|
||||||
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
|
otps = {} # Temporary dictionary to store OTPs and their creation time
|
||||||
|
|
||||||
@@ -41,10 +43,8 @@ def generate_uuid_short():
|
|||||||
|
|
||||||
def get_email(client_id:str):
|
def get_email(client_id:str):
|
||||||
"""Returns the email of a client given their client_id. If the client is not found, returns None."""
|
"""Returns the email of a client given their client_id. If the client is not found, returns None."""
|
||||||
for client in session.query(Client).all():
|
client = session.query(Client).filter_by(client_id=client_id).one_or_none()
|
||||||
if client.client_id == client_id:
|
return client.email if client else None
|
||||||
return client.email
|
|
||||||
return None
|
|
||||||
|
|
||||||
def format_response(success: bool, message: str = '', data: dict = None):
|
def format_response(success: bool, message: str = '', data: dict = None):
|
||||||
"""Formats the response for the API so that it is standardised across all functions."""
|
"""Formats the response for the API so that it is standardised across all functions."""
|
||||||
@@ -68,10 +68,12 @@ def get_current_client():
|
|||||||
|
|
||||||
def verify_otp(client_id:str, otp:int):
|
def verify_otp(client_id:str, otp:int):
|
||||||
"""Verifies a one time password for a client. Returns True if the OTP is correct and False otherwise."""
|
"""Verifies a one time password for a client. Returns True if the OTP is correct and False otherwise."""
|
||||||
if client_id in otps and otps[client_id][0] == otp:
|
if client_id in otps:
|
||||||
return True
|
stored_otp, creation_time = otps[client_id]
|
||||||
|
if stored_otp == otp and time.time() - creation_time <= 300: # Check if OTP is within 5 minutes
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_otp(client_id:str):
|
def delete_otp(client_id:str):
|
||||||
"""Deletes the OTP for a client."""
|
"""Deletes the OTP for a client."""
|
||||||
if client_id in otps:
|
if client_id in otps:
|
||||||
@@ -88,14 +90,18 @@ def check_expired_otps():
|
|||||||
### Authentication ###
|
### Authentication ###
|
||||||
######################
|
######################
|
||||||
|
|
||||||
def login(client_id:str, password:str):
|
def login():
|
||||||
"""Logs in a client using their client_id and password. Returns a success message if the login is successful and an error message otherwise."""
|
"""Logs in a client using their client_id and password hash. Returns a success message if the login is successful and an error message otherwise."""
|
||||||
password_hash = hash_password(password)
|
data = request.get_json()
|
||||||
for client in session.query(Client).all():
|
client_id = data.get('client_id')
|
||||||
if client.client_id == client_id and client.hash == password_hash:
|
client_hash = data.get('client_hash')
|
||||||
flask_session['client_id'] = client_id
|
|
||||||
return format_response(True, f"{flask_session['client_id']} logged in succsessfully."), 200
|
client = session.query(Client).filter_by(client_id=client_id).first()
|
||||||
return format_response(False, "Invalid client_id or password."), 400
|
if client and client.hash == client_hash:
|
||||||
|
flask_session['client_id'] = client_id
|
||||||
|
return format_response(True, f"{flask_session['client_id']} logged in successfully."), 200
|
||||||
|
return format_response(False, "Invalid client_id or password."), 401
|
||||||
|
|
||||||
|
|
||||||
def logout():
|
def logout():
|
||||||
"""Logs out a client. Returns a success message if the logout is successful and an error message otherwise."""
|
"""Logs out a client. Returns a success message if the logout is successful and an error message otherwise."""
|
||||||
@@ -162,14 +168,16 @@ def generate_otp(client_id: str):
|
|||||||
##############
|
##############
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def get_client(client_id:str):
|
def get_client(client_id: str):
|
||||||
"""Returns a specific client in the database. If the client is not found, returns an error message."""
|
"""Returns a specific client in the database. If the client is not found, returns an error message."""
|
||||||
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 format_response(False, "You can only view your own client information."), 403
|
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:
|
client = session.query(Client).filter_by(client_id=client_id).one_or_none()
|
||||||
return format_response(True, "", client.to_dict()), 200
|
if client:
|
||||||
|
return format_response(True, "", client.to_dict()), 200
|
||||||
|
|
||||||
return format_response(False, "Client not found."), 404
|
return format_response(False, "Client not found."), 404
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -192,25 +200,44 @@ def update_client(client_id:str, otp_code:int, **kwargs):
|
|||||||
return format_response(False, "Client not found."), 404
|
return format_response(False, "Client not found."), 404
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
def change_password():
|
||||||
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."""
|
"""Changes the password for a client in the database. If the client is not found, returns an error message."""
|
||||||
|
data = request.get_json()
|
||||||
|
client_id = data.get('client_id')
|
||||||
|
hash_old_password = data.get('hash_old_password')
|
||||||
|
hash_new_password = data.get('hash_new_password')
|
||||||
|
otp_code = data.get('otp_code')
|
||||||
current_client_id, is_admin = get_current_client()
|
current_client_id, is_admin = get_current_client()
|
||||||
|
|
||||||
|
# Verify if the OTP is correct
|
||||||
|
otp_verified = verify_otp(client_id, otp_code)
|
||||||
|
|
||||||
|
# Check if the client is authorized to change the password
|
||||||
if not is_admin and client_id != current_client_id:
|
if not is_admin and client_id != current_client_id:
|
||||||
return format_response(False, "You can only update your own client information."), 403
|
return format_response(False, "You can only update your own client information."), 401
|
||||||
if not verify_otp(client_id, otp):
|
|
||||||
return format_response(False, "Invalid OTP."), 400
|
# Recheck OTP verification after authorisation check
|
||||||
old_hash = hash_password(password)
|
if not otp_verified:
|
||||||
new_hash = hash_password(new_password)
|
return format_response(False, "Invalid OTP."), 400
|
||||||
for client in session.query(Client).all():
|
|
||||||
if client.client_id == client_id:
|
# Validate new password format
|
||||||
if client.hash == old_hash:
|
hash_format = r'^[0-9a-f]{128}$'
|
||||||
client.hash = new_hash
|
if not re.match(hash_format, hash_new_password):
|
||||||
session.commit()
|
return format_response(False, "Invalid new password format (must be provided as a hash)."), 400
|
||||||
delete_otp(client_id)
|
|
||||||
return format_response(True, f"Password for client_id: {client_id} has been updated."), 200
|
# Check if the old password hash matches and update to the new password hash
|
||||||
return format_response(False, "Invalid password."), 400
|
client = session.query(Client).filter_by(client_id=client_id).first()
|
||||||
return format_response(False, "Client not found."), 404
|
if client:
|
||||||
|
if client.hash == hash_old_password:
|
||||||
|
client.hash = hash_new_password
|
||||||
|
session.commit()
|
||||||
|
delete_otp(client_id)
|
||||||
|
return format_response(True, f"Password for client_id: {client_id} has been updated."), 200
|
||||||
|
else:
|
||||||
|
return format_response(False, "Invalid old password."), 400
|
||||||
|
else:
|
||||||
|
return format_response(False, "Client not found."), 404
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def get_accounts(client_id: str):
|
def get_accounts(client_id: str):
|
||||||
|
|||||||
BIN
test_database.db
BIN
test_database.db
Binary file not shown.
Reference in New Issue
Block a user