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 import base64 import logging # Set up logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') # Helper function to check if a string is base64-encoded def is_base64_encoded(s): try: if isinstance(s, str): s = s.encode('utf-8') return base64.urlsafe_b64encode(base64.urlsafe_b64decode(s)) == s except Exception: return False 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('home.html') @app.route('/accountcreation') def account_creation(): 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'] # Check if the email or account name already exists in the database conn = get_db_connection() cursor = conn.cursor(dictionary=True) try: # Check if the account name or email already exists cursor.execute("SELECT id FROM account WHERE username = %s OR email = %s", (account_name, email)) existing_account = cursor.fetchone() if existing_account: return jsonify({'message': 'Sorry, either the account name or email is already in use.'}), 400 except mysql.connector.Error as err: return jsonify({'message': f'Database error: {str(err)}'}), 500 finally: cursor.close() conn.close() # Proceed with account creation if the email and account name are not already in use result = create_account(account_name, email, passwd1, passwd2) # Send a confirmation email after account creation send_email('new_account', email, "", 'Account Created', 'Your Angry Haircraft account has been successfully created.') return jsonify({'message': result}), 201 @app.route('/reset_password', methods=['POST']) def reset_password_request(): data = request.json email = data.get('email') # Validate email input if not email: return jsonify({'message': 'Email is required.'}), 400 # Get MySQL database connection conn = get_db_connection() cursor = conn.cursor(dictionary=True) try: # Check if email exists in the account table cursor.execute("SELECT id FROM account WHERE email = %s", (email,)) account = cursor.fetchone() if not account: return jsonify({'message': 'Email not found.'}), 404 # Generate secure token and base64 encode the email token = secrets.token_urlsafe(16) encoded_email = urlsafe_b64encode(email.encode()).decode() # Insert token into SQLite database sqlite_conn = get_sqlite_connection() with sqlite_conn: sqlite_cursor = sqlite_conn.cursor() sqlite_cursor.execute( "INSERT INTO password_reset_tokens (email, token) VALUES (?, ?)", (email, token) ) # Generate reset and disable token links 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 the reset password email send_email( email_template='reset_password', to_email=email, reset_link=reset_link, disable_link=disable_link, subject='Angry Haircraft Password Reset Request' ) return jsonify({'message': 'Password reset link has been sent to your email.'}), 200 except mysql.connector.Error as err: return jsonify({'message': f'Database error: {str(err)}'}), 500 except sqlite3.Error as err: return jsonify({'message': f'SQLite error: {str(err)}'}), 500 except Exception as e: return jsonify({'message': f'An unexpected error occurred: {str(e)}'}), 500 finally: # Ensure both database connections are closed 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() # Log the incoming data to inspect the request logging.debug(f"Incoming POST data: {data}") email = data.get('email') password = data.get('password') token = data.get('token') # Validate input fields if not email or not password or not token: logging.warning("Missing fields in the request data.") return jsonify({'message': 'All fields (email, password, token) are required.'}), 400 # Validate password length if len(password) < 8: logging.warning("Password length is less than 8 characters.") return jsonify({'message': 'Password must be at least 8 characters long.'}), 400 # Attempt to decode base64 email try: decoded_email = urlsafe_b64decode(email.encode()).decode() if is_base64_encoded(email) else email logging.debug(f"Decoded email: {decoded_email}, Token: {token}") except Exception as e: logging.error("Error decoding email", exc_info=True) return jsonify({'message': 'Invalid or expired token.'}), 400 # Validate token from SQLite database try: 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: logging.warning(f"Invalid or expired token for email: {decoded_email}") return jsonify({'message': 'Invalid or expired token.'}), 400 # Delete token after it's been used cursor.execute("DELETE FROM password_reset_tokens WHERE token = ? AND email = ?", (token, decoded_email)) conn.commit() except sqlite3.Error as err: logging.error(f"SQLite error during token validation: {err}") return jsonify({'message': 'Database error occurred while validating the token.'}), 500 finally: cursor.close() conn.close() # Update the password in the MySQL database try: conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT username FROM account WHERE email = %s", (decoded_email,)) account = cursor.fetchone() if not account: logging.warning(f"Account not found for email: {decoded_email}") 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 confirmation email after successful password update send_email( email_template='password_updated', to_email=decoded_email, reset_link='', disable_link='', subject='Password Changed', ) logging.info(f"Password updated successfully for email: {decoded_email}") return jsonify({'message': 'Password updated successfully!'}), 200 except mysql.connector.Error as err: logging.error(f"MySQL error during password update: {err}") return jsonify({'message': f'Database error: {str(err)}'}), 500 finally: cursor.close() conn.close() return jsonify({'message': 'Unexpected error occurred.'}), 500 def send_email(email_template, to_email, reset_link, disable_link, subject): try: config = get_config() from_email = config["SMTP_EMAIL_ADDRESS"] from_password = config["SMTP_EMAIL_PASSWORD"] # Create message container msg = MIMEMultipart('alternative') msg['From'] = from_email msg['To'] = to_email msg['Subject'] = subject if email_template == 'reset_password': text_content = f'Click the link to reset your password: {reset_link}' html_content = f"""
Click the button below to reset your password:
Reset PasswordIf you did not request this email, click the button below to disable the token:
Disable TokenYour Angry Haircraft account has been successfully created!
The username and password to download the game are private/supersecret. Click one of the clients below to get started:
Download the Simple Client Download the HD Client*Note: The HD client can benefit from an extra configuration step. It might also require manual editing of a config file to get resolutions over 1080p. If you don't want that, just get the Simple Client.
Be sure to read the FAQ on www.angry.hair
This is a confirmation that the password for your Angry Haircraft account has been successfully updated.