diff --git a/.gitignore b/.gitignore index 759ae93..0f0c103 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ data -src/static/uploads/* +venv +src/static/uploads diff --git a/README.md b/README.md index f967df8..b638102 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -Minimal dating website writen in python +# Dating-Website +## Description +Minimal dating website made in python. +It uses flask to render the HTML, saves the data in a MySQL 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. + +## TODO +- making the website responsive +- adding search features +- likes and dislikes +- cool css +- a grid with all of the users on the index page (where the search will also be) +- security audits +- maybe more stuff later... + +## 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 +#### Docker/Podman +`docker compose up -d` / `podman-compose up -d` + +### python enviornment +`python -m venv venv` + +### Install the dependencies +`pip install -r requirements.txxt` + +### Run the program +`pyhton src/main.py` diff --git a/src/main.py b/src/main.py index 3a65e68..47f97ef 100644 --- a/src/main.py +++ b/src/main.py @@ -1,14 +1,15 @@ 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 -# defines where the upload folder is and creates it -UPLOAD_FOLDER = "static/uploads" -os.makedirs(UPLOAD_FOLDER, exist_ok=True) +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 @@ -78,26 +79,22 @@ def calculate_age(dob: date) -> int: # saves files to the upload folder and returns their URL def save_files(username: str, profile_file, pictures_files): - # creates a path for the user inside the upload forlder user_folder = os.path.join(app.config['UPLOAD_FOLDER'], username) os.makedirs(user_folder, exist_ok=True) - # prevents unsafe characters to be used in the filename profile_filename = secure_filename(profile_file.filename) profile_path = os.path.join(user_folder, profile_filename) - - # saves the profile picture to the path profile_file.save(profile_path) - profile_url = f"/{profile_path.replace(os.sep, '/')}" - # saves all of the other pictures + 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"/{path.replace(os.sep, '/')}") + pictures_urls.append(f"/static/uploads/{username}/{filename}") return profile_url, pictures_urls @@ -119,8 +116,58 @@ def pgp_encrypt_and_import(pgp_key: str, message: str): @app.route("/") def home(): - return render_template("index.html") + 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()] + for like in likes_list: + query = query.filter( + text(f"JSON_CONTAINS(likes, '\"{like}\"')") + ) + + 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(): @@ -128,9 +175,39 @@ def register(): # 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","prefered_age_range" + "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): @@ -169,19 +246,30 @@ def register(): # 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} - session["pgp_expected_phrase"] = challenge_phrase + 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) @@ -191,18 +279,23 @@ def register(): @app.route("/verify", methods=["POST"]) def verify(): - # retrieve the phrase from the session - expected_phrase = session.get("pgp_expected_phrase") # 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 + # 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.") @@ -234,6 +327,8 @@ def verify(): 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 ) @@ -244,9 +339,9 @@ def verify(): # creates login session session['user_id'] = new_user.id session['username'] = new_user.username + # remove temporary session session.pop("pending_user", None) - session.pop("pgp_expected_phrase", None) flash("PGP verification successful! Account created.") return redirect(url_for("home")) diff --git a/src/static/drip.css b/src/static/drip.css index 3f25630..e48c8aa 100644 --- a/src/static/drip.css +++ b/src/static/drip.css @@ -1,28 +1,30 @@ @font-face { font-family: 'font'; - src: url('/font/font.ttf') format('truetype'); + src: url('/static/font/font.ttf') format('truetype'); font-weight: normal; font-style: normal; font-display: swap; } @font-face { font-family: 'font'; - src: url('/font/font-Bold.ttf') format('truetype'); + src: url('/static/font/font-Bold.ttf') format('truetype'); font-weight: bold; font-style: normal; font-display: swap; } body { - background: #101210; - color: #e0e0e0; - text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.8); - max-width: 800px; + background: #FFE0F4; + color: #FF00AA; + text-shadow: 0px 0px 5px rgba(255, 0, 170, 0.8); + padding: 5px; + max-width: 75%; margin: auto; font-family: font; font-weight: normal; line-height: 1.2rem; word-wrap: break-word; + font-size: 22px; } footer { @@ -42,44 +44,55 @@ strong, b { font-weight: bold; } +section { + margin-top: 32px; + background: #fff; + padding: 5px; + border: medium; + border-color: #FF00AA; + border-radius: 5px; + border-style: dashed; +} + h1 { - color: #00ff00; + color: #FF00AA; text-decoration: underline yellow; text-align: center; } h2 { - color: #00ff00; + color: #FF00AA; } h3 { - color: #00ff00; + color: #FF00AA; } a { - color: #ffff00; + color: #FF699B; } a:hover { color: #ffffff; } -summary { - color: #008800; - background: #101210; +table { + width: 100%; + border-collapse: collapse; } -details { - background: #222; +th, td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; } -summary:hover { - color: #fff; - cursor: pointer; +th { + background-color: #FF00AA; + color: white; } -.service { - padding: 0.5rem; - border: solid thin #ffff00; +tr:nth-child(even) { + background-color: #FF00AA; } diff --git a/src/font/font-Bold.ttf b/src/static/font/font-Bold.ttf similarity index 100% rename from src/font/font-Bold.ttf rename to src/static/font/font-Bold.ttf diff --git a/src/font/font.ttf b/src/static/font/font.ttf similarity index 100% rename from src/font/font.ttf rename to src/static/font/font.ttf diff --git a/src/templates/index.html b/src/templates/index.html index 8bd4d52..575c8e6 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -1,15 +1,47 @@ {% extends "page.html" %} {% block content %} -

Home

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

-

Page text

+

Discover Users

+ +
+

Search Users

+
+ + + + + + + + + + + + + +
+
+ +
+

Users

+ {% if users %} +
+ {% for user in users %} +
+ + {{ user.username }}
+ Age: {{ (date.today() - user.date_of_birth).days // 365 }}
+ Country: {{ user.country }} +
+
+ {% endfor %} +
+ {% else %} +

No users found.

+ {% endif %} +
{% endblock %} diff --git a/src/templates/login.html b/src/templates/login.html index f0b5116..0fe3b7b 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -2,15 +2,34 @@ {% block content %}

Login

-

Enter your username and PGP public key to receive a challenge.

+

Enter your username and PGP public key to receive a verification challenge.

-
-

-
-

+
+ Account Verification - +
+

+ +
+
+ Paste your full public key block +
+ +
+
+ + +{% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} +{% endwith %} + {% endblock %} diff --git a/src/templates/page.html b/src/templates/page.html index 0f95a01..9cd5589 100644 --- a/src/templates/page.html +++ b/src/templates/page.html @@ -19,9 +19,7 @@ - -{% block content %}{% endblock %} - + {% block content %}{% endblock %} diff --git a/src/templates/register.html b/src/templates/register.html index 8cf7492..70945ae 100644 --- a/src/templates/register.html +++ b/src/templates/register.html @@ -5,52 +5,117 @@
-

Identity (required)

-
-
+
+ Account (required) -

Personal Info (required)

-
-
+
+

- +
+ +
+ Personal Info (required) + +
+

+ +
+

+ +
+
+

-
+
+ +
-

Profile Picture (required)

-
+
+ Pictures -

Other Pictures (optional, multiple)

-
+
+

-

Location (required)

- +
+ +
+ Location + +
+
-
+

-

Physical Attributes (optional)

-
-
-
+
+ +
-

Preferences (optional)

-
+
+ Physical Attributes -

Contacts (required)

-
-
-

+
+

- +
+

+
+ +
+ +
+ Preferences + +
+ +
+
+ +
+
+ +
+
+ Separate with commas

+ +
+
+ Separate with commas +
+ +
+ Contacts (required) + +
+

+ +
+

+ +
+ +
+ +
+
{% with messages = get_flashed_messages() %} diff --git a/src/templates/user.html b/src/templates/user.html index cfa71bf..eae8f80 100644 --- a/src/templates/user.html +++ b/src/templates/user.html @@ -1,37 +1,97 @@ {% extends "page.html" %} {% block content %} -

{{ user.firstname }} {{ user.lastname }}

-Profile Picture
+ +
+

{{ user.firstname }} {{ user.lastname }}

+ + Profile Picture

+ +

Age: {{ (date.today() - user.date_of_birth).days // 365 }}

+

Sex: {{ user.sex|capitalize }}

+
+ {% if user.pictures %} -

Pictures

-{% for pic in user.pictures %} - -{% endfor %} +
+

Gallery

+ {% for pic in user.pictures %} + + {% endfor %} +
{% endif %} -

Personal Info

-

Sex: {{ user.sex }}

-

Date of Birth: {{ user.date_of_birth }}

-

Age: {{ (date.today() - user.date_of_birth).days // 365 }}

-

Race: {{ user.race or 'Not specified' }}

-

Location

-

Country: {{ user.country }}

-

City: {{ user.city or 'Not specified' }}

+
+

Personal Info

+

Date of Birth: {{ user.date_of_birth }}

+

Race: {{ user.race or 'Not specified' }}

+
-

Physical Attributes

-

Height: {{ user.height or 'Not specified' }} m

-

Weight: {{ user.weight or 'Not specified' }} kg

-

Preferences

-

Preferred Age Range: {{ user.prefered_age_range or 'Not specified' }}

-

Likes: {{ user.likes | join(', ') if user.likes else 'Not specified' }}

-

Dislikes: {{ user.dislikes | join(', ') if user.dislikes else 'Not specified' }}

+
+

Location

+

Country: {{ user.country }}

+

City: {{ user.city or 'Not specified' }}

+
+ + +
+

Physical Attributes

+

Height: + {% if user.height %} + {{ user.height }} m + {% else %} + Not specified + {% endif %} +

+ +

Weight: + {% if user.weight %} + {{ user.weight }} kg + {% else %} + Not specified + {% endif %} +

+
+ + +
+

Preferences

+ +

Preferred Age Range: + {{ user.prefered_age_range or 'Not specified' }} +

+ +

Likes:

+ {% if user.likes %} + + {% else %} +

Not specified

+ {% endif %} + +

Dislikes:

+ {% if user.dislikes %} + + {% else %} +

Not specified

+ {% endif %} +
+ + +
+

Contacts

+

XMPP: {{ user.xmpp }}

+

Email: {{ user.email or 'Not specified' }}

+

Phone: {{ user.phone or 'Not specified' }}

+
-

Contacts

-

XMPP: {{ user.xmpp }}

-

Email: {{ user.email or 'Not specified' }}

-

Phone: {{ user.phone or 'Not specified' }}

{% endblock %}