diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/AzerothCore.iml b/.idea/AzerothCore.iml new file mode 100644 index 0000000..f099b8a --- /dev/null +++ b/.idea/AzerothCore.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..cb50efa --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f4c9d75 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AzerothcoreAccountCreation/manage.py b/AzerothcoreAccountCreation/manage.py deleted file mode 100755 index 580afdf..0000000 --- a/AzerothcoreAccountCreation/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'AzerothcoreAccountCreation.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/README.md b/README.md index e69de29..da9c591 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,84 @@ +# AzerothCore Account Management + +This application allows users to create and manage accounts for the World of Warcraft: Wrath of the Lich King private server. It provides features such as account creation, password reset, and email notifications. + +## Features + +- **Account Creation**: Create new accounts with username, email, password, and expansion details. +- **Password Reset**: Reset account passwords through email verification. +- **Secure Communication**: Utilizes Gmail App Passwords for secure email communication. + +## Prerequisites + +- **Python 3.8+** +- **MySQL**: Database for storing user data +- **Gmail App Passwords**: For sending emails securely + +## Installation + +1. **Clone the repository:** + + ```bash + git clone https://github.com/BeardedInfoSec/AzerothCore.git + cd AzerothCore + ``` + +2. **Configure the application:** + + Ensure the `config.json` file in the root directory has the following structure and update it with your details: + + ```json + { + "USERNAME": "acore", + "PASSWORD": "password", + "SERVER_IP": "127.0.0.1", + "MYSQL_PORT": 3306, + "DATABASE": "acore_auth", + "SMTP_EMAIL_ADDRESS": "your_email@gmail.com", + "SMTP_EMAIL_PASSWORD": "your_app_password" + } + ``` + + **Note**: Ensure you create a [Gmail App Password](https://myaccount.google.com/apppasswords) and enable [2-Step Verification](https://support.google.com/accounts/answer/185833?hl=en) for your Google account. + +## Running the Application + +1. **Start the Flask application:** + + ```bash + python website.py + ``` + + The application will be available at `http://127.0.0.1:5000/`. + + **Note**: The SQLite database for password reset tokens will be auto-initialized when the website is run. + +## Configuration Notes + +### HTTP vs. HTTPS + +- **HTTP**: Sends web traffic in plain text, making it potentially vulnerable to interception and attacks. It is **not secure**. +- **HTTPS**: Encrypts web traffic, ensuring data is securely transmitted between the client and server. It is **recommended** for all web applications to protect sensitive data. + +To secure your application: + +- Open ports 80 (HTTP) and 443 (HTTPS) on your server. +- Configure your firewall to allow traffic on these ports and point to your server's IP address or domain. +- Obtain and install an SSL/TLS certificate to enable HTTPS. + +## Security Best Practices + +- **Disable Debug Mode**: Ensure `debug=False` in your app configuration. +- **Use Environment Variables**: Store sensitive data in environment variables. +- **Enable HTTPS**: Secure your application with HTTPS. +- **Set Secure Headers**: Use libraries like `Flask-Talisman` to set secure headers. +- **Rate Limiting**: Implement rate limiting to protect against brute force attacks. +- **Input Validation**: Always validate and sanitize input data. + +## Contact + +For any issues or questions, please contact [your_email@example.com]. + +--- + +This README provides comprehensive instructions for setting up and running your AzerothCore account management application securely. diff --git a/config.json b/config.json index 265d9e9..9e49a50 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,10 @@ { "USERNAME" : "acore", "PASSWORD" : "password", - "SERVER_IP" : "127.0.01", - "MYSQL_PORT" : 3306 + "SERVER_IP" : "127.0.0.1", + "MYSQL_PORT" : 3306, + "DATABASE" : "acore_auth", + "SMTP_EMAIL_ADDRESS" : "gmail_address", + "SMTP_EMAIL_PASSWORD" : "gmail_app_password" } \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index f7696e9..0000000 --- a/main.py +++ /dev/null @@ -1 +0,0 @@ -import flask diff --git a/scripts/__pycache__/createAccount.cpython-310.pyc b/scripts/__pycache__/createAccount.cpython-310.pyc new file mode 100644 index 0000000..223dc78 Binary files /dev/null and b/scripts/__pycache__/createAccount.cpython-310.pyc differ diff --git a/scripts/__pycache__/initialize_db.cpython-310.pyc b/scripts/__pycache__/initialize_db.cpython-310.pyc new file mode 100644 index 0000000..cb6b41e Binary files /dev/null and b/scripts/__pycache__/initialize_db.cpython-310.pyc differ diff --git a/scripts/__pycache__/resetPassword.cpython-310.pyc b/scripts/__pycache__/resetPassword.cpython-310.pyc new file mode 100644 index 0000000..407d88c Binary files /dev/null and b/scripts/__pycache__/resetPassword.cpython-310.pyc differ diff --git a/scripts/createAccount.py b/scripts/createAccount.py index e69de29..9d33345 100644 --- a/scripts/createAccount.py +++ b/scripts/createAccount.py @@ -0,0 +1,85 @@ +import mysql.connector +import hashlib +import os +import json +from pathlib import Path + +def sha1(data): + return hashlib.sha1(data).digest() + +def generate_salt(): + return os.urandom(32) + +def calculate_verifier(username, password, salt): + g = 7 + N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) + + username = username.upper() + password = password.upper() + + h1 = sha1(f"{username}:{password}".encode()) + h2 = sha1(salt + h1) + + h2_int = int.from_bytes(h2, 'little') + verifier_int = pow(g, h2_int, N) + + verifier = verifier_int.to_bytes((verifier_int.bit_length() + 7) // 8, 'little') + return verifier + +def create_account(account_name, email, passwd1, passwd2, expansion): + if passwd1 != passwd2: + return "Passwords do not match." + + script_dir = Path(__file__).resolve().parent + config_path = script_dir / "../config.json" + + with open(config_path) as config_file: + config = json.load(config_file) + + USERNAME = config["USERNAME"] + PASSWORD = config["PASSWORD"] + SERVER_IP = config["SERVER_IP"] + PORT = config["MYSQL_PORT"] + DATABASE = config["DATABASE"] + + conn = None + cursor = None + + try: + conn = mysql.connector.connect( + host=SERVER_IP, + user=USERNAME, + password=PASSWORD, + database=DATABASE, + port=PORT + ) + cursor = conn.cursor() + + # Check if the username already exists + cursor.execute("SELECT id FROM account WHERE username = %s", (account_name,)) + if cursor.fetchone(): + return "Username already taken." + + cursor.execute("SELECT MAX(id) FROM account") + max_id = cursor.fetchone()[0] + new_id = max_id + 1 if max_id else 1 + + salt = generate_salt() + verifier = calculate_verifier(account_name, passwd1, salt) + + cursor.execute( + "INSERT INTO account (id, username, salt, verifier, email) VALUES (%s, %s, %s, %s, %s)", + (new_id, account_name, salt, verifier, email) + ) + conn.commit() + + return "Account created successfully!" + + except mysql.connector.Error as err: + return f"Error: {err}" + + finally: + if cursor is not None: + cursor.close() + if conn is not None: + conn.close() diff --git a/scripts/initialize_db.py b/scripts/initialize_db.py new file mode 100644 index 0000000..1a71f8e --- /dev/null +++ b/scripts/initialize_db.py @@ -0,0 +1,17 @@ +import sqlite3 + +def initialize_db(): + conn = sqlite3.connect('tokens.db') + cursor = conn.cursor() + + cursor.execute(''' + CREATE TABLE IF NOT EXISTS password_reset_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + token TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() diff --git a/scripts/resetPassword.py b/scripts/resetPassword.py index e69de29..0ca1496 100644 --- a/scripts/resetPassword.py +++ b/scripts/resetPassword.py @@ -0,0 +1,212 @@ +import mysql.connector +import hashlib +import os +import json +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from flask import Flask, request, render_template, jsonify, url_for + +app = Flask(__name__) + +def get_db_connection(): + script_dir = Path(__file__).resolve().parent + config_path = script_dir / "../config.json" + + with open(config_path) as config_file: + config = json.load(config_file) + + return mysql.connector.connect( + host=config["SERVER_IP"], + user=config["USERNAME"], + password=config["PASSWORD"], + database=config["DATABASE"], + port=config["MYSQL_PORT"] + ) + +def get_config(): + script_dir = Path(__file__).resolve().parent + config_path = script_dir / "../config.json" + + with open(config_path) as config_file: + return json.load(config_file) + +@app.route('/resetpassword') +def reset_password(): + return render_template('resetpassword.html') + +@app.route('/reset_password', methods=['POST']) +def handle_reset_password(): + data = request.json + email = data.get('email') + + if not email: + return jsonify({'success': False, 'message': 'Email is required.'}), 400 + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + try: + cursor.execute("SELECT id FROM account WHERE email = %s", (email,)) + account = cursor.fetchone() + + if not account: + return jsonify({'success': False, 'message': 'Email not found.'}), 404 + + token = os.urandom(24).hex() + + cursor.execute( + "INSERT INTO password_reset_tokens (account_id, token) VALUES (%s, %s)", + (account['id'], token) + ) + conn.commit() + + reset_link = url_for('reset_password_token', token=token, _external=True) + disable_link = url_for('disable_token', token=token, email=email, _external=True) + + send_email(email, reset_link, disable_link, 'Azerothcore Password Reset Request') + + return jsonify({'success': True, 'message': 'Password reset link has been sent to your email.'}) + + except mysql.connector.Error as err: + return jsonify({'success': False, 'message': str(err)}), 500 + + finally: + cursor.close() + conn.close() + +def send_email(to_email, reset_link, disable_link, subject): + config = get_config() + from_email = config["SMTP_EMAIL_ADDRESS"] + from_password = config["SMTP_EMAIL_PASSWORD"] + + msg = MIMEMultipart('alternative') + msg['From'] = from_email + msg['To'] = to_email + msg['Subject'] = subject + + text_content = f'Click the link to reset your password: {reset_link}' + html_content = f""" + + +
+

