From 57d1bd654cb973d48c9293b2a0ab7db154f7a2f9 Mon Sep 17 00:00:00 2001 From: b33-stinger Date: Sun, 5 Apr 2026 04:32:22 +0900 Subject: [PATCH] Subscriptions feature BASE --- app/controllers/__init__.py | 2 ++ app/controllers/sub.py | 50 ++++++++++++++++++++++++++ app/controllers/user.py | 9 ++++- app/models/subscription.py | 60 ++++++++++++++++++++++++++++++++ app/models/user.py | 4 ++- config/config.json.example | 41 ---------------------- review/routes.py | 11 ++++++ review/users.json.example | 10 ------ static/css/custom.css | 24 +++++++++++++ templates/partial/menu.html | 1 + templates/sub/subscriptions.html | 18 ++++++++++ templates/user/read.html | 42 +++++++++++++++++++++- 12 files changed, 218 insertions(+), 54 deletions(-) create mode 100644 app/controllers/sub.py create mode 100644 app/models/subscription.py delete mode 100644 config/config.json.example delete mode 100644 review/users.json.example create mode 100644 templates/sub/subscriptions.html diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index b2a40f3..c376bc1 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -22,6 +22,7 @@ def register_blueprints(app): from app.controllers.password_reset import password_reset_bp from app.controllers.static import static_bp from app.controllers.error import error_bp + from app.controllers.sub import sub_bp app.register_blueprint(index_bp) app.register_blueprint(login_bp) @@ -40,3 +41,4 @@ def register_blueprints(app): app.register_blueprint(password_reset_bp) app.register_blueprint(static_bp) app.register_blueprint(error_bp) + app.register_blueprint(sub_bp) \ No newline at end of file diff --git a/app/controllers/sub.py b/app/controllers/sub.py new file mode 100644 index 0000000..c103bae --- /dev/null +++ b/app/controllers/sub.py @@ -0,0 +1,50 @@ +""" +Subs controller - Subs page. +""" + +from flask import Blueprint, render_template, request, session, abort +from app.controllers.decorators import login_required +from app.models.user import user_by_name +from app.models.subscription import user_subscribe_to, user_unsubscribe_to, get_user_subs + +sub_bp = Blueprint('sub', __name__) + +@sub_bp.route('/subscriptions', methods=["GET"]) +@login_required +def subscriptions(): + """Display the People user has subscribed to.""" + subscribed_users = get_user_subs(session.get('name', None)) + print(subscribed_users) + return render_template('sub/subscriptions.html', subscribed_users=subscribed_users) + +@sub_bp.route("/subscriptions/subscribe", methods=["POST"]) +#@limit("20 per min") # enable rate limit if needed +@login_required +def subscribe(): + name = request.form.get('to') + user = user_by_name(name).get('name') + current_user = session.get('name', None) + if not current_user or not user: + abort(403) + try: + user_subscribe_to(current_user, user) + except Exception as e: + abort(500) + + return {"status": "ok"} + +@sub_bp.route("/subscriptions/unsubscribe", methods=["POST"]) +#@limit("20 per min") # enable rate limit if needed +@login_required +def unsubscribe(): + name = request.form.get('to') + user = user_by_name(name).get('name') + current_user = session.get('name', None) + if not current_user or not user: + abort(403) + try: + user_unsubscribe_to(current_user, user) + except Exception as e: + abort(500) + + return {"status": "ok"} \ No newline at end of file diff --git a/app/controllers/user.py b/app/controllers/user.py index 84bb8d7..9560693 100644 --- a/app/controllers/user.py +++ b/app/controllers/user.py @@ -7,6 +7,7 @@ from app.models.crackme import crackmes_by_user from app.models.solution import solutions_by_user from app.models.comment import comments_by_user +from app.models.subscription import get_user_subs from app.models.errors import ErrNoResult user_bp = Blueprint('user', __name__) @@ -50,6 +51,11 @@ def user_profile(name): session_username = session.get('name', '') viewing_own_page = session_username and session_username == actual_username + # Check wether USER has subscribed to NAME + is_subscribed = False + if session_username and not viewing_own_page: + is_subscribed = user['name'] in get_user_subs(session_username) + return render_template('user/read.html', username=user['name'], NbCrackmes=nb_crackmes, @@ -58,7 +64,8 @@ def user_profile(name): crackmes=crackmes, solutions=solutions_extended, comments=comments, - viewingOwnPage=viewing_own_page) + viewingOwnPage=viewing_own_page, + isSubscribed=is_subscribed) except Exception as e: print(f"Error getting user data: {e}") diff --git a/app/models/subscription.py b/app/models/subscription.py new file mode 100644 index 0000000..57db342 --- /dev/null +++ b/app/models/subscription.py @@ -0,0 +1,60 @@ +""" +subscriptions model for database operations. +""" + +import re +from app.services.database import get_collection, check_connection +from app.models.user import user_by_name +from app.models.errors import ErrNoResult + +def get_users_subbed_to(name): + """Return a list with all users subscribed to NAME""" + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + collection = get_collection('subscriptions') + user = user_by_name(name)['name'] + result = [x.get('name') for x in collection.find({'to': user})] + + if result is None: + raise ErrNoResult("No Subscribers yet") + + return result + +def get_user_subs(name): + """Return the subscriptions list of NAME""" + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + collection = get_collection('subscriptions') + user = user_by_name(name)['name'] + result = [x.get('to') for x in collection.find({'name': user})] + + if result is None: + raise ErrNoResult("No Subscribtions yet") + + return result + +def user_subscribe_to(name, to_name): + """Make NAME subscribe to TO_NAME""" + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + if to_name in get_user_subs(name): + return + + collection = get_collection('subscriptions') + + collection.insert_one({'name': name, 'to': to_name}) + +def user_unsubscribe_to(name, to_name): + """Make USER unsubscribe to TO_NAME""" + if not check_connection(): + raise ErrUnavailable("Database is unavailable") + + if not to_name in get_user_subs(name): + return + + collection = get_collection('subscriptions') + + collection.delete_one({'name': name, 'to': to_name}) \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index e84350f..de5342a 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -133,7 +133,9 @@ def user_create(name, email, password): 'email': email, 'password': password, 'visible': True, - 'deleted': False + 'deleted': False, + # TODO: allow the user to enable/disable it + 'subs_email': False # whether to send an email when someone user subscribed to uploaded a crackme } collection.insert_one(user) diff --git a/config/config.json.example b/config/config.json.example deleted file mode 100644 index c644d36..0000000 --- a/config/config.json.example +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Database": { - "URL": "mongodb://127.0.0.1:27017", - "Name": "crackmesone" - }, - "Server": { - "Host": "127.0.0.1", - "Port": 8081 - }, - "Session": { - "SecretKey": "change-this-to-a-secure-random-string", - "CookieName": "crackmesone" - }, - "Recaptcha": { - "Enabled": false, - "SiteKey": "your-recaptcha-site-key", - "Secret": "your-recaptcha-secret" - }, - "RateLimiter": { - "Enabled": true, - "StorageUri": "memory://" - }, - "Discord": { - "Enabled": false, - "WebhookPublic": "your-public-discord-webhook-url", - "WebhookPrivate": "your-private-discord-webhook-url", - "WebhookModeration": "your-moderation-discord-webhook-url" - }, - "Reviewer": { - "Enabled": false, - "PasswordSalt": "change-this-to-a-secure-random-salt" - }, - "Email": { - "Enabled": false, - "ApiKey": "re_your_resend_api_key", - "From": "crackmes.one " - }, - "Site": { - "BaseURL": "https://crackmes.one" - } -} diff --git a/review/routes.py b/review/routes.py index 12c1556..912726a 100644 --- a/review/routes.py +++ b/review/routes.py @@ -32,6 +32,7 @@ from review.logger import log_reviewer_operation +from app.models.subscription import get_users_subbed_to # ============================================================================= # Configuration and Constants @@ -832,6 +833,16 @@ def approve_pending_crackme(hexid): crackme["author"], f"Your crackme '{html_escape(crackme['name'])}' has been accepted!" ) + + #TODO: maybe make this async + # Notify people that have subscribed to author + users_subbed = get_users_subbed_to(crackme["author"]) + for user in users_subbed: + send_user_notification( + user, + f"User {crackme['author']}, you've subscribed to, uploaded a new crackme '{html_escape(crackme['name'])}'!" + ) + return True, f"Crackme '{crackme['name']}' approved successfully" diff --git a/review/users.json.example b/review/users.json.example deleted file mode 100644 index ed291ab..0000000 --- a/review/users.json.example +++ /dev/null @@ -1,10 +0,0 @@ -{ - "example_regular_user": { - "password_hash": "your_password_hash_here", - "is_admin": false - }, - "example_admin_user": { - "password_hash": "your_password_hash_here", - "is_admin": true - } -} diff --git a/static/css/custom.css b/static/css/custom.css index 020c82c..f85ef6f 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -339,4 +339,28 @@ h3:hover .anchor-link { font-size: 14px; min-width: 120px; text-align: center; +} + +/* Subscribe Button */ +.subscribe-btn{ + color: #9acc14; + float: right; + font-size: 70%; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin: 0; +} + +.subscribe-btn:hover{ + color: red; +} + +.unsub{ + color: red !important; +} + +.unsub:hover{ + color: #9acc14 !important; } \ No newline at end of file diff --git a/templates/partial/menu.html b/templates/partial/menu.html index d1ae5e3..c49537d 100644 --- a/templates/partial/menu.html +++ b/templates/partial/menu.html @@ -16,6 +16,7 @@

crackmes.one

Discord 🏆 CTF (Feb 2026) Profile + Subs Logout diff --git a/templates/sub/subscriptions.html b/templates/sub/subscriptions.html new file mode 100644 index 0000000..7e28483 --- /dev/null +++ b/templates/sub/subscriptions.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}My subscriptions{% endblock %} +{% block page_title %}My subscriptions{% endblock %} +{% block head %}{% endblock %} +{% block content %} + + {% if subscribed_users %} + + {% for user in subscribed_users %} + {{ user }} + {% endfor %} + + {% else %} + No subscriptions + {% endif %} +{% include 'partial/footer.html' %} +{% endblock %} +{% block foot %}{% endblock %} \ No newline at end of file diff --git a/templates/user/read.html b/templates/user/read.html index 2284760..f423023 100644 --- a/templates/user/read.html +++ b/templates/user/read.html @@ -11,7 +11,15 @@ }
-

{{ username }}'s profile

+

{{ username }}'s profile + {% if not viewingOwnPage %} +
+ + + +
+ {% endif %} +

@@ -144,6 +152,38 @@

Comments

{% endif %}
+{% if not viewingOwnPage %} + +{% endif %} + {% include 'partial/footer.html' %} {% endblock %}