commit c8b2f023d154e78b5caa21fb967806fd2eaa175d
Author: bacalhau <bacalhau@based.pt>
Date: Fri, 13 Mar 2026 22:51:50 +0000
migrated from forgejo
Diffstat:
17 files changed, 1031 insertions(+), 0 deletions(-)
diff --git a/Dockerfile b/Dockerfile
@@ -0,0 +1,19 @@
+FROM python:3.14.3-alpine3.23
+
+WORKDIR /app
+
+COPY requirements.txt .
+
+RUN apk add gpg
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY src/ ./src/
+
+EXPOSE 5000
+
+ENV FLASK_APP=src/main.py
+ENV FLASK_RUN_HOST=0.0.0.0
+ENV FLASK_ENV=production
+
+CMD ["flask", "run"]
diff --git a/README.md b/README.md
@@ -0,0 +1,19 @@
+# Dating-Website
+## Description
+Minimal dating website made in python.
+It uses flask to render the HTML, saves the data in a database and also features an authentication method using PGP where the database stores the user's PGP key's fingerprint and uses that to encrypt a message only you decrypt, but successfully decrypting the message you athenticate to your account.
+It's also supposed to be very easy to use, currently its still in development but the vision is that you can be enganged on the website right from the start featuring a very powerfull search page and not needing an account to use the website.
+This website also does not use JavaScript making it easy to run on any browser.
+
+## Contributing
+If you have suggestions, find bugs, or want to provide code, just open an issue before submiting a PR. I will probably not accept a PR unless I see that it's actually somewhat important, exceptions can be made, but its kinda goofy to write the code before submiting an issue.
+
+## Running the program
+### python enviornment
+`python -m venv venv`
+
+### Install the dependencies
+`pip install -r requirements.txxt`
+
+### Run the program
+`pyhton src/main.py`
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -0,0 +1,14 @@
+services:
+ web:
+ build: .
+ container_name: dating
+ ports:
+ - "5000:5000"
+ volumes:
+ - ./src:/app/src
+ - ./data:/app/data
+ environment:
+ - FLASK_APP=src/main.py
+ - FLASK_RUN_HOST=0.0.0.0
+ - FLASK_ENV=development
+ restart: unless-stopped
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,4 @@
+Flask
+Flask-SQLAlchemy
+SQLAlchemy
+python-gnupg
diff --git a/src/__pycache__/main.cpython-314.pyc b/src/__pycache__/main.cpython-314.pyc
Binary files differ.
diff --git a/src/lovedb.db b/src/lovedb.db
Binary files differ.
diff --git a/src/main.py b/src/main.py
@@ -0,0 +1,453 @@
+from flask import Flask, render_template, request, redirect, url_for, flash, session
+from flask_sqlalchemy import SQLAlchemy
+from sqlalchemy import text
+from datetime import date
+import gnupg
+import secrets
+import os
+from werkzeug.utils import secure_filename
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # sets the base dir as the diretory where the python file is
+UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") # joins the directories
+os.makedirs(UPLOAD_FOLDER, exist_ok=True) # creates the uploads directorie
+
+# configures the app
+app = Flask(__name__) # creates de app
+app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(BASE_DIR, 'lovedb.db')}" # database connection
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # disable track modifications (for better performance)
+app.config['SECRET_KEY'] = 'random' # sets the secret key used to generate random numbers
+app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # sets the upload folder
+
+db = SQLAlchemy(app) # its like a shortcut to the database
+gpg = gnupg.GPG() # same as above but for gpg
+
+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" ]
+
+
+# Database creation
+class User(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(128), unique=True, nullable=False)
+ pgp = db.Column(db.String(4096), nullable=False)
+ 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)
+ date_of_birth = db.Column(db.Date, nullable=False)
+ profile_picture = db.Column(db.String(200), nullable=False)
+ pictures = db.Column(db.JSON, nullable=True)
+ country = db.Column(db.String(128), nullable=False)
+ city = db.Column(db.String(128), nullable=True)
+ height = db.Column(db.Float, nullable=True)
+ weight = db.Column(db.Integer, nullable=True)
+ race = db.Column(db.String(20), nullable=True)
+ prefered_age_range = db.Column(db.String(20), nullable=True)
+ likes = db.Column(db.JSON, nullable=True)
+ dislikes = db.Column(db.JSON, nullable=True)
+ about = db.Column(db.String(4096), nullable=True)
+ xmpp = db.Column(db.String(128), unique=True, nullable=False)
+ email = db.Column(db.String(128), unique=True, nullable=True)
+ phone = db.Column(db.String(20), unique=True, nullable=True)
+ is_verified = db.Column(db.Boolean, default=False)
+
+# calculates user age
+def calculate_age(dob: date) -> int:
+ today = date.today()
+ return today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
+
+# saves files to the upload folder and returns their URL
+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"/static/uploads/{username}/{profile_filename}"
+
+ 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"/static/uploads/{username}/{filename}")
+
+ return profile_url, pictures_urls
+
+# encrypts the chalange for the user to then decrypt with pgp
+def pgp_encrypt_and_import(pgp_key: str, message: str):
+ # imports the user's key
+ result = gpg.import_keys(pgp_key)
+ # check to see if the key has fingerprints
+ if not result.fingerprints:
+ return None, None
+ fingerprint = result.fingerprints[0]
+ # encrypts message to the user's fingerprint
+ encrypted = gpg.encrypt(message, recipients=[fingerprint], always_trust=True)
+ if not encrypted.ok:
+ return fingerprint, None
+ return fingerprint, str(encrypted)
+
+# ROUTES ------------------------------------------------------------------------------------------------------
+
+@app.route("/")
+def home():
+ query = User.query
+
+ country = request.args.get("country")
+ city = request.args.get("city")
+ sex = request.args.get("sex")
+ age_min = request.args.get("age_min")
+ age_max = request.args.get("age_max")
+ race = request.args.get("race")
+ likes = request.args.get("likes")
+ dislikes = request.args.get("dislikes")
+
+ if country:
+ query = query.filter(User.country.ilike(f"%{country}%"))
+ if city:
+ query = query.filter(User.city.ilike(f"%{city}%"))
+ if sex:
+ query = query.filter(User.sex==sex)
+ if race:
+ query = query.filter(User.race.ilike(f"%{race}%"))
+
+ today = date.today()
+ if age_min:
+ try:
+ min_age = int(age_min)
+ dob_max = date(today.year - min_age, today.month, today.day)
+ query = query.filter(User.date_of_birth <= dob_max)
+ except ValueError:
+ pass
+ if age_max:
+ try:
+ max_age = int(age_max)
+ dob_min = date(today.year - max_age - 1, today.month, today.day)
+ query = query.filter(User.date_of_birth >= dob_min)
+ except ValueError:
+ pass
+
+ if likes:
+ likes_list = [x.strip().lower() for x in likes.split(",") if x.strip()]
+ users = query.all()
+ users = [u for u in users if u.likes and all(l in u.likes for l in likes_list)]
+
+ if dislikes:
+ dislikes_list = [x.strip().lower() for x in dislikes.split(",") if x.strip()]
+ for dislike in dislikes_list:
+ query = query.filter(
+ text(f"JSON_CONTAINS(dislikes, '\"{dislike}\"')")
+ )
+
+ users = query.all()
+ return render_template("index.html", users=users, date=date)
+
+@app.route("/register", methods=["GET", "POST"])
+def register():
+ if request.method == "POST":
+ # collect data to a dictionary
+ 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"
+ ]}
+
+ min_age = request.form.get("preferred_age_min")
+ max_age = request.form.get("preferred_age_max")
+
+ if min_age and max_age:
+ try:
+ min_age = int(min_age)
+ max_age = int(max_age)
+
+ if min_age < 18 or max_age < 18:
+ flash("Minimum age is 18.")
+ return redirect(url_for("register"))
+
+ if min_age > max_age:
+ flash("Minimum age cannot be greater than maximum age.")
+ return redirect(url_for("register"))
+
+ data["prefered_age_range"] = f"{min_age}-{max_age}"
+
+ except ValueError:
+ flash("Invalid age range.")
+ return redirect(url_for("register"))
+ else:
+ data["prefered_age_range"] = None
+
+ likes_raw = request.form.get("likes", "")
+ dislikes_raw = request.form.get("dislikes", "")
+
+ data["likes"] = list(set(x.strip().lower() for x in likes_raw.split(",") if x.strip()))
+ data["dislikes"] = list(set(x.strip().lower() for x in dislikes_raw.split(",") if x.strip()))
+
+ # required fields
+ 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.")
+ return redirect(url_for("register"))
+
+ # check if fields are unique
+ 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"))
+
+ # validates date format to iso (YYYY-MM-DD)
+ try:
+ dob = date.fromisoformat(data["date_of_birth"])
+ except ValueError:
+ flash("Invalid date format.")
+ return redirect(url_for("register"))
+
+ # blocks underage users
+ if calculate_age(dob) < 18:
+ flash("You must be at least 18 years old to register.")
+ return redirect(url_for("register"))
+
+ # retrieves the user uploaded pictures
+ profile_file = request.files.get("profile_picture")
+ pictures_files = request.files.getlist("pictures")
+
+ # doesn't let the user create an account without a profile picture
+ if not profile_file:
+ flash("Profile picture is required.")
+ return redirect(url_for("register"))
+
+ # saves the users pictures
+ profile_url, pictures_urls = save_files(data["username"], profile_file, pictures_files)
+
+ # creates a random string
+ random_string = secrets.token_hex(16)
+
+ # uses the string to create the message that wll be encrypted
+ challenge_phrase = f"this is the unencrypted string: {random_string}"
+
+ # encrypts message
+ fingerprint, encrypted_msg = pgp_encrypt_and_import(data["pgp"], challenge_phrase)
+
+
+
+ # checks fingerprint
+ if not fingerprint or not encrypted_msg:
+ flash("Invalid PGP key or encryption failed.")
+ return redirect(url_for("register"))
+ print(fingerprint)
+
+ # creates a temporary session used to verify the user
+ session["pending_user"] = {
+ **data,
+ "profile_url": profile_url,
+ "pictures_urls": pictures_urls,
+ "fingerprint": fingerprint
+ }
+
+ session['pgp_expected_phrase'] = challenge_phrase
+
+ # renders the verification page
+ return render_template("verify.html", encrypted_message=encrypted_msg)
+
+ return render_template("register.html", countries=COUNTRIES)
+
+
+@app.route("/verify", methods=["POST"])
+def verify():
+ # retrieve user data from the session
+ data = session.get("pending_user")
+
+ fingerprint = data.get("fingerprint")
+
+ # retrieve the phrase from the session
+ expected_phrase = session.get("pgp_expected_phrase")
+
+
+ # check to see if data exists
+ if not data or not expected_phrase:
+ flash("Session expired.")
+ return redirect(url_for("register"))
+
+ # get the decrypted message from form
+ submitted = request.form.get("decrypted_message")
+
+ # check to see if submission was empty
+ if not submitted:
+ flash("You must paste the decrypted message.")
+ return redirect(url_for("register"))
+
+ # checks if frase is correct
+ if submitted.strip() != expected_phrase:
+ flash("Verification failed. Account not created.")
+ return redirect(url_for("register"))
+
+ # saves the correcty formated date of birth
+ dob = date.fromisoformat(data["date_of_birth"])
+
+ # stores the data on the database
+ new_user = User(
+ username=data["username"],
+ pgp=fingerprint, # i store the fingerprint not the whole pgp key
+ 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,
+ likes=data.get("likes") or [],
+ dislikes=data.get("dislikes") or [],
+ prefered_age_range=data.get("prefered_age_range") or None,
+ is_verified=True
+ )
+
+ db.session.add(new_user)
+ db.session.commit()
+
+ # creates login session
+ session['user_id'] = new_user.id
+ session['username'] = new_user.username
+
+ # remove temporary session
+ session.pop("pending_user", None)
+
+ flash("PGP verification successful! Account created.")
+ return redirect(url_for("home"))
+
+
+@app.route("/login", methods=["GET","POST"])
+def login():
+ # Requests username and pgp
+ if request.method == "POST":
+ username = request.form.get("username")
+ pgp_key = request.form.get("pgp")
+
+ if not username or not pgp_key:
+ flash("Please enter both username and PGP key.")
+ return redirect(url_for("login"))
+
+ # cehcks if user exists
+ user = User.query.filter_by(username=username).first()
+ if not user:
+ flash("User not found.")
+ return redirect(url_for("login"))
+
+ # checks if imported pgp key has valid fingerprints
+ pgp = gpg.import_keys(pgp_key)
+ if not pgp.fingerprints:
+ flash("Invalid PGP key.")
+ return redirect(url_for("login"))
+
+ # retrieves fingerprint
+ submitted_fingerprint = pgp.fingerprints[0]
+
+ # Checks if pgp matches the user's pgp
+ if submitted_fingerprint != user.pgp:
+ flash("PGP key does not match our records.")
+ return redirect(url_for("login"))
+
+ # Generate a challenge for PGP verification
+ random_string = secrets.token_hex(16)
+ challenge_phrase = f"this is the unencrypted string: {random_string}"
+
+ # Encrypt the challenge phrase using the stored fingerprint
+ encrypted = gpg.encrypt(challenge_phrase, recipients=[submitted_fingerprint])
+ if not encrypted.ok:
+ flash("Encryption failed.")
+ return redirect(url_for("login"))
+
+ # Store login verification data in session (temporary)
+ session["login_user_id"] = user.id
+ session["login_expected_phrase"] = challenge_phrase
+
+ # Render page where user will paste decrypted message
+ return render_template("login_verify.html", encrypted_message=str(encrypted))
+
+ return render_template("login.html")
+
+
+@app.route("/login_verify", methods=["POST"])
+def login_verify():
+ # get the temporary session data
+ user_id = session.get("login_user_id")
+ expected_phrase = session.get("login_expected_phrase")
+
+ # cehcks if session exists
+ if not user_id or not expected_phrase:
+ flash("Login session expired")
+ return redirect(url_for("login"))
+
+ # cehcks if decrypted frase was submited
+ submitted = request.form.get("decrypted_message")
+ if not submitted:
+ flash("You must paste the decrypted message")
+ return redirect(url_for("login"))
+
+ # Checks if submited frase matches the expected
+ if submitted.strip() != expected_phrase:
+ flash("Verification failed")
+ return redirect(url_for("login"))
+
+ # saves session
+ user = User.query.get(user_id)
+ session['user_id'] = user.id
+ session['username'] = user.username
+
+ # removes temporary session
+ session.pop("login_user_id", None)
+ session.pop("login_expected_phrase", None)
+
+ flash("Logged in successfully")
+ return redirect(url_for("home"))
+
+
+@app.route("/logout")
+def logout():
+ # removes session
+ session.pop('user_id', None)
+ session.pop('username', None)
+ flash("Logged out successfully")
+ return redirect(url_for("home"))
+
+# Renders users route
+@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)
+
+
+if __name__ == "__main__":
+ with app.app_context():
+ db.create_all()
+ app.run(debug=True)
diff --git a/src/static/drip.css b/src/static/drip.css
@@ -0,0 +1,154 @@
+@font-face {
+ font-family: 'font';
+ src: url('/static/font/font.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'font';
+ src: url('/static/font/font-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+ font-display: swap;
+}
+
+body {
+ background: #FFE0F4;
+ color: #FF00AA;
+ text-shadow: 0px 0px 5px rgba(255, 0, 170, 0.8);
+ padding: 10px;
+ max-width: 900px;
+ width: 100%;
+ margin: auto;
+ font-family: font;
+ font-weight: normal;
+ line-height: 1.4rem;
+ word-wrap: break-word;
+ font-size: 22px;
+}
+
+footer {
+ padding: 0 0 1.5rem 0;
+ text-align: center;
+ margin-top: auto;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+strong, b {
+ font-weight: bold;
+}
+
+section {
+ margin-top: 32px;
+ background: #fff;
+ padding: 10px;
+ border: medium dashed #FF00AA;
+ border-radius: 5px;
+}
+
+h1 {
+ color: #FF00AA;
+ text-decoration: underline yellow;
+ text-align: center;
+}
+
+h2, h3 {
+ color: #FF00AA;
+}
+
+a {
+ color: #FF699B;
+}
+
+a:hover {
+ color: #ffffff;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ overflow-x: auto;
+ display: block;
+}
+
+th, td {
+ border: 1px solid #ddd;
+ padding: 8px;
+ text-align: left;
+}
+
+th {
+ background-color: #FF00AA;
+ color: white;
+}
+
+tr:nth-child(even) {
+ background-color: #FFB3DA;
+}
+
+@media (max-width: 768px) {
+
+ body {
+ font-size: 20px;
+ padding: 8px;
+ }
+
+ section {
+ padding: 8px;
+ margin-top: 24px;
+ }
+
+ h1 {
+ font-size: 1.8rem;
+ }
+
+ h2 {
+ font-size: 1.4rem;
+ }
+
+ h3 {
+ font-size: 1.2rem;
+ }
+
+}
+
+@media (max-width: 480px) {
+
+ body {
+ font-size: 18px;
+ padding: 6px;
+ line-height: 1.5rem;
+ }
+
+ section {
+ padding: 8px;
+ margin-top: 20px;
+ }
+
+ h1 {
+ font-size: 1.5rem;
+ }
+
+ h2 {
+ font-size: 1.2rem;
+ }
+
+ h3 {
+ font-size: 1.1rem;
+ }
+
+ table {
+ font-size: 14px;
+ }
+
+ th, td {
+ padding: 6px;
+ }
+
+}
diff --git a/src/static/font/font-Bold.ttf b/src/static/font/font-Bold.ttf
Binary files differ.
diff --git a/src/static/font/font.ttf b/src/static/font/font.ttf
Binary files differ.
diff --git a/src/templates/index.html b/src/templates/index.html
@@ -0,0 +1,47 @@
+{% extends "page.html" %}
+
+{% block content %}
+<h2>Discover Users</h2>
+
+<section>
+ <h3>Search Users</h3>
+ <form method="GET" action="{{ url_for('home') }}">
+ <input type="text" name="country" placeholder="Country" value="{{ request.args.get('country', '') }}">
+ <input type="text" name="city" placeholder="City" value="{{ request.args.get('city', '') }}">
+
+ <select name="sex">
+ <option value="">Any Sex</option>
+ <option value="male" {% if request.args.get('sex')=='male' %}selected{% endif %}>Male</option>
+ <option value="female" {% if request.args.get('sex')=='female' %}selected{% endif %}>Female</option>
+ </select>
+
+ <input type="number" name="age_min" placeholder="Min Age" min="18" value="{{ request.args.get('age_min', '') }}">
+ <input type="number" name="age_max" placeholder="Max Age" min="18" value="{{ request.args.get('age_max', '') }}">
+
+ <input type="text" name="race" placeholder="Race" value="{{ request.args.get('race', '') }}">
+ <input type="text" name="likes" placeholder="Likes" value="{{ request.args.get('likes', '') }}">
+ <input type="text" name="dislikes" placeholder="Dislikes" value="{{ request.args.get('dislikes', '') }}">
+
+ <button type="submit">Search</button>
+ </form>
+</section>
+
+<section>
+ <h3>Users</h3>
+ {% if users %}
+ <div>
+ {% for user in users %}
+ <div>
+ <a href="{{ url_for('user_profile', username=user.username) }}">
+ <img src="{{ user.profile_picture }}" alt="{{ user.username }}" width="100"><br>
+ Age: {{ (date.today() - user.date_of_birth).days // 365 }}<br>
+ Country: {{ user.country }}
+ </a>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <p>No users found.</p>
+ {% endif %}
+</section>
+{% endblock %}
diff --git a/src/templates/login.html b/src/templates/login.html
@@ -0,0 +1,35 @@
+{% extends "page.html" %}
+
+{% block content %}
+<h2>Login</h2>
+<p>Enter your username and PGP public key to receive a verification challenge.</p>
+
+<form method="POST" action="{{ url_for('login') }}">
+
+ <fieldset>
+ <legend>Account Verification</legend>
+
+ <label for="username">Username</label><br>
+ <input type="text" id="username" name="username" required><br><br>
+
+ <label for="pgp">PGP Public Key</label><br>
+ <textarea id="pgp" name="pgp" rows="8" required></textarea><br>
+ <small>Paste your full public key block</small>
+ </fieldset>
+
+ <br>
+ <button type="submit">Send Challenge</button>
+</form>
+
+<!-- Flash messages -->
+{% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul>
+ {% for message in messages %}
+ <li style="color:red;">{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endwith %}
+
+{% endblock %}
diff --git a/src/templates/login_verify.html b/src/templates/login_verify.html
@@ -0,0 +1,14 @@
+{% extends "page.html" %}
+
+{% block content %}
+<h2>Decrypt Challenge</h2>
+<p>Copy the message below, decrypt it with your PGP private key, and paste the decrypted message into the box.</p>
+
+<textarea rows="10" cols="60" readonly>{{ encrypted_message }}</textarea>
+
+<form method="POST" action="{{ url_for('login_verify') }}">
+ <label>Decrypted Message:</label><br>
+ <textarea name="decrypted_message" rows="4" cols="60" required></textarea><br><br>
+ <button type="submit">Verify</button>
+</form>
+{% endblock %}
diff --git a/src/templates/page.html b/src/templates/page.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Dating Website</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='drip.css') }}">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<header><h1>Dating Website</h1></header>
+<nav>
+
+ <a href="{{ url_for('home') }}">Home</a>
+
+ {% if session.get('username') %}
+ <span>Welcome, {{ session['username'] }}!</span>
+ <a href="{{ url_for('logout') }}">Logout</a>
+ {% else %}
+ <a href="{{ url_for('login') }}">Login</a>
+ <a href="{{ url_for('register') }}">Register</a>
+ {% endif %}
+</nav>
+
+<body>
+ {% block content %}{% endblock %}
+<footer>Dating Website</footer>
+
+</body>
+</html>
diff --git a/src/templates/register.html b/src/templates/register.html
@@ -0,0 +1,131 @@
+{% extends "page.html" %}
+
+{% block content %}
+<h2>Register</h2>
+
+<form method="POST" enctype="multipart/form-data">
+
+ <fieldset>
+ <legend>Account (required)</legend>
+
+ <label for="username">Username</label><br>
+ <input type="text" id="username" name="username" required><br><br>
+
+ <label for="pgp">PGP Public Key</label><br>
+ <textarea id="pgp" name="pgp" rows="6" required></textarea>
+ </fieldset>
+
+ <fieldset>
+ <legend>Personal Info (required)</legend>
+
+ <label for="firstname">First Name</label><br>
+ <input type="text" id="firstname" name="firstname" required><br><br>
+
+ <label for="lastname">Last Name</label><br>
+ <input type="text" id="lastname" name="lastname" required><br><br>
+
+ <label for="sex">Sex</label><br>
+ <select id="sex" name="sex" required>
+ <option value="">Select Sex</option>
+ <option value="male">Male</option>
+ <option value="female">Female</option>
+ </select><br><br>
+
+ <label for="date_of_birth">Date of Birth</label><br>
+ <input type="date" id="date_of_birth" name="date_of_birth" required>
+ </fieldset>
+
+ <fieldset>
+ <legend>Pictures</legend>
+
+ <label for="profile_picture">Profile Picture (required)</label><br>
+ <input type="file" id="profile_picture" name="profile_picture" accept="image/*" required><br><br>
+
+ <label for="pictures">Other Pictures (optional)</label><br>
+ <input type="file" id="pictures" name="pictures" accept="image/*" multiple>
+ </fieldset>
+
+ <fieldset>
+ <legend>Location</legend>
+
+ <label for="country">Country</label><br>
+ <select id="country" name="country" required>
+ <option value="">Select Country</option>
+ {% for c in countries %}
+ <option value="{{ c }}">{{ c }}</option>
+ {% endfor %}
+ </select><br><br>
+
+ <label for="city">City</label><br>
+ <input type="text" id="city" name="city">
+ </fieldset>
+
+ <fieldset>
+ <legend>Physical Attributes</legend>
+
+ <label for="height">Height (meters)</label><br>
+ <input type="number" step="0.01" id="height" name="height"><br><br>
+
+ <label for="weight">Weight (kg)</label><br>
+ <input type="number" id="weight" name="weight"><br><br>
+
+ <label for="race">Race</label><br>
+ <select id="race" name="race">
+ <option value="">Select race</option>
+ <option value="Latino(a)">Latino</option>
+ <option value="Asian">Asian</option>
+ <option value="White">White</option>
+ <option value="Black">Black</option>
+ <option value="Brown">Brown</option>
+ <option value="Native American">Native American</option>
+ </select>
+ </fieldset>
+
+ <fieldset>
+ <legend>Preferences</legend>
+
+ <label>Preferred Age Range</label><br>
+
+ <label for="preferred_age_min">Minimum Age</label><br>
+ <input type="number" id="preferred_age_min" name="preferred_age_min" min="18" max="120"><br>
+
+ <label for="preferred_age_max">Maximum Age</label><br>
+ <input type="number" id="preferred_age_max" name="preferred_age_max" min="18" max="120"><br>
+
+ <label for="likes">Things You Like</label><br>
+ <input type="text" id="likes" name="likes" placeholder="music, travel, coding"><br>
+ <small>Separate with commas</small><br><br>
+
+ <label for="dislikes">Things You Don't Like</label><br>
+ <input type="text" id="dislikes" name="dislikes" placeholder="smoking, drama"><br>
+ <small>Separate with commas</small>
+ </fieldset>
+
+ <fieldset>
+ <legend>Contacts (required)</legend>
+
+ <label for="xmpp">XMPP</label><br>
+ <input type="text" id="xmpp" name="xmpp" required><br><br>
+
+ <label for="email">Email</label><br>
+ <input type="email" id="email" name="email"><br><br>
+
+ <label for="phone">Phone</label><br>
+ <input type="text" id="phone" name="phone">
+ </fieldset>
+
+ <br>
+ <button type="submit">Register</button>
+</form>
+
+{% with messages = get_flashed_messages() %}
+ {% if messages %}
+ <ul>
+ {% for message in messages %}
+ <li style="color:red;">{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endwith %}
+
+{% endblock %}
diff --git a/src/templates/user.html b/src/templates/user.html
@@ -0,0 +1,97 @@
+{% extends "page.html" %}
+
+{% block content %}
+
+<section>
+ <h2>{{ user.firstname }} {{ user.lastname }}</h2>
+
+ <img src="{{ user.profile_picture }}" alt="Profile Picture" style="max-width:200px; border-radius:8px;"><br><br>
+
+ <p><strong>Age:</strong> {{ (date.today() - user.date_of_birth).days // 365 }}</p>
+ <p><strong>Sex:</strong> {{ user.sex|capitalize }}</p>
+</section>
+
+
+{% if user.pictures %}
+<section>
+ <h3>Gallery</h3>
+ {% for pic in user.pictures %}
+ <img src="{{ pic }}" style="max-width:120px; margin:5px; border-radius:6px;">
+ {% endfor %}
+</section>
+{% endif %}
+
+
+<section>
+ <h3>Personal Info</h3>
+ <p><strong>Date of Birth:</strong> {{ user.date_of_birth }}</p>
+ <p><strong>Race:</strong> {{ user.race or 'Not specified' }}</p>
+</section>
+
+
+<section>
+ <h3>Location</h3>
+ <p><strong>Country:</strong> {{ user.country }}</p>
+ <p><strong>City:</strong> {{ user.city or 'Not specified' }}</p>
+</section>
+
+
+<section>
+ <h3>Physical Attributes</h3>
+ <p><strong>Height:</strong>
+ {% if user.height %}
+ {{ user.height }} m
+ {% else %}
+ Not specified
+ {% endif %}
+ </p>
+
+ <p><strong>Weight:</strong>
+ {% if user.weight %}
+ {{ user.weight }} kg
+ {% else %}
+ Not specified
+ {% endif %}
+ </p>
+</section>
+
+
+<section>
+ <h3>Preferences</h3>
+
+ <p><strong>Preferred Age Range:</strong>
+ {{ user.prefered_age_range or 'Not specified' }}
+ </p>
+
+ <p><strong>Likes:</strong></p>
+ {% if user.likes %}
+ <ul>
+ {% for like in user.likes %}
+ <li>{{ like }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>Not specified</p>
+ {% endif %}
+
+ <p><strong>Dislikes:</strong></p>
+ {% if user.dislikes %}
+ <ul>
+ {% for dislike in user.dislikes %}
+ <li>{{ dislike }}</li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p>Not specified</p>
+ {% endif %}
+</section>
+
+
+<section>
+ <h3>Contacts</h3>
+ <p><strong>XMPP:</strong> {{ user.xmpp }}</p>
+ <p><strong>Email:</strong> {{ user.email or 'Not specified' }}</p>
+ <p><strong>Phone:</strong> {{ user.phone or 'Not specified' }}</p>
+</section>
+
+{% endblock %}
diff --git a/src/templates/verify.html b/src/templates/verify.html
@@ -0,0 +1,17 @@
+{% extends "page.html" %}
+
+{% block content %}
+<h2>PGP Verification</h2>
+
+<p>Decrypt the message below using your private key and paste the result.</p>
+
+<textarea rows="10" cols="80" readonly>
+{{ encrypted_message }}
+</textarea>
+
+<form method="POST" action="{{ url_for('verify') }}">
+ <textarea name="decrypted_message" placeholder="Paste decrypted message here" required></textarea><br>
+ <button type="submit">Verify</button>
+</form>
+
+{% endblock %}