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-03-04 00:26:12 +00:00
|
|
|
import os
|
|
|
|
|
from werkzeug.utils import secure_filename
|
|
|
|
|
|
|
|
|
|
UPLOAD_FOLDER = "static/uploads"
|
|
|
|
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
2026-02-28 11:38:48 +00:00
|
|
|
|
2026-03-03 23:49:50 +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'
|
2026-03-04 00:26:12 +00:00
|
|
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
2026-03-03 23:07:59 +00:00
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
db = SQLAlchemy(app)
|
2026-03-03 23:49:50 +00:00
|
|
|
gpg = gnupg.GPG()
|
2026-02-28 11:38:48 +00:00
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
COUNTRIES = [ "Afghanistan","Albania","Algeria","Andorra","Angola","Antigua and Barbuda","Argentina",
|
|
|
|
|
"Armenia","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh",
|
|
|
|
|
"Barbados","Belarus","Belgium","Belize","Benin","Bhutan","Bolivia","Bosnia and Herzegovina",
|
|
|
|
|
"Botswana","Brazil","Brunei","Bulgaria","Burkina Faso","Burundi","Cabo Verde","Cambodia",
|
|
|
|
|
"Cameroon","Canada","Central African Republic","Chad","Chile","China","Colombia","Comoros",
|
|
|
|
|
"Congo (Congo-Brazzaville)","Costa Rica","Croatia","Cuba","Cyprus","Czechia (Czech Republic)",
|
|
|
|
|
"Democratic Republic of the Congo","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador",
|
|
|
|
|
"Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Eswatini (fmr. Swaziland)",
|
|
|
|
|
"Ethiopia","Fiji","Finland","France","Gabon","Gambia","Georgia","Germany","Ghana","Greece",
|
|
|
|
|
"Grenada","Guatemala","Guinea","Guinea-Bissau","Guyana","Haiti","Holy See","Honduras","Hungary",
|
|
|
|
|
"Iceland","India","Indonesia","Iran","Iraq","Ireland","Israel","Italy","Jamaica","Japan","Jordan",
|
|
|
|
|
"Kazakhstan","Kenya","Kiribati","Kuwait","Kyrgyzstan","Laos","Latvia","Lebanon","Lesotho",
|
|
|
|
|
"Liberia","Libya","Liechtenstein","Lithuania","Luxembourg","Madagascar","Malawi","Malaysia",
|
|
|
|
|
"Maldives","Mali","Malta","Marshall Islands","Mauritania","Mauritius","Mexico","Micronesia",
|
|
|
|
|
"Moldova","Monaco","Mongolia","Montenegro","Morocco","Mozambique","Myanmar (Burma)","Namibia",
|
|
|
|
|
"Nauru","Nepal","Netherlands","New Zealand","Nicaragua","Niger","Nigeria","North Korea",
|
|
|
|
|
"North Macedonia","Norway","Oman","Pakistan","Palau","Palestine State","Panama","Papua New Guinea",
|
|
|
|
|
"Paraguay","Peru","Philippines","Poland","Portugal","Qatar","Romania","Russia","Rwanda",
|
|
|
|
|
"Saint Kitts and Nevis","Saint Lucia","Saint Vincent and the Grenadines","Samoa","San Marino",
|
|
|
|
|
"Sao Tome and Principe","Saudi Arabia","Senegal","Serbia","Seychelles","Sierra Leone","Singapore",
|
|
|
|
|
"Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Korea","South Sudan",
|
|
|
|
|
"Spain","Sri Lanka","Sudan","Suriname","Sweden","Switzerland","Syria","Tajikistan","Tanzania",
|
|
|
|
|
"Thailand","Timor-Leste","Togo","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan",
|
|
|
|
|
"Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States","Uruguay",
|
|
|
|
|
"Uzbekistan","Vanuatu","Venezuela","Vietnam","Yemen","Zambia","Zimbabwe" ]
|
|
|
|
|
|
|
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
class User(db.Model):
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2026-03-02 01:13:05 +00:00
|
|
|
username = db.Column(db.String(128), unique=True, nullable=False)
|
2026-03-03 23:49:50 +00:00
|
|
|
pgp = db.Column(db.String(4096), nullable=False)
|
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)
|
|
|
|
|
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-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)
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
|
|
|
|
|
def calculate_age(dob: date) -> int:
|
|
|
|
|
today = date.today()
|
|
|
|
|
return today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
|
|
|
|
|
|
|
|
|
|
def save_files(username: str, profile_file, pictures_files):
|
|
|
|
|
user_folder = os.path.join(app.config['UPLOAD_FOLDER'], username)
|
|
|
|
|
os.makedirs(user_folder, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
profile_filename = secure_filename(profile_file.filename)
|
|
|
|
|
profile_path = os.path.join(user_folder, profile_filename)
|
|
|
|
|
profile_file.save(profile_path)
|
|
|
|
|
profile_url = f"/{profile_path.replace(os.sep, '/')}"
|
|
|
|
|
|
|
|
|
|
pictures_urls = []
|
|
|
|
|
for pic in pictures_files:
|
|
|
|
|
if pic.filename:
|
|
|
|
|
filename = secure_filename(pic.filename)
|
|
|
|
|
path = os.path.join(user_folder, filename)
|
|
|
|
|
pic.save(path)
|
|
|
|
|
pictures_urls.append(f"/{path.replace(os.sep, '/')}")
|
|
|
|
|
|
|
|
|
|
return profile_url, pictures_urls
|
|
|
|
|
|
|
|
|
|
def pgp_encrypt_and_import(pgp_key: str, message: str):
|
|
|
|
|
result = gpg.import_keys(pgp_key)
|
|
|
|
|
if not result.fingerprints:
|
|
|
|
|
return None, None
|
|
|
|
|
fingerprint = result.fingerprints[0]
|
|
|
|
|
encrypted = gpg.encrypt(message, recipients=[fingerprint])
|
|
|
|
|
if not encrypted.ok:
|
|
|
|
|
return fingerprint, None
|
|
|
|
|
return fingerprint, str(encrypted)
|
|
|
|
|
|
|
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
@app.route("/")
|
|
|
|
|
def home():
|
|
|
|
|
return render_template("index.html")
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
|
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":
|
2026-03-04 00:41:27 +00:00
|
|
|
data = {key: request.form.get(key) for key in [
|
|
|
|
|
"username","pgp","firstname","lastname","sex","date_of_birth","country","xmpp",
|
|
|
|
|
"email","phone","city","height","weight","race","prefered_age_range"
|
|
|
|
|
]}
|
2026-03-03 23:07:59 +00:00
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
required_fields = ["username","pgp","firstname","lastname","sex","date_of_birth","country","xmpp"]
|
|
|
|
|
if not all(data[f] for f in required_fields):
|
|
|
|
|
flash("Please fill all required fields.")
|
2026-03-03 23:07:59 +00:00
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
for field in ["username","xmpp","email","phone"]:
|
|
|
|
|
if data.get(field) and User.query.filter_by(**{field:data[field]}).first():
|
|
|
|
|
flash(f"{field.capitalize()} already exists.")
|
|
|
|
|
return redirect(url_for("register"))
|
2026-03-03 23:07:59 +00:00
|
|
|
|
|
|
|
|
try:
|
2026-03-04 00:41:27 +00:00
|
|
|
dob = date.fromisoformat(data["date_of_birth"])
|
2026-03-03 23:07:59 +00:00
|
|
|
except ValueError:
|
|
|
|
|
flash("Invalid date format.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
if calculate_age(dob) < 18:
|
2026-03-03 23:07:59 +00:00
|
|
|
flash("You must be at least 18 years old to register.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:26:12 +00:00
|
|
|
profile_file = request.files.get("profile_picture")
|
|
|
|
|
pictures_files = request.files.getlist("pictures")
|
|
|
|
|
|
|
|
|
|
if not profile_file:
|
|
|
|
|
flash("Profile picture is required.")
|
|
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
profile_url, pictures_urls = save_files(data["username"], profile_file, pictures_files)
|
2026-03-03 23:07:59 +00:00
|
|
|
|
|
|
|
|
random_string = secrets.token_hex(16)
|
|
|
|
|
challenge_phrase = f"this is the unencrypted string: {random_string}"
|
2026-03-04 00:41:27 +00:00
|
|
|
fingerprint, encrypted_msg = pgp_encrypt_and_import(data["pgp"], challenge_phrase)
|
|
|
|
|
|
|
|
|
|
if not fingerprint or not encrypted_msg:
|
|
|
|
|
flash("Invalid PGP key or encryption failed.")
|
2026-03-03 23:07:59 +00:00
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
session["pending_user"] = {**data, "profile_url": profile_url, "pictures_urls": pictures_urls}
|
2026-03-03 23:07:59 +00:00
|
|
|
session["pgp_expected_phrase"] = challenge_phrase
|
2026-03-04 00:41:27 +00:00
|
|
|
|
|
|
|
|
return render_template("verify.html", encrypted_message=encrypted_msg)
|
2026-03-03 23:07:59 +00:00
|
|
|
|
2026-03-03 23:49:50 +00:00
|
|
|
return render_template("register.html", countries=COUNTRIES)
|
2026-03-03 19:05:11 +00:00
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
|
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-04 00:41:27 +00:00
|
|
|
if submitted.strip() != expected_phrase:
|
2026-03-03 23:27:33 +00:00
|
|
|
flash("Verification failed. Account not created.")
|
2026-03-03 23:07:59 +00:00
|
|
|
return redirect(url_for("register"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
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_url"],
|
|
|
|
|
pictures=data["pictures_urls"],
|
|
|
|
|
country=data["country"],
|
|
|
|
|
xmpp=data["xmpp"],
|
|
|
|
|
email=data.get("email") or None,
|
|
|
|
|
phone=data.get("phone") or None,
|
|
|
|
|
city=data.get("city") or None,
|
|
|
|
|
height=float(data["height"]) if data.get("height") else None,
|
|
|
|
|
weight=int(data["weight"]) if data.get("weight") else None,
|
|
|
|
|
race=data.get("race") or None,
|
|
|
|
|
prefered_age_range=data.get("prefered_age_range") or None,
|
|
|
|
|
is_verified=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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! Account created.")
|
|
|
|
|
return redirect(url_for("home"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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")
|
2026-03-04 00:41:27 +00:00
|
|
|
pgp_key = request.form.get("pgp")
|
2026-03-03 23:27:33 +00:00
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
if not username or not pgp_key:
|
2026-03-03 23:27:33 +00:00
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
random_string = secrets.token_hex(16)
|
|
|
|
|
challenge_phrase = f"this is the unencrypted string: {random_string}"
|
2026-03-04 00:41:27 +00:00
|
|
|
fingerprint, encrypted_msg = pgp_encrypt_and_import(pgp_key, challenge_phrase)
|
2026-03-03 23:27:33 +00:00
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
if not fingerprint or not encrypted_msg:
|
|
|
|
|
flash("Invalid PGP key or encryption failed.")
|
2026-03-03 23:27:33 +00:00
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
|
|
|
|
session["login_user_id"] = user.id
|
|
|
|
|
session["login_expected_phrase"] = challenge_phrase
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
return render_template("login_verify.html", encrypted_message=encrypted_msg)
|
2026-03-03 23:27:33 +00:00
|
|
|
|
2026-03-03 19:05:11 +00:00
|
|
|
return render_template("login.html")
|
2026-03-03 18:44:17 +00:00
|
|
|
|
2026-03-04 00:41:27 +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"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
if submitted.strip() != expected_phrase:
|
2026-03-03 23:27:33 +00:00
|
|
|
flash("Verification failed")
|
|
|
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
|
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-03-04 00:41:27 +00:00
|
|
|
|
2026-03-03 23:49:50 +00:00
|
|
|
@app.route("/user/<username>")
|
|
|
|
|
def user_profile(username):
|
|
|
|
|
user = User.query.filter_by(username=username).first_or_404()
|
|
|
|
|
return render_template("user.html", user=user, date=date)
|
|
|
|
|
|
2026-03-04 00:41:27 +00:00
|
|
|
|
2026-02-28 11:38:48 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
with app.app_context():
|
|
|
|
|
db.create_all()
|
|
|
|
|
app.run(debug=True)
|