2026-03-03 23:07:59 +00:00
|
|
|
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
2026-02-28 11:38:48 +00:00
|
|
|
from flask_sqlalchemy import SQLAlchemy
|
2026-03-03 18:44:17 +00:00
|
|
|
from datetime import date
|
2026-03-03 23:07:59 +00:00
|
|
|
import gnupg
|
|
|
|
|
import secrets
|
2026-02-28 11:38:48 +00:00
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
2026-03-03 18:44:17 +00:00
|
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://love:love@localhost:3309/lovedb'
|
2026-02-28 11:38:48 +00:00
|
|
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
2026-03-03 23:07:59 +00:00
|
|
|
app.config['SECRET_KEY'] = 'random'
|
|
|
|
|
|
|
|
|
|
gpg = gnupg.GPG()
|
2026-02-28 11:38:48 +00:00
|
|
|
|
|
|
|
|
db = SQLAlchemy(app)
|
|
|
|
|
|
|
|
|
|
class User(db.Model):
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2026-03-03 18:44:17 +00:00
|
|
|
|
2026-03-02 01:13:05 +00:00
|
|
|
username = db.Column(db.String(128), unique=True, nullable=False)
|
2026-03-03 23:27:33 +00:00
|
|
|
pgp = db.Column(db.String(8128), nullable=False)
|
2026-02-28 11:38:48 +00:00
|
|
|
|
2026-03-03 18:44:17 +00:00
|
|
|
firstname = db.Column(db.String(128), nullable=False)
|
|
|
|
|
lastname = db.Column(db.String(128), nullable=False)
|
|
|
|
|
|
|
|
|
|
sex = db.Column(db.Enum('male', 'female'), nullable=False)
|
2026-02-28 11:38:48 +00:00
|
|
|
date_of_birth = db.Column(db.Date, nullable=False)
|
2026-03-03 18:44:17 +00:00
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
profile_picture = db.Column(db.String(200), nullable=False)
|
2026-03-03 18:44:17 +00:00
|
|
|
|
|
|
|
|
pictures = db.Column(db.JSON, nullable=True)
|
|
|
|
|
|
|
|
|
|
country = db.Column(db.String(128), nullable=False)
|
|
|
|
|
city = db.Column(db.String(128), nullable=True)
|
2026-02-28 11:38:48 +00:00
|
|
|
|
|
|
|
|
height = db.Column(db.Float, nullable=True)
|
|
|
|
|
weight = db.Column(db.Integer, nullable=True)
|
2026-03-03 18:44:17 +00:00
|
|
|
race = db.Column(db.String(20), nullable=True)
|
2026-02-28 11:38:48 +00:00
|
|
|
|
|
|
|
|
prefered_age_range = db.Column(db.String(20), nullable=True)
|
2026-03-03 18:44:17 +00:00
|
|
|
|
|
|
|
|
likes = db.Column(db.JSON, nullable=True)
|
|
|
|
|
dislikes = db.Column(db.JSON, nullable=True)
|
2026-02-28 11:38:48 +00:00
|
|
|
|
2026-03-02 01:13:05 +00:00
|
|
|
xmpp = db.Column(db.String(128), unique=True, nullable=False)
|
|
|
|
|
email = db.Column(db.String(128), unique=True, nullable=True)
|
2026-02-28 11:38:48 +00:00
|
|
|
phone = db.Column(db.String(20), unique=True, nullable=True)
|
|
|
|
|
|
|
|
|
|
is_verified = db.Column(db.Boolean, default=False)
|
|
|
|
|
|
|
|
|
|
@app.route("/")
|
|
|
|
|
def home():
|
|
|
|
|
return render_template("index.html")
|
|
|
|
|
|
2026-03-03 23:07:59 +00:00
|
|
|
@app.route("/register", methods=["GET", "POST"])
|
2026-03-03 19:05:11 +00:00
|
|
|
def register():
|
2026-03-03 23:07:59 +00:00
|
|
|
if request.method == "POST":
|
|
|
|
|
|
|
|
|
|
username = request.form.get("username")
|
|
|
|
|
pgp = request.form.get("pgp")
|
|
|
|
|
firstname = request.form.get("firstname")
|
|
|
|
|
lastname = request.form.get("lastname")
|
|
|
|
|
sex = request.form.get("sex")
|
|
|
|
|
date_of_birth = request.form.get("date_of_birth")
|
|
|
|
|
profile_picture = request.form.get("profile_picture")
|
|
|
|
|
country = request.form.get("country")
|
|
|
|
|
xmpp = request.form.get("xmpp")
|
|
|
|
|
|
|
|
|
|
email = request.form.get("email")
|
|
|
|
|
phone = request.form.get("phone")
|
|
|
|
|
city = request.form.get("city")
|
|
|
|
|
height = request.form.get("height")
|
|
|
|
|
weight = request.form.get("weight")
|
|
|
|
|
race = request.form.get("race")
|
|
|
|
|
prefered_age_range = request.form.get("prefered_age_range")
|
|
|
|
|
|
|
|
|
|
if not all([username, pgp, firstname, lastname, sex,
|
|
|
|
|
date_of_birth, profile_picture, country, xmpp]):
|
|
|
|
|
flash("Please fill all required fields.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
if User.query.filter_by(username=username).first():
|
|
|
|
|
flash("Username already exists.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
if User.query.filter_by(xmpp=xmpp).first():
|
|
|
|
|
flash("XMPP already exists.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
if email and User.query.filter_by(email=email).first():
|
|
|
|
|
flash("Email already exists.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
if phone and User.query.filter_by(phone=phone).first():
|
|
|
|
|
flash("Phone already exists.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
dob = date.fromisoformat(date_of_birth)
|
|
|
|
|
except ValueError:
|
|
|
|
|
flash("Invalid date format.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
today = date.today()
|
|
|
|
|
age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
|
|
|
|
|
if age < 18:
|
|
|
|
|
flash("You must be at least 18 years old to register.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
session["pending_user"] = {
|
|
|
|
|
"username": username,
|
|
|
|
|
"pgp": pgp,
|
|
|
|
|
"firstname": firstname,
|
|
|
|
|
"lastname": lastname,
|
|
|
|
|
"sex": sex,
|
|
|
|
|
"date_of_birth": date_of_birth,
|
|
|
|
|
"profile_picture": profile_picture,
|
|
|
|
|
"country": country,
|
|
|
|
|
"xmpp": xmpp,
|
|
|
|
|
"email": email,
|
|
|
|
|
"phone": phone,
|
|
|
|
|
"city": city,
|
|
|
|
|
"height": height,
|
|
|
|
|
"weight": weight,
|
|
|
|
|
"race": race,
|
|
|
|
|
"prefered_age_range": prefered_age_range
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
import_result = gpg.import_keys(pgp)
|
|
|
|
|
if not import_result.fingerprints:
|
|
|
|
|
flash("Invalid PGP key. Make sure you pasted the full ASCII-armored public key.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
fingerprint = import_result.fingerprints[0]
|
|
|
|
|
|
|
|
|
|
random_string = secrets.token_hex(16)
|
|
|
|
|
challenge_phrase = f"this is the unencrypted string: {random_string}"
|
|
|
|
|
|
|
|
|
|
encrypted_data = gpg.encrypt(
|
|
|
|
|
challenge_phrase,
|
|
|
|
|
recipients=[fingerprint]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not encrypted_data.ok:
|
|
|
|
|
flash("Failed to encrypt challenge. Check your PGP key.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
session["pgp_expected_phrase"] = challenge_phrase
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
"verify.html",
|
|
|
|
|
encrypted_message=str(encrypted_data)
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-03 19:05:11 +00:00
|
|
|
return render_template("register.html")
|
|
|
|
|
|
2026-03-03 23:07:59 +00:00
|
|
|
@app.route("/verify", methods=["POST"])
|
|
|
|
|
def verify():
|
|
|
|
|
expected_phrase = session.get("pgp_expected_phrase")
|
|
|
|
|
data = session.get("pending_user")
|
|
|
|
|
|
2026-03-03 23:27:33 +00:00
|
|
|
if not data or not expected_phrase:
|
|
|
|
|
flash("Session expired.")
|
2026-03-03 23:07:59 +00:00
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
|
|
|
|
submitted = request.form.get("decrypted_message")
|
|
|
|
|
if not submitted:
|
|
|
|
|
flash("You must paste the decrypted message.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-03 23:27:33 +00:00
|
|
|
if submitted.strip() == expected_phrase:
|
|
|
|
|
dob = date.fromisoformat(data["date_of_birth"])
|
|
|
|
|
|
|
|
|
|
new_user = User(
|
|
|
|
|
username=data["username"],
|
|
|
|
|
pgp=data["pgp"],
|
|
|
|
|
firstname=data["firstname"],
|
|
|
|
|
lastname=data["lastname"],
|
|
|
|
|
sex=data["sex"],
|
|
|
|
|
date_of_birth=dob,
|
|
|
|
|
profile_picture=data["profile_picture"],
|
|
|
|
|
country=data["country"],
|
|
|
|
|
xmpp=data["xmpp"],
|
|
|
|
|
email=data["email"] or None,
|
|
|
|
|
phone=data["phone"] or None,
|
|
|
|
|
city=data["city"] or None,
|
|
|
|
|
height=float(data["height"]) if data["height"] else None,
|
|
|
|
|
weight=int(data["weight"]) if data["weight"] else None,
|
|
|
|
|
race=data["race"] or None,
|
|
|
|
|
prefered_age_range=data["prefered_age_range"] or None,
|
|
|
|
|
is_verified=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
db.session.add(new_user)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
session['user_id'] = new_user.id
|
|
|
|
|
session['username'] = new_user.username
|
|
|
|
|
|
|
|
|
|
session.pop("pending_user", None)
|
|
|
|
|
session.pop("pgp_expected_phrase", None)
|
|
|
|
|
|
|
|
|
|
flash("PGP verification successful!")
|
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
flash("Verification failed. Account not created.")
|
2026-03-03 23:07:59 +00:00
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-03 23:27:33 +00:00
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
2026-03-03 19:05:11 +00:00
|
|
|
def login():
|
2026-03-03 23:27:33 +00:00
|
|
|
if request.method == "POST":
|
|
|
|
|
username = request.form.get("username")
|
|
|
|
|
pgp = request.form.get("pgp")
|
|
|
|
|
|
|
|
|
|
if not username or not pgp:
|
|
|
|
|
flash("Please enter both username and PGP key.")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
user = User.query.filter_by(username=username).first()
|
|
|
|
|
if not user:
|
|
|
|
|
flash("User not found.")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
import_result = gpg.import_keys(pgp)
|
|
|
|
|
if not import_result.fingerprints:
|
|
|
|
|
flash("Invalid PGP key.")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
fingerprint = import_result.fingerprints[0]
|
|
|
|
|
|
|
|
|
|
random_string = secrets.token_hex(16)
|
|
|
|
|
challenge_phrase = f"this is the unencrypted string: {random_string}"
|
|
|
|
|
|
|
|
|
|
encrypted_data = gpg.encrypt(
|
|
|
|
|
challenge_phrase,
|
|
|
|
|
recipients=[fingerprint]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not encrypted_data.ok:
|
|
|
|
|
flash("Failed to encrypt challenge.")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
session["login_user_id"] = user.id
|
|
|
|
|
session["login_expected_phrase"] = challenge_phrase
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
"login_verify.html",
|
|
|
|
|
encrypted_message=str(encrypted_data)
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-03 19:05:11 +00:00
|
|
|
return render_template("login.html")
|
2026-03-03 18:44:17 +00:00
|
|
|
|
2026-03-03 23:27:33 +00:00
|
|
|
@app.route("/login_verify", methods=["POST"])
|
|
|
|
|
def login_verify():
|
|
|
|
|
user_id = session.get("login_user_id")
|
|
|
|
|
expected_phrase = session.get("login_expected_phrase")
|
|
|
|
|
|
|
|
|
|
if not user_id or not expected_phrase:
|
|
|
|
|
flash("Login session expired")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
submitted = request.form.get("decrypted_message")
|
|
|
|
|
if not submitted:
|
|
|
|
|
flash("You must paste the decrypted message")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
if submitted.strip() == expected_phrase:
|
|
|
|
|
user = User.query.get(user_id)
|
|
|
|
|
session['user_id'] = user.id
|
|
|
|
|
session['username'] = user.username
|
|
|
|
|
|
|
|
|
|
session.pop("login_user_id", None)
|
|
|
|
|
session.pop("login_expected_phrase", None)
|
|
|
|
|
|
|
|
|
|
flash("Logged in successfully")
|
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
|
else:
|
|
|
|
|
flash("Verification failed")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
2026-03-03 23:07:59 +00:00
|
|
|
@app.route("/logout")
|
|
|
|
|
def logout():
|
|
|
|
|
session.pop('user_id', None)
|
|
|
|
|
session.pop('username', None)
|
|
|
|
|
flash("Logged out successfully")
|
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
with app.app_context():
|
|
|
|
|
db.create_all()
|
|
|
|
|
app.run(debug=True)
|