Password Reset Request

+

Click the button below to reset your password:

+ Reset Password +

If you did not request this email, click the button below to disable the token:

+ Disable Token +

If you have any questions, feel free to contact our support team.

+
+ + + """ + + part1 = MIMEText(text_content, 'plain') + part2 = MIMEText(html_content, 'html') + + msg.attach(part1) + msg.attach(part2) + + try: + with smtplib.SMTP('smtp.gmail.com', 587) as server: + server.starttls() + server.login(from_email, from_password) + server.sendmail(from_email, to_email, msg.as_string()) + return True + except Exception as e: + print(f"Failed to send email: {e}") + return False + +@app.route('/disable_token', methods=['GET']) +def disable_token(): + token = request.args.get('token') + email = request.args.get('email') + + if not token or not email: + return jsonify({'message': 'Invalid request.'}), 400 + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute("DELETE FROM password_reset_tokens WHERE token = %s AND email = %s", (token, email)) + conn.commit() + + if cursor.rowcount == 0: + return jsonify({'message': 'Token not found or already disabled.'}), 404 + + return jsonify({'message': 'Token disabled successfully.'}), 200 + + except mysql.connector.Error as err: + return jsonify({'message': str(err)}), 500 + + finally: + cursor.close() + conn.close() + +@app.route('/reset_password/', methods=['GET', 'POST']) +def reset_password_token(token): + if request.method == 'POST': + data = request.form + password = data.get('password') + confirm_password = data.get('confirm_password') + + if not password or not confirm_password: + return render_template('resetpassword.html', message='All fields are required.') + + if password != confirm_password: + return render_template('resetpassword.html', message='Passwords do not match.') + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute("SELECT account_id FROM password_reset_tokens WHERE token = %s", (token,)) + result = cursor.fetchone() + + if not result: + return render_template('resetpassword.html', message='Invalid or expired token.') + + account_id = result[0] + salt = os.urandom(32) + verifier = calculate_verifier("USERNAME", password, salt) # Update USERNAME appropriately + + cursor.execute( + "UPDATE account SET salt = %s, verifier = %s WHERE id = %s", + (salt, verifier, account_id) + ) + conn.commit() + + cursor.execute("DELETE FROM password_reset_tokens WHERE token = %s", (token,)) + conn.commit() + + return redirect(url_for('success')) + + finally: + cursor.close() + conn.close() + + return render_template('resetpassword.html') + +@app.route('/success') +def success(): + return render_template('success.html') + +def calculate_verifier(username, password, salt): + g = 7 + N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) + + username = username.upper() + password = password.upper() + + h1 = hashlib.sha1(f"{username}:{password}".encode()).digest() + h2 = hashlib.sha1(salt + h1).digest() + + h2_int = int.from_bytes(h2, 'little') + verifier_int = pow(g, h2_int, N) + + verifier = verifier_int.to_bytes((verifier_int.bit_length() + 7) // 8, 'little') + return verifier + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/images/background.png b/static/images/background.png new file mode 100644 index 0000000..bc64185 Binary files /dev/null and b/static/images/background.png differ diff --git a/static/images/resetemail.png b/static/images/resetemail.png new file mode 100644 index 0000000..4b9d747 Binary files /dev/null and b/static/images/resetemail.png differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..ac7407e --- /dev/null +++ b/static/style.css @@ -0,0 +1,48 @@ +.form { + display: flex; + flex-direction: column; + gap: 15px; + padding: 20px; +} + +.input-field { + display: flex; + flex-direction: column; + gap: 5px; +} + +.input-field input,.input-field select { + padding: 10px; + font-size: 1rem; + border-radius: 5px; + border: 1px solid #ced4da; + width: 100%; +} + +.input-field label { + font-size: 1rem; + margin-bottom: 5px; +} + +.btn-container { + display: flex; + justify-content: center; + margin-top: 20px; +} + +.btn { + padding: 10px 20px; + font-size: 1rem; + text-transform: uppercase; + font-weight: bold; + color: white; + background-color: #007bff; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/styles/style.css b/styles/style.css deleted file mode 100644 index e69de29..0000000 diff --git a/templates/accountcreation.html b/templates/accountcreation.html index 6c40fc5..fa95ab5 100644 --- a/templates/accountcreation.html +++ b/templates/accountcreation.html @@ -1,27 +1,80 @@ - - - - - - Azerothcore Account Creator - - -

Account Creation

-
-
-
-
-
-
-
-
-

- +{% extends "base.html" %} + +{% block content %} +
+
Create Account
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
- - - \ No newline at end of file +

+
+ +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..af35740 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,112 @@ + + + + + + Azerothcore + + + + + +
+ {% block content %}{% endblock %} +
+ + diff --git a/templates/newpassword.html b/templates/newpassword.html new file mode 100644 index 0000000..62cfd84 --- /dev/null +++ b/templates/newpassword.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} + +{% block content %} +
+
Set New Password
+
+ + +
+ + +
+
+ + +
+
+
+ +
+
+

+
+ +{% endblock %} diff --git a/templates/resetpassword.html b/templates/resetpassword.html index e69de29..646f687 100644 --- a/templates/resetpassword.html +++ b/templates/resetpassword.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
+
Reset Password
+
+
+ + +
+
+ +
+
+

+
+ +{% endblock %} diff --git a/templates/success.html b/templates/success.html new file mode 100644 index 0000000..fa2b0bc --- /dev/null +++ b/templates/success.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
+
Success
+

Your password has been updated successfully!

+ Return to Home +
+{% endblock %} diff --git a/tokens.db b/tokens.db new file mode 100644 index 0000000..f8edaff Binary files /dev/null and b/tokens.db differ diff --git a/website.py b/website.py new file mode 100644 index 0000000..0daa60b --- /dev/null +++ b/website.py @@ -0,0 +1,295 @@ +from flask import Flask, render_template, request, jsonify, url_for, redirect +import mysql.connector +import hashlib +import os +import json +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.mime.image import MIMEImage +from pathlib import Path +from base64 import urlsafe_b64encode, urlsafe_b64decode +import secrets +import sqlite3 +import time +from scripts.createAccount import create_account +from scripts.initialize_db import initialize_db + +app = Flask(__name__) + +# Initialize the SQLite database +initialize_db() + +def get_db_connection(): + script_dir = Path(__file__).resolve().parent + config_path = script_dir / "config.json" + + with open(config_path) as config_file: + config = json.load(config_file) + + return mysql.connector.connect( + host=config["SERVER_IP"], + user=config["USERNAME"], + password=config["PASSWORD"], + database=config["DATABASE"], + port=config["MYSQL_PORT"] + ) + +def get_sqlite_connection(): + return sqlite3.connect('tokens.db') + +def get_config(): + script_dir = Path(__file__).resolve().parent + config_path = script_dir / "config.json" + + with open(config_path) as config_file: + return json.load(config_file) + +@app.route('/') +def home(): + return render_template('accountcreation.html') + +@app.route('/resetpassword') +def reset_password(): + return render_template('resetpassword.html') + +@app.route('/success') +def success(): + return render_template('success.html') + +@app.route('/newpassword') +def new_password(): + token = request.args.get('token') + email = request.args.get('email') + + if not token or not email: + return "Invalid or expired reset link.", 400 + + try: + decoded_email = urlsafe_b64decode(email.encode()).decode() + except Exception as e: + return "Invalid or expired reset link.", 400 + + conn = get_sqlite_connection() + cursor = conn.cursor() + + cursor.execute("SELECT email FROM password_reset_tokens WHERE token = ? AND email = ? AND created_at > datetime('now', '-1 hour')", (token, decoded_email)) + token_info = cursor.fetchone() + conn.close() + + if not token_info: + return "Invalid or expired reset link.", 400 + + return render_template('newpassword.html', email=decoded_email, token=token) + +@app.route('/create_account', methods=['POST']) +def handle_create_account(): + data = request.get_json() + account_name = data['accountName'] + email = data['email'] + passwd1 = data['passwd1'] + passwd2 = data['passwd2'] + expansion = data['expansion'] + + result = create_account(account_name, email, passwd1, passwd2, expansion) + return jsonify({'message': result}) + +@app.route('/reset_password', methods=['POST']) +def reset_password_request(): + data = request.json + email = data.get('email') + + if not email: + return jsonify({'message': 'Email is required.'}), 400 + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + try: + cursor.execute("SELECT id FROM account WHERE email = %s", (email,)) + account = cursor.fetchone() + + if not account: + return jsonify({'message': 'Email not found.'}), 404 + + token = secrets.token_urlsafe(16) + encoded_email = urlsafe_b64encode(email.encode()).decode() + + sqlite_conn = get_sqlite_connection() + sqlite_cursor = sqlite_conn.cursor() + sqlite_cursor.execute("INSERT INTO password_reset_tokens (email, token) VALUES (?, ?)", (email, token)) + sqlite_conn.commit() + sqlite_conn.close() + + reset_link = url_for('new_password', token=token, email=encoded_email, _external=True) + disable_link = url_for('disable_token', token=token, email=encoded_email, _external=True) + + send_email(email, reset_link, disable_link, 'Azerothcore Password Reset Request') + + return jsonify({'message': 'Password reset link has been sent to your email.'}) + + except mysql.connector.Error as err: + return jsonify({'message': str(err)}), 500 + + finally: + cursor.close() + conn.close() + +@app.route('/disable_token', methods=['GET']) +def disable_token(): + token = request.args.get('token') + email = request.args.get('email') + + if not token or not email: + return jsonify({'message': 'Invalid request.'}), 400 + + try: + decoded_email = urlsafe_b64decode(email.encode()).decode() + except Exception as e: + return jsonify({'message': 'Invalid request.'}), 400 + + conn = get_sqlite_connection() + cursor = conn.cursor() + + try: + cursor.execute("DELETE FROM password_reset_tokens WHERE token = ? AND email = ?", (token, decoded_email)) + conn.commit() + + if cursor.rowcount == 0: + return jsonify({'message': 'Token not found or already disabled.'}), 404 + + return jsonify({'message': 'Token disabled successfully.'}), 200 + + except sqlite3.Error as err: + return jsonify({'message': str(err)}), 500 + + finally: + cursor.close() + conn.close() + +@app.route('/update_password', methods=['POST']) +def update_password(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + token = data.get('token') + + if not email or not password or not token: + return jsonify({'message': 'All fields are required.'}), 400 + + if len(password) < 8: + return jsonify({'message': 'Password must be at least 8 characters long.'}), 400 + + try: + decoded_email = urlsafe_b64decode(email.encode()).decode() + except Exception as e: + return jsonify({'message': 'Invalid or expired token.'}), 400 + + conn = get_sqlite_connection() + cursor = conn.cursor() + + cursor.execute("SELECT email FROM password_reset_tokens WHERE token = ? AND email = ? AND created_at > datetime('now', '-1 hour')", (token, decoded_email)) + token_info = cursor.fetchone() + + if not token_info: + conn.close() + return jsonify({'message': 'Invalid or expired token.'}), 400 + + cursor.execute("DELETE FROM password_reset_tokens WHERE token = ? AND email = ?", (token, decoded_email)) + conn.commit() + conn.close() + + conn = get_db_connection() + cursor = conn.cursor(dictionary=True) + + try: + cursor.execute("SELECT username FROM account WHERE email = %s", (decoded_email,)) + account = cursor.fetchone() + + if not account: + return jsonify({'message': 'Account not found.'}), 404 + + username = account['username'] + salt = os.urandom(32) + verifier = calculate_verifier(username, password, salt) + + cursor.execute("UPDATE account SET salt = %s, verifier = %s WHERE email = %s", (salt, verifier, decoded_email)) + conn.commit() + + send_email(decoded_email, "", "", 'Password Changed', 'Your password has been successfully changed.') + + return jsonify({'message': 'Password updated successfully!'}) + + except mysql.connector.Error as err: + return jsonify({'message': str(err)}), 500 + + finally: + cursor.close() + conn.close() + +def send_email(to_email, reset_link, disable_link, subject): + config = get_config() + from_email = config["SMTP_EMAIL_ADDRESS"] + from_password = config["SMTP_EMAIL_PASSWORD"] + + msg = MIMEMultipart('alternative') + msg['From'] = from_email + msg['To'] = to_email + msg['Subject'] = subject + + text_content = f'Click the link to reset your password: {reset_link}' + html_content = f""" + + +
+

