dating-website

Log | Files | Refs | README

commit c8b2f023d154e78b5caa21fb967806fd2eaa175d
Author: bacalhau <bacalhau@based.pt>
Date:   Fri, 13 Mar 2026 22:51:50 +0000

migrated from forgejo

Diffstat:
ADockerfile | 19+++++++++++++++++++
AREADME.md | 19+++++++++++++++++++
Adocker-compose.yml | 14++++++++++++++
Arequirements.txt | 4++++
Asrc/__pycache__/main.cpython-314.pyc | 0
Asrc/lovedb.db | 0
Asrc/main.py | 453+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/static/drip.css | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/static/font/font-Bold.ttf | 0
Asrc/static/font/font.ttf | 0
Asrc/templates/index.html | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/templates/login.html | 35+++++++++++++++++++++++++++++++++++
Asrc/templates/login_verify.html | 14++++++++++++++
Asrc/templates/page.html | 27+++++++++++++++++++++++++++
Asrc/templates/register.html | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/templates/user.html | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/templates/verify.html | 17+++++++++++++++++
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 %}