Added functionality to Create account, send password reset links
This commit is contained in:
parent
3ae7d17064
commit
4b3b844602
27 changed files with 1125 additions and 51 deletions
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
21
.idea/AzerothCore.iml
Normal 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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal 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
7
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -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()
|
|
||||||
84
README.md
84
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.
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
{
|
{
|
||||||
"USERNAME" : "acore",
|
"USERNAME" : "acore",
|
||||||
"PASSWORD" : "password",
|
"PASSWORD" : "password",
|
||||||
"SERVER_IP" : "127.0.01",
|
"SERVER_IP" : "127.0.0.1",
|
||||||
"MYSQL_PORT" : 3306
|
"MYSQL_PORT" : 3306,
|
||||||
|
"DATABASE" : "acore_auth",
|
||||||
|
"SMTP_EMAIL_ADDRESS" : "gmail_address",
|
||||||
|
"SMTP_EMAIL_PASSWORD" : "gmail_app_password"
|
||||||
|
|
||||||
}
|
}
|
||||||
1
main.py
1
main.py
|
|
@ -1 +0,0 @@
|
||||||
import flask
|
|
||||||
BIN
scripts/__pycache__/createAccount.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/createAccount.cpython-310.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/initialize_db.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/initialize_db.cpython-310.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/resetPassword.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/resetPassword.cpython-310.pyc
Normal file
Binary file not shown.
|
|
@ -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
17
scripts/initialize_db.py
Normal 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()
|
||||||
|
|
@ -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)
|
||||||
BIN
static/images/background.png
Normal file
BIN
static/images/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 612 KiB |
BIN
static/images/resetemail.png
Normal file
BIN
static/images/resetemail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
48
static/style.css
Normal file
48
static/style.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,80 @@
|
||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="UTF-8">
|
<div class="container">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<div class="heading">Create Account</div>
|
||||||
<title>Azerothcore Account Creator</title>
|
<form id="accountForm" class="form" onsubmit="event.preventDefault(); createAccount();">
|
||||||
</head>
|
<div class="input-field">
|
||||||
<body>
|
<label for="accountName">Account Name</label>
|
||||||
<h1>Account Creation</h1>
|
<input type="text" id="accountName" maxlength="20" required autocomplete="off" />
|
||||||
<form>
|
</div>
|
||||||
<label for="accountName">Account Name:</label><br>
|
<div class="input-field">
|
||||||
<input type="text" id="accountName" maxlength="20"><br>
|
<label for="email">Email Address</label>
|
||||||
<label for="email">Email Address:</label><br>
|
<input type="email" id="email" required autocomplete="off" />
|
||||||
<input type="email" id="email"><br>
|
</div>
|
||||||
<label for="passwd1">Password:</label><br>
|
<div class="input-field">
|
||||||
<input type="password" id="passwd1"><br>
|
<label for="passwd1">Password</label>
|
||||||
<label for="passwd2">Re-enter Password:</label><br>
|
<input type="password" id="passwd1" minlength="8" required autocomplete="off" />
|
||||||
<input type="password" id="passwd2"><br><br>
|
</div>
|
||||||
<select name="expansion" id="exp">
|
<div class="input-field">
|
||||||
<option value="2">Wrath of the Lich King</option>
|
<label for="passwd2">Re-enter Password</label>
|
||||||
<option value="1">The Burning Crusade</option>
|
<input type="password" id="passwd2" minlength="8" required autocomplete="off" />
|
||||||
<option value="0">World of Warcraft (Classic)</option>
|
</div>
|
||||||
</select>
|
<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>
|
</form>
|
||||||
|
<p id="result"></p>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
<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;
|
||||||
|
|
||||||
|
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
112
templates/base.html
Normal 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>
|
||||||
84
templates/newpassword.html
Normal file
84
templates/newpassword.html
Normal 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 %}
|
||||||
|
|
@ -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
9
templates/success.html
Normal 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
BIN
tokens.db
Normal file
Binary file not shown.
295
website.py
Normal file
295
website.py
Normal 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)
|
||||||
Loading…
Reference in a new issue