Password Reset Request

+

Click the button below to reset your password:

+ Reset Password +

If you did not request this email, click the button below to disable the token:

+ Disable Token +

If you have any questions, feel free to contact our support team.

+
+ + + """ + + part1 = MIMEText(text_content, 'plain') + part2 = MIMEText(html_content, 'html') + + # Attach the background image + with open("static/images/resetemail.png", "rb") as img_file: + img = MIMEImage(img_file.read()) + img.add_header('Content-ID', '') + msg.attach(img) + + msg.attach(part1) + msg.attach(part2) + + try: + with smtplib.SMTP('smtp.gmail.com', 587) as server: + server.starttls() + server.login(from_email, from_password) + server.sendmail(from_email, to_email, msg.as_string()) + return True + except Exception as e: + print(f"Failed to send email: {e}") + return False + +def calculate_verifier(username, password, salt): + g = 7 + N = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) + + username = username.upper() + password = password.upper() + + h1 = hashlib.sha1(f"{username}:{password}".encode()).digest() + h2 = hashlib.sha1(salt + h1).digest() + + h2_int = int.from_bytes(h2, 'little') + verifier_int = pow(g, h2_int, N) + + verifier = verifier_int.to_bytes((verifier_int.bit_length() + 7) // 8, 'little') + return verifier + +if __name__ == '__main__': + app.run(debug=True)