Start migration to postgresql and other minor fixes and improvements
This commit is contained in:
@@ -5,12 +5,12 @@ FROM python:3.12.3
|
|||||||
|
|
||||||
LABEL maintainer="522499@student.fontys.nl"
|
LABEL maintainer="522499@student.fontys.nl"
|
||||||
|
|
||||||
WORKDIR /banking-system
|
WORKDIR /server
|
||||||
|
|
||||||
COPY / /banking-system/
|
COPY . /server
|
||||||
|
|
||||||
EXPOSE 81
|
EXPOSE 81
|
||||||
|
|
||||||
RUN pip install --no-cache-dir --upgrade -r /banking-system/requirements.txt
|
RUN pip install --no-cache-dir --upgrade -r /requirements.txt
|
||||||
|
|
||||||
ENTRYPOINT ["python", "./api.py", "--host", "0.0.0.0", "--port", "81"]
|
ENTRYPOINT ["python", "./api.py", "--host", "0.0.0.0", "--port", "81"]
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# Lucas Mathews - Fontys Student ID: 5023572
|
||||||
|
# Banking System Login Page
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
import customtkinter
|
import customtkinter
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
|
||||||
# Banking System Base Class
|
|
||||||
|
|
||||||
from sqlalchemy.orm import declarative_base
|
|
||||||
|
|
||||||
Base = declarative_base()
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
|
||||||
# Banking System Config Parser
|
|
||||||
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
CONFIG = configparser.ConfigParser()
|
|
||||||
CONFIG.read("database.ini")
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[database]
|
|
||||||
name=bank.db
|
|
||||||
|
|
||||||
[server]
|
|
||||||
url=http://0.0.0.0:80
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
|
||||||
# Banking System Manager File
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from config import CONFIG # Import Config
|
|
||||||
|
|
||||||
if os.path.exists(CONFIG["database"]["name"]): # Check if the database exists
|
|
||||||
print(f"Database {CONFIG["database"]["name"]} already exists.")
|
|
||||||
else:
|
|
||||||
print(f"Database {CONFIG["database"]["name"]} does not exist. Creating it now.")
|
|
||||||
|
|
||||||
db_url : str = "sqlite:///" + CONFIG["database"]["name"] # Sets the database file to be used from the configuration file
|
|
||||||
print(f"Database file set to: {db_url}")
|
|
||||||
|
|
||||||
engine = create_engine(db_url, echo=True) # Creates the database engine (does not create the database file if it already exists)
|
|
||||||
|
|
||||||
from class_base import Base # Imports the base class required by SQLAlchemy
|
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine) # Creates the tables in the database from the classes
|
|
||||||
|
|
||||||
Session = sessionmaker(bind=engine) # Creates a session to interact with the database
|
|
||||||
session = Session() # Creates a session object
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
|
||||||
# Banking System Test Database Generator
|
|
||||||
|
|
||||||
ADMIN_EMAIL = "lmath56@hotmail.com"
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from server.class_base import Base
|
|
||||||
from class_account import Account
|
|
||||||
from class_client import Client
|
|
||||||
from class_transaction import Transaction
|
|
||||||
from faker import Faker
|
|
||||||
import random
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
|
|
||||||
def generate_hash():
|
|
||||||
seed = str(random.random()).encode('utf-8')
|
|
||||||
return hashlib.sha512(seed).hexdigest()
|
|
||||||
|
|
||||||
def generate_uuid():
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def generate_uuid_short():
|
|
||||||
return str(uuid.uuid4())[:8]
|
|
||||||
|
|
||||||
def current_timestamp():
|
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
def random_date(start, end):
|
|
||||||
return start + timedelta(seconds=random.randint(0, int((end - start).total_seconds())))
|
|
||||||
|
|
||||||
def timestamp_this_year():
|
|
||||||
start = datetime(datetime.now().year, 1, 1)
|
|
||||||
end = datetime(datetime.now().year, 12, 31, 23, 59, 59)
|
|
||||||
return random_date(start, end).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
def timestamp_this_century():
|
|
||||||
start = datetime(2000, 1, 1)
|
|
||||||
end = datetime(2099, 12, 31, 23, 59, 59)
|
|
||||||
return random_date(start, end).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
engine = create_engine('sqlite:///bank.db')
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
fake = Faker()
|
|
||||||
all_account_ids = []
|
|
||||||
|
|
||||||
for i in range(50):
|
|
||||||
is_administrator = 1 if i == 0 else 0
|
|
||||||
password_hash = "7835062ec36ed529fe22cc63baf3ec18d347dacb21c9801da8ba0848cc18efdf1e51717dd5b1240f7556aca3947aa0722452858be6002c1d46b1f1c311b0e9d8" if i == 0 else generate_hash()
|
|
||||||
client_id = generate_uuid_short()
|
|
||||||
client = Client(
|
|
||||||
client_id=client_id,
|
|
||||||
name="ADMIN" if i == 0 else fake.name(),
|
|
||||||
birthdate="ADMIN" if i == 0 else timestamp_this_century(),
|
|
||||||
opening_timestamp=current_timestamp() if i == 0 else timestamp_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(),
|
|
||||||
administrator=is_administrator,
|
|
||||||
hash=password_hash,
|
|
||||||
notes=fake.text(max_nb_chars=50),
|
|
||||||
enabled=1,
|
|
||||||
accounts=[]
|
|
||||||
)
|
|
||||||
session.add(client)
|
|
||||||
|
|
||||||
for j in range(2):
|
|
||||||
account_id = generate_uuid_short()
|
|
||||||
balance = 1000
|
|
||||||
|
|
||||||
for k in range(40):
|
|
||||||
transaction_type = random.choice(['Deposit', 'Withdrawal'])
|
|
||||||
amount = random.randint(1, 200)
|
|
||||||
|
|
||||||
if transaction_type == 'Withdrawal' and balance - amount < 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if transaction_type == 'Deposit':
|
|
||||||
balance += amount
|
|
||||||
else:
|
|
||||||
balance -= amount
|
|
||||||
|
|
||||||
transaction = Transaction(
|
|
||||||
transaction_id=generate_uuid(),
|
|
||||||
account_id=account_id,
|
|
||||||
recipient_account_id=random.choice(all_account_ids) if all_account_ids else account_id,
|
|
||||||
transaction_type=transaction_type,
|
|
||||||
amount=amount,
|
|
||||||
timestamp=timestamp_this_year(),
|
|
||||||
description=fake.text(max_nb_chars=20)
|
|
||||||
)
|
|
||||||
session.add(transaction)
|
|
||||||
|
|
||||||
account = Account(
|
|
||||||
account_id=account_id,
|
|
||||||
client_id=client_id,
|
|
||||||
description=fake.text(max_nb_chars=20),
|
|
||||||
open_timestamp=timestamp_this_year(),
|
|
||||||
account_type=random.choice(['Spending', 'Savings']),
|
|
||||||
balance=balance,
|
|
||||||
enabled=1,
|
|
||||||
notes=fake.text(max_nb_chars=50),
|
|
||||||
transactions=[]
|
|
||||||
)
|
|
||||||
session.add(account)
|
|
||||||
all_account_ids.append(account_id)
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Retrieve the client_id of the administrator account from the session
|
|
||||||
admin_client_id = session.query(Client.client_id).filter(Client.administrator == 1).first()[0]
|
|
||||||
|
|
||||||
# Print the client_id of the administrator account
|
|
||||||
print(f"The client_id of the administrator account of this test database is: {admin_client_id}")
|
|
||||||
print("The password is: Happymeal1")
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
container_name: banking_system_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: banking_system
|
||||||
|
POSTGRES_USER: dbadmin
|
||||||
|
POSTGRES_PASSWORD: your_db_password
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
container_name: banking_system_api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://dbadmin:your_db_password@db/banking_system
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
@@ -6,3 +6,4 @@ flask-session
|
|||||||
faker
|
faker
|
||||||
customtkinter
|
customtkinter
|
||||||
schedule
|
schedule
|
||||||
|
psycopg2
|
||||||
@@ -20,7 +20,7 @@ from manager import log_event # Imports the log_event function from the manager
|
|||||||
def create_app():
|
def create_app():
|
||||||
"""Creates the API using Connexion."""
|
"""Creates the API using Connexion."""
|
||||||
app = connexion.FlaskApp(__name__)
|
app = connexion.FlaskApp(__name__)
|
||||||
app.add_api(CONFIG["api_file"]["name"])
|
app.add_api(CONFIG["server"]["api_file"])
|
||||||
|
|
||||||
flask_app = app.app
|
flask_app = app.app
|
||||||
flask_app.config['SECRET_KEY'] = CONFIG["sessions"]["secret_key"]
|
flask_app.config['SECRET_KEY'] = CONFIG["sessions"]["secret_key"]
|
||||||
@@ -34,7 +34,7 @@ def API():
|
|||||||
app = create_app()
|
app = create_app()
|
||||||
debug_value = CONFIG["server"]["debug"]
|
debug_value = CONFIG["server"]["debug"]
|
||||||
debug = False if debug_value.lower() == 'false' else True
|
debug = False if debug_value.lower() == 'false' else True
|
||||||
app.run(host=CONFIG["server"]["listen_ip"], port=CONFIG["server"]["port"], debug=debug)
|
app.run(host=CONFIG["server"]["url"], debug=debug)
|
||||||
|
|
||||||
################
|
################
|
||||||
### Run Code ###
|
### Run Code ###
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
# Lucas Mathews - Fontys Student ID: 5023572
|
# Lucas Mathews - Fontys Student ID: 5023572
|
||||||
# Banking System Manager File
|
# Banking System Manager File
|
||||||
|
|
||||||
import os.path
|
from config import CONFIG # Import Config
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from config import CONFIG # Import Config
|
db_url : str = f"{CONFIG['database']['type']}://{CONFIG['database']['user']}:{CONFIG['database']['password']}@{CONFIG['database']['ip']}:{CONFIG['database']['port']}/{CONFIG['database']['name']}"
|
||||||
|
print(f"Database URL set to: {db_url}")
|
||||||
|
|
||||||
if os.path.exists(CONFIG["database"]["name"]): # Check if the database exists
|
engine = create_engine(db_url, echo=True) # Creates the database engine
|
||||||
print(f"Database {CONFIG["database"]["name"]} already exists.")
|
|
||||||
else:
|
|
||||||
print(f"Database {CONFIG["database"]["name"]} does not exist. Creating it now.")
|
|
||||||
|
|
||||||
db_url : str = "sqlite:///" + CONFIG["database"]["name"] # Sets the database file to be used from the configuration file
|
|
||||||
print(f"Database file set to: {db_url}")
|
|
||||||
|
|
||||||
engine = create_engine(db_url, echo=True) # Creates the database engine (does not create the database file if it already exists)
|
|
||||||
|
|
||||||
from class_base import Base # Imports the base class required by SQLAlchemy
|
from class_base import Base # Imports the base class required by SQLAlchemy
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ 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 CONFIG["smtp"]["true"] == "False":
|
||||||
|
return True
|
||||||
if client_id in otps:
|
if client_id in otps:
|
||||||
stored_otp, creation_time = otps[client_id]
|
stored_otp, creation_time = otps[client_id]
|
||||||
if stored_otp == otp and time.time() - creation_time <= 300: # Check if OTP is within 5 minutes
|
if stored_otp == otp and time.time() - creation_time <= 300: # Check if OTP is within 5 minutes
|
||||||
@@ -150,10 +152,11 @@ def admin_required(f):
|
|||||||
@login_required
|
@login_required
|
||||||
def generate_otp(client_id: str):
|
def generate_otp(client_id: str):
|
||||||
"""Generates a one-time password for a client and sends it to their email address. Returns a success message if the OTP is generated and an error message otherwise."""
|
"""Generates a one-time password for a client and sends it to their email address. Returns a success message if the OTP is generated and an error message otherwise."""
|
||||||
|
if CONFIG["smtp"]["true"] == "False":
|
||||||
|
return format_response(True, "OTP generation disabled as SMTP is not enabled."), 200
|
||||||
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 generate OTPs for your own client account."), 403
|
return format_response(False, "You can only generate OTPs for your own client account."), 403
|
||||||
|
|
||||||
email = get_email(client_id)
|
email = get_email(client_id)
|
||||||
if email:
|
if email:
|
||||||
password = int(random.randint(100000, 999999)) # Generate a 6-digit OTP
|
password = int(random.randint(100000, 999999)) # Generate a 6-digit OTP
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
[database]
|
|
||||||
name=bank.db
|
|
||||||
|
|
||||||
[api_file]
|
|
||||||
name=api.yml
|
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
|
url=http://0.0.0.0:80/
|
||||||
|
api_file=api.yml
|
||||||
debug=False
|
debug=False
|
||||||
scheduler=True
|
scheduler=True
|
||||||
|
|
||||||
[api]
|
[database]
|
||||||
url=http://0.0.0.0:81/
|
type=postgresql
|
||||||
|
ip=0.0.0.0
|
||||||
|
port=5432
|
||||||
|
name=bank
|
||||||
|
user=root
|
||||||
|
password=
|
||||||
|
|
||||||
[sessions]
|
[sessions]
|
||||||
secret_key=
|
secret_key=
|
||||||
@@ -17,8 +18,8 @@ secret_key=
|
|||||||
[smtp]
|
[smtp]
|
||||||
enabled=True
|
enabled=True
|
||||||
host=
|
host=
|
||||||
port=
|
port=465
|
||||||
username=
|
username=
|
||||||
password=
|
password=
|
||||||
sender_name=Luxbank
|
sender_name=Luxbank
|
||||||
sender_email=
|
sender_email=bank@domain.com
|
||||||
Reference in New Issue
Block a user