Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
50 changes: 50 additions & 0 deletions app/controllers/sub.py
Original file line number Diff line number Diff line change
@@ -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"}
9 changes: 8 additions & 1 deletion app/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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,
Expand All @@ -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}")
Expand Down
60 changes: 60 additions & 0 deletions app/models/subscription.py
Original file line number Diff line number Diff line change
@@ -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})
4 changes: 3 additions & 1 deletion app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 0 additions & 41 deletions config/config.json.example

This file was deleted.

11 changes: 11 additions & 0 deletions review/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from review.logger import log_reviewer_operation

from app.models.subscription import get_users_subbed_to

# =============================================================================
# Configuration and Constants
Expand Down Expand Up @@ -832,6 +833,16 @@ def approve_pending_crackme(hexid):
crackme["author"],
f"Your crackme '<a href=\"/crackme/{crackme['hexid']}\">{html_escape(crackme['name'])}</a>' 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 <a href=\"/user/{crackme['author']}\">{crackme['author']}</a>, you've subscribed to, uploaded a new crackme '<a href=\"/crackme/{crackme['hexid']}\">{html_escape(crackme['name'])}</a>'!"
)


return True, f"Crackme '{crackme['name']}' approved successfully"

Expand Down
10 changes: 0 additions & 10 deletions review/users.json.example

This file was deleted.

24 changes: 24 additions & 0 deletions static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions templates/partial/menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h2><a href="/" class="title-navbar">crackmes.one</a></h2>
<a href="https://discord.gg/2pPV3yq" class="btn btn-link">Discord</a>
<a href="https://crackmesone.ctfd.io/" class="btn btn-link" target="_blank" style="color: #9acc14; font-weight: bold;">🏆 CTF (Feb 2026)</a>
<a href="{{ BaseURI }}user/{{ usersess }}" class="btn btn-link">Profile</a>
<a href="{{ BaseURI }}subscriptions" class="btn btn-link">Subs</a>
<a href="{{ BaseURI }}logout" class="btn btn-link">Logout</a>
</section>
</header>
Expand Down
18 changes: 18 additions & 0 deletions templates/sub/subscriptions.html
Original file line number Diff line number Diff line change
@@ -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 %}
<span class="text-center"><a href="/user/{{ user }}">{{ user }}</a></span>
{% endfor %}

{% else %}
<span class="text-center">No subscriptions</span>
{% endif %}
{% include 'partial/footer.html' %}
{% endblock %}
{% block foot %}{% endblock %}
42 changes: 41 additions & 1 deletion templates/user/read.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
}
</script>
<div class="container grid-lg wrapper">
<h3><a href="">{{ username }}</a>'s profile</h3>
<h3><a href="">{{ username }}</a>'s profile
{% if not viewingOwnPage %}
<form id="sub_form" style="display: inline;">
<button id="sub_btn" class="subscribe-btn"></button>
<input type="hidden" name="to" value="{{ username }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form>
{% endif %}
</h3>
<div class="columns col-12 ">
<div class="column col-4">
<div class="column col-12 panel-background">
Expand Down Expand Up @@ -144,6 +152,38 @@ <h3>Comments</h3>
{% endif %}

</div>
{% if not viewingOwnPage %}
<script>
let isSubbed = {{ isSubscribed | tojson }};
const subForm = document.getElementById("sub_form");
const subBtn = document.getElementById("sub_btn");

function update_btn() {
subBtn.textContent = isSubbed ? "Unsubscribe" : "Subscribe";
}

update_btn();
subForm.addEventListener("click", async (e) => {
e.preventDefault();
const url = `/subscriptions/${isSubbed ? 'un' : ''}subscribe`;
const data = new FormData(subForm);
try {
const response = await fetch(url, {
method: "POST",
body: data
});
const result = await response.json();
if (result.status === "ok") {
isSubbed = !isSubbed;
update_btn();
}
} catch (error) {
console.error("Fetch error:", error);
}
});
</script>
{% endif %}


{% include 'partial/footer.html' %}
{% endblock %}
Expand Down