Added functionality to Create account, send password reset links

This commit is contained in:
Aaron Barbas 2024-07-21 19:13:40 -05:00
parent 3ae7d17064
commit 4b3b844602
27 changed files with 1125 additions and 51 deletions

8
.idea/.gitignore vendored Normal file
View file

@ -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

21
.idea/AzerothCore.iml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/Webserver/templates" />
</list>
</option>
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (AzerothCore)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (AzerothCore)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AzerothCore.iml" filepath="$PROJECT_DIR$/.idea/AzerothCore.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -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()

View file

@ -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.

View file

@ -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"
}

View file

@ -1 +0,0 @@
import flask

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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()

17
scripts/initialize_db.py Normal file
View file

@ -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()

View file

@ -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"""
<html>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
<div style="max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
<h2 style="color: #333;">Password Reset Request</h2>
<p>Click the button below to reset your password:</p>
<a href="{reset_link}" style="display: inline-block; padding: 10px 20px; font-size: 16px; text-transform: uppercase; font-weight: bold; color: white; background-color: #007bff; text-decoration: none; border-radius: 5px;">Reset Password</a>
<p>If you did not request this email, click the button below to disable the token:</p>
<a href="{disable_link}" style="display: inline-block; padding: 10px 20px; font-size: 16px; text-transform: uppercase; font-weight: bold; color: white; background-color: #ff0000; text-decoration: none; border-radius: 5px;">Disable Token</a>
<p style="color: #999; margin-top: 20px;">If you have any questions, feel free to contact our support team.</p>
</div>
</body>
</html>
"""
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/<token>', 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

48
static/style.css Normal file
View file

@ -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;
}

View file

View file

@ -1,27 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azerothcore Account Creator</title>
</head>
<body>
<h1>Account Creation</h1>
<form>
<label for="accountName">Account Name:</label><br>
<input type="text" id="accountName" maxlength="20"><br>
<label for="email">Email Address:</label><br>
<input type="email" id="email"><br>
<label for="passwd1">Password:</label><br>
<input type="password" id="passwd1"><br>
<label for="passwd2">Re-enter Password:</label><br>
<input type="password" id="passwd2"><br><br>
<select name="expansion" id="exp">
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="heading">Create Account</div>
<form id="accountForm" class="form" onsubmit="event.preventDefault(); createAccount();">
<div class="input-field">
<label for="accountName">Account Name</label>
<input type="text" id="accountName" maxlength="20" required autocomplete="off" />
</div>
<div class="input-field">
<label for="email">Email Address</label>
<input type="email" id="email" required autocomplete="off" />
</div>
<div class="input-field">
<label for="passwd1">Password</label>
<input type="password" id="passwd1" minlength="8" required autocomplete="off" />
</div>
<div class="input-field">
<label for="passwd2">Re-enter Password</label>
<input type="password" id="passwd2" minlength="8" required autocomplete="off" />
</div>
<div class="input-field">
<label for="exp">Expansion</label>
<select name="expansion" id="exp" required>
<option value="2">Wrath of the Lich King</option>
<option value="1">The Burning Crusade</option>
<option value="0">World of Warcraft (Classic)</option>
</select>
</div>
<div class="btn-container">
<button type="submit" class="btn">Create Account</button>
</div>
</form>
<p id="result"></p>
</div>
<script>
function createAccount() {
const accountName = document.getElementById('accountName').value;
const email = document.getElementById('email').value;
const passwd1 = document.getElementById('passwd1').value;
const passwd2 = document.getElementById('passwd2').value;
const expansion = document.getElementById('exp').value;
</body>
</html>
if (passwd1.length < 8) {
document.getElementById('result').innerText = 'Password must be at least 8 characters long.';
return;
}
if (passwd1 !== passwd2) {
document.getElementById('result').innerText = 'Passwords do not match.';
return;
}
fetch('/create_account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
accountName: accountName,
email: email,
passwd1: passwd1,
passwd2: passwd2,
expansion: expansion
}),
})
.then(response => response.json())
.then(data => {
document.getElementById('result').innerText = data.message;
if (data.message === "Account created successfully!") {
document.getElementById('accountForm').reset();
}
})
.catch((error) => {
console.error('Error:', error);
});
}
</script>
{% endblock %}

112
templates/base.html Normal file
View file

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azerothcore</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<style>
body {
font-family: Arial, sans-serif;
background: url("{{ url_for('static', filename='../static/images/background.png') }}") no-repeat center center fixed;
background-size: cover;
margin: 0;
padding: 0;
}
nav ul {
list-style-type: none;
padding: 0;
background-color: rgba(52, 58, 64, 0.8);
margin: 0;
display: flex;
justify-content: flex-end; /* Aligns links to the right */
}
nav ul li {
margin: 0;
}
nav ul li a {
display: block;
color: white;
text-align: center;
padding: 14px 20px;
text-decoration: none;
}
nav ul li a:hover {
background-color: #495057;
}
.content {
display: flex;
justify-content: center;
align-items: center;
height: calc(100vh - 60px); /* Reduced the height to decrease top space */
padding: 20px;
}
.container {
border: solid 1px rgba(52, 58, 64, 0.8);
padding: 20px;
border-radius: 20px;
background-color: rgba(52, 58, 64, 0.8); /* Match with navigation bar */
max-width: 500px;
width: 100%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.heading {
font-size: 1.5rem;
margin-bottom: 20px;
font-weight: bolder;
color: white;
}
.form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form input, .form select {
padding: 10px;
font-size: 1rem;
border-radius: 5px;
border: 1px solid #ced4da;
width: 100%;
box-sizing: border-box; /* Ensure the select and input are the same width */
background-color: rgba(255, 255, 255, 0.9); /* Light background for inputs */
}
.form label {
color: white;
text-align: left;
display: block;
margin-bottom: 5px;
}
.btn {
padding: 10px;
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;
display: inline-block;
margin-top: 10px;
width: 100%;
box-sizing: border-box;
}
.btn:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<nav>
<ul>
<li><a href="{{ url_for('home') }}">Account Creation</a></li>
<li><a href="{{ url_for('reset_password') }}">Reset Password</a></li>
</ul>
</nav>
<div class="content">
{% block content %}{% endblock %}
</div>
</body>
</html>

View file

@ -0,0 +1,84 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="heading">Set New Password</div>
<form class="form" onsubmit="event.preventDefault(); updatePassword();">
<input type="hidden" id="email" value="{{ email }}">
<input type="hidden" id="token" value="{{ token }}">
<div class="input-field">
<label for="passwd1">New Password</label>
<input type="password" id="passwd1" minlength="8" required>
</div>
<div class="input-field">
<label for="passwd2">Confirm New Password</label>
<input type="password" id="passwd2" minlength="8" required>
</div>
<div id="passwordMatchMessage" style="color: red;"></div>
<div class="btn-container">
<button type="submit" class="btn" id="updatePasswordBtn" disabled>Update Password</button>
</div>
</form>
<p id="result"></p>
</div>
<script>
function validatePasswords() {
const passwd1 = document.getElementById('passwd1').value;
const passwd2 = document.getElementById('passwd2').value;
const passwordMatchMessage = document.getElementById('passwordMatchMessage');
const updatePasswordBtn = document.getElementById('updatePasswordBtn');
if (passwd1.length >= 8 && passwd2.length >= 8 && passwd1 === passwd2) {
passwordMatchMessage.innerText = '';
updatePasswordBtn.disabled = false;
} else {
if (passwd1 !== passwd2) {
passwordMatchMessage.innerText = 'Passwords do not match.';
} else {
passwordMatchMessage.innerText = '';
}
updatePasswordBtn.disabled = true;
}
}
function updatePassword() {
const email = document.getElementById('email').value;
const token = document.getElementById('token').value;
const passwd1 = document.getElementById('passwd1').value;
const passwd2 = document.getElementById('passwd2').value;
const result = document.getElementById('result');
if (passwd1 !== passwd2) {
result.innerText = 'Passwords do not match.';
return;
}
fetch('/update_password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
token: token,
password: passwd1
}),
})
.then(response => response.json())
.then(data => {
if (data.message === 'Password updated successfully!') {
window.location.href = '/success';
} else {
result.innerText = data.message;
}
})
.catch((error) => {
result.innerText = 'An error occurred. Please try again.';
console.error('Error:', error);
});
}
document.getElementById('passwd1').addEventListener('input', validatePasswords);
document.getElementById('passwd2').addEventListener('input', validatePasswords);
</script>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="heading">Reset Password</div>
<form id="resetForm" class="form" onsubmit="event.preventDefault(); resetPassword();">
<div class="input-field">
<label for="email">Email Address</label>
<input type="email" id="email" required autocomplete="off" />
</div>
<div class="btn-container">
<button type="submit" class="btn">Reset Password</button>
</div>
</form>
<p id="result" style="color: white;"></p>
</div>
<script>
function resetPassword() {
const email = document.getElementById('email').value;
fetch('/reset_password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
.then(response => response.json())
.then(data => {
const resultElement = document.getElementById('result');
resultElement.innerText = data.message;
resultElement.style.color = data.success ? 'red' : 'white';
})
.catch((error) => {
console.error('Error:', error);
});
}
</script>
{% endblock %}

9
templates/success.html Normal file
View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="heading">Success</div>
<p>Your password has been updated successfully!</p>
<a href="/" class="btn">Return to Home</a>
</div>
{% endblock %}

BIN
tokens.db Normal file

Binary file not shown.

295
website.py Normal file
View file

@ -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"""
<html>
<body style="font-family: Arial, sans-serif; background-image: url('cid:background'); background-size: cover; padding: 20px;">
<div style="max-width: 600px; margin: 0 auto; background: rgba(0, 0, 0, 0.8); padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
<h2 style="color: #fff;">Password Reset Request</h2>
<p style="color: #fff;">Click the button below to reset your password:</p>
<a href="{reset_link}" style="display: inline-block; padding: 10px 20px; font-size: 16px; text-transform: uppercase; font-weight: bold; color: white; background-color: #007bff; text-decoration: none; border-radius: 5px;">Reset Password</a>
<p style="color: #fff;">If you did not request this email, click the button below to disable the token:</p>
<a href="{disable_link}" style="display: inline-block; padding: 10px 20px; font-size: 16px; text-transform: uppercase; font-weight: bold; color: white; background-color: #ff0000; text-decoration: none; border-radius: 5px;">Disable Token</a>
<p style="color: #ccc; margin-top: 20px;">If you have any questions, feel free to contact our support team.</p>
</div>
</body>
</html>
"""
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', '<background>')
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)