mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
3.0.0a2 dev (#1528)
# 3.0.0a2 / 2020-07-09 **General** * Accept additional profile fields during registration (affiliation, website, country) * This does not add additional inputs. Themes or additional JavaScript can add the form inputs. **Admin Panel** * Redesign the challenge creation form to use a radio button with challenge type selection instead of a select input **API** * Admins can no longer ban themselves through `PATCH /api/v1/users/[user_id]` **Themes** * Spinner centering has been switched from a hard coded margin in CSS to flexbox CSS classes from Bootstrap **Plugins** * Revert plugin menu (`register_admin_plugin_menu_bar`, `register_user_page_menu_bar`) changes to 2.x code **Miscellaneous** * Fix issue with `Configs.ctf_name` returning incorrect value * Add prerender step back into challenges.js * Better handling of missing challenge types. Missing challenge types no longer bring down all other challenges.
This commit is contained in:
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,3 +1,32 @@
|
|||||||
|
# 3.0.0a2 / 2020-07-09
|
||||||
|
|
||||||
|
**General**
|
||||||
|
|
||||||
|
- Accept additional profile fields during registration (affiliation, website, country)
|
||||||
|
- This does not add additional inputs. Themes or additional JavaScript can add the form inputs.
|
||||||
|
|
||||||
|
**Admin Panel**
|
||||||
|
|
||||||
|
- Redesign the challenge creation form to use a radio button with challenge type selection instead of a select input
|
||||||
|
|
||||||
|
**API**
|
||||||
|
|
||||||
|
- Admins can no longer ban themselves through `PATCH /api/v1/users/[user_id]`
|
||||||
|
|
||||||
|
**Themes**
|
||||||
|
|
||||||
|
- Spinner centering has been switched from a hard coded margin in CSS to flexbox CSS classes from Bootstrap
|
||||||
|
|
||||||
|
**Plugins**
|
||||||
|
|
||||||
|
- Revert plugin menu (`register_admin_plugin_menu_bar`, `register_user_page_menu_bar`) changes to 2.x code
|
||||||
|
|
||||||
|
**Miscellaneous**
|
||||||
|
|
||||||
|
- Fix issue with `Configs.ctf_name` returning incorrect value
|
||||||
|
- Add prerender step back into challenges.js
|
||||||
|
- Better handling of missing challenge types. Missing challenge types no longer bring down all other challenges.
|
||||||
|
|
||||||
# 3.0.0a1 / 2020-07-01
|
# 3.0.0a1 / 2020-07-01
|
||||||
|
|
||||||
**General**
|
**General**
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from CTFd.utils.migrations import create_database, migrations, stamp_latest_revi
|
|||||||
from CTFd.utils.sessions import CachingSessionInterface
|
from CTFd.utils.sessions import CachingSessionInterface
|
||||||
from CTFd.utils.updates import update_check
|
from CTFd.utils.updates import update_check
|
||||||
|
|
||||||
__version__ = "3.0.0a1"
|
__version__ = "3.0.0a2"
|
||||||
|
|
||||||
|
|
||||||
class CTFdRequest(Request):
|
class CTFdRequest(Request):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from flask import render_template, request, url_for
|
from flask import abort, render_template, request, url_for
|
||||||
|
|
||||||
from CTFd.admin import admin
|
from CTFd.admin import admin
|
||||||
from CTFd.models import Challenges, Flags, Solves
|
from CTFd.models import Challenges, Flags, Solves
|
||||||
from CTFd.plugins.challenges import get_chal_class
|
from CTFd.plugins.challenges import CHALLENGE_CLASSES, get_chal_class
|
||||||
from CTFd.utils.decorators import admins_only
|
from CTFd.utils.decorators import admins_only
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,14 @@ def challenges_detail(challenge_id):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||||
challenge_class = get_chal_class(challenge.type)
|
|
||||||
|
try:
|
||||||
|
challenge_class = get_chal_class(challenge.type)
|
||||||
|
except KeyError:
|
||||||
|
abort(
|
||||||
|
500,
|
||||||
|
f"The underlying challenge type ({challenge.type}) is not installed. This challenge can not be loaded.",
|
||||||
|
)
|
||||||
|
|
||||||
update_j2 = render_template(
|
update_j2 = render_template(
|
||||||
challenge_class.templates["update"].lstrip("/"), challenge=challenge
|
challenge_class.templates["update"].lstrip("/"), challenge=challenge
|
||||||
@@ -67,4 +74,5 @@ def challenges_detail(challenge_id):
|
|||||||
@admin.route("/admin/challenges/new")
|
@admin.route("/admin/challenges/new")
|
||||||
@admins_only
|
@admins_only
|
||||||
def challenges_new():
|
def challenges_new():
|
||||||
return render_template("admin/challenges/new.html")
|
types = CHALLENGE_CLASSES.keys()
|
||||||
|
return render_template("admin/challenges/new.html", types=types)
|
||||||
|
|||||||
@@ -187,7 +187,13 @@ class ChallengeList(Resource):
|
|||||||
# Fallthrough to continue
|
# Fallthrough to continue
|
||||||
continue
|
continue
|
||||||
|
|
||||||
challenge_type = get_chal_class(challenge.type)
|
try:
|
||||||
|
challenge_type = get_chal_class(challenge.type)
|
||||||
|
except KeyError:
|
||||||
|
# Challenge type does not exist. Fall through to next challenge.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Challenge passes all checks, add it to response
|
||||||
response.append(
|
response.append(
|
||||||
{
|
{
|
||||||
"id": challenge.id,
|
"id": challenge.id,
|
||||||
@@ -268,7 +274,13 @@ class Challenge(Resource):
|
|||||||
and_(Challenges.state != "hidden", Challenges.state != "locked"),
|
and_(Challenges.state != "hidden", Challenges.state != "locked"),
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
|
|
||||||
chal_class = get_chal_class(chal.type)
|
try:
|
||||||
|
chal_class = get_chal_class(chal.type)
|
||||||
|
except KeyError:
|
||||||
|
abort(
|
||||||
|
500,
|
||||||
|
f"The underlying challenge type ({chal.type}) is not installed. This challenge can not be loaded.",
|
||||||
|
)
|
||||||
|
|
||||||
if chal.requirements:
|
if chal.requirements:
|
||||||
requirements = chal.requirements.get("prerequisites", [])
|
requirements = chal.requirements.get("prerequisites", [])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from flask import abort, request
|
from flask import abort, request, session
|
||||||
from flask_restx import Namespace, Resource
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
from CTFd.api.v1.helpers.models import build_model_filters
|
from CTFd.api.v1.helpers.models import build_model_filters
|
||||||
@@ -218,6 +218,16 @@ class UserPublic(Resource):
|
|||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
data["id"] = user_id
|
data["id"] = user_id
|
||||||
|
|
||||||
|
# Admins should not be able to ban themselves
|
||||||
|
if data["id"] == session["id"] and (
|
||||||
|
data.get("banned") is True or data.get("banned") == "true"
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
{"success": False, "errors": {"id": "You cannot ban yourself"}},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
schema = UserSchema(view="admin", instance=user, partial=True)
|
schema = UserSchema(view="admin", instance=user, partial=True)
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
|
|||||||
38
CTFd/auth.py
38
CTFd/auth.py
@@ -22,6 +22,7 @@ from CTFd.utils.logging import log
|
|||||||
from CTFd.utils.modes import TEAMS_MODE
|
from CTFd.utils.modes import TEAMS_MODE
|
||||||
from CTFd.utils.security.auth import login_user, logout_user
|
from CTFd.utils.security.auth import login_user, logout_user
|
||||||
from CTFd.utils.security.signing import unserialize
|
from CTFd.utils.security.signing import unserialize
|
||||||
|
from CTFd.utils.validators import ValidationError
|
||||||
|
|
||||||
auth = Blueprint("auth", __name__)
|
auth = Blueprint("auth", __name__)
|
||||||
|
|
||||||
@@ -189,6 +190,10 @@ def register():
|
|||||||
email_address = request.form.get("email", "").strip().lower()
|
email_address = request.form.get("email", "").strip().lower()
|
||||||
password = request.form.get("password", "").strip()
|
password = request.form.get("password", "").strip()
|
||||||
|
|
||||||
|
website = request.form.get("website")
|
||||||
|
affiliation = request.form.get("affiliation")
|
||||||
|
country = request.form.get("country")
|
||||||
|
|
||||||
name_len = len(name) == 0
|
name_len = len(name) == 0
|
||||||
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
|
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
|
||||||
emails = (
|
emails = (
|
||||||
@@ -201,6 +206,25 @@ def register():
|
|||||||
valid_email = validators.validate_email(email_address)
|
valid_email = validators.validate_email(email_address)
|
||||||
team_name_email_check = validators.validate_email(name)
|
team_name_email_check = validators.validate_email(name)
|
||||||
|
|
||||||
|
if country:
|
||||||
|
try:
|
||||||
|
validators.validate_country_code(country)
|
||||||
|
valid_country = True
|
||||||
|
except ValidationError:
|
||||||
|
valid_country = False
|
||||||
|
else:
|
||||||
|
valid_country = True
|
||||||
|
|
||||||
|
if website:
|
||||||
|
valid_website = validators.validate_url(website)
|
||||||
|
else:
|
||||||
|
valid_website = True
|
||||||
|
|
||||||
|
if affiliation:
|
||||||
|
valid_affiliation = len(affiliation) < 128
|
||||||
|
else:
|
||||||
|
valid_affiliation = True
|
||||||
|
|
||||||
if not valid_email:
|
if not valid_email:
|
||||||
errors.append("Please enter a valid email address")
|
errors.append("Please enter a valid email address")
|
||||||
if email.check_email_is_whitelisted(email_address) is False:
|
if email.check_email_is_whitelisted(email_address) is False:
|
||||||
@@ -221,6 +245,12 @@ def register():
|
|||||||
errors.append("Pick a shorter password")
|
errors.append("Pick a shorter password")
|
||||||
if name_len:
|
if name_len:
|
||||||
errors.append("Pick a longer user name")
|
errors.append("Pick a longer user name")
|
||||||
|
if valid_website is False:
|
||||||
|
errors.append("Websites must be a proper URL starting with http or https")
|
||||||
|
if valid_country is False:
|
||||||
|
errors.append("Invalid country")
|
||||||
|
if valid_affiliation is False:
|
||||||
|
errors.append("Please provide a shorter affiliation")
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
return render_template(
|
return render_template(
|
||||||
@@ -233,6 +263,14 @@ def register():
|
|||||||
else:
|
else:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
user = Users(name=name, email=email_address, password=password)
|
user = Users(name=name, email=email_address, password=password)
|
||||||
|
|
||||||
|
if website:
|
||||||
|
user.website = website
|
||||||
|
if affiliation:
|
||||||
|
user.affiliation = affiliation
|
||||||
|
if country:
|
||||||
|
user.country = country
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class _ConfigsWrapper:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ctf_name(self):
|
def ctf_name(self):
|
||||||
return get_config("theme_header", default="CTFd")
|
return get_config("ctf_name", default="CTFd")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def theme_header(self):
|
def theme_header(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
|
|
||||||
# 404
|
# 404
|
||||||
@@ -13,7 +14,10 @@ def forbidden(error):
|
|||||||
|
|
||||||
# 500
|
# 500
|
||||||
def general_error(error):
|
def general_error(error):
|
||||||
return render_template("errors/500.html"), 500
|
if error.description == InternalServerError.description:
|
||||||
|
error.description = "An Internal Server Error has occurred"
|
||||||
|
|
||||||
|
return render_template("errors/500.html", error=error.description), 500
|
||||||
|
|
||||||
|
|
||||||
# 502
|
# 502
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from flask_marshmallow import Marshmallow
|
from flask_marshmallow import Marshmallow
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
@@ -77,7 +78,22 @@ class Challenges(db.Model):
|
|||||||
hints = db.relationship("Hints", backref="challenge")
|
hints = db.relationship("Hints", backref="challenge")
|
||||||
flags = db.relationship("Flags", backref="challenge")
|
flags = db.relationship("Flags", backref="challenge")
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
class alt_defaultdict(defaultdict):
|
||||||
|
"""
|
||||||
|
This slightly modified defaultdict is intended to allow SQLAlchemy to
|
||||||
|
not fail when querying Challenges that contain a missing challenge type.
|
||||||
|
|
||||||
|
e.g. Challenges.query.all() should not fail if `type` is `a_missing_type`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __missing__(self, key):
|
||||||
|
return self["standard"]
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
"polymorphic_identity": "standard",
|
||||||
|
"polymorphic_on": type,
|
||||||
|
"_polymorphic_map": alt_defaultdict(),
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import send_file, send_from_directory, url_for
|
from flask import send_file, send_from_directory
|
||||||
|
|
||||||
from CTFd.utils.config.pages import get_pages
|
from CTFd.utils.config.pages import get_pages
|
||||||
from CTFd.utils.decorators import admins_only as admins_only_wrapper
|
from CTFd.utils.decorators import admins_only as admins_only_wrapper
|
||||||
@@ -114,9 +114,6 @@ def register_admin_plugin_menu_bar(title, route):
|
|||||||
:param route: A string that is the href used by the link
|
:param route: A string that is the href used by the link
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if (route.startswith("http://") or route.startswith("https://")) is False:
|
|
||||||
route = url_for("views.static_html", route=route)
|
|
||||||
|
|
||||||
am = Menu(title=title, route=route)
|
am = Menu(title=title, route=route)
|
||||||
app.admin_plugin_menu_bar.append(am)
|
app.admin_plugin_menu_bar.append(am)
|
||||||
|
|
||||||
@@ -138,9 +135,6 @@ def register_user_page_menu_bar(title, route):
|
|||||||
:param route: A string that is the href used by the link
|
:param route: A string that is the href used by the link
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if (route.startswith("http://") or route.startswith("https://")) is False:
|
|
||||||
route = url_for("views.static_html", route=route)
|
|
||||||
|
|
||||||
p = Menu(title=title, route=route)
|
p = Menu(title=title, route=route)
|
||||||
app.plugin_menu_bar.append(p)
|
app.plugin_menu_bar.append(p)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
.CodeMirror-scroll {
|
.CodeMirror-scroll {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
height: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
|
|||||||
@@ -237,17 +237,6 @@ function handleChallengeOptions(event) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createChallenge(_event) {
|
|
||||||
const challenge = $(this)
|
|
||||||
.find("option:selected")
|
|
||||||
.data("meta");
|
|
||||||
if (challenge === undefined) {
|
|
||||||
$("#create-chal-entry-div").empty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadChalTemplate(challenge);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(() => {
|
$(() => {
|
||||||
$(".preview-challenge").click(function(_e) {
|
$(".preview-challenge").click(function(_e) {
|
||||||
window.challenge = new Object();
|
window.challenge = new Object();
|
||||||
@@ -430,29 +419,12 @@ $(() => {
|
|||||||
$(".edit-flag").click(editFlagModal);
|
$(".edit-flag").click(editFlagModal);
|
||||||
|
|
||||||
$.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) {
|
$.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) {
|
||||||
$("#create-chals-select").empty();
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
const chal_type_amt = Object.keys(data).length;
|
loadChalTemplate(data["standard"]);
|
||||||
if (chal_type_amt > 1) {
|
|
||||||
const option = "<option> -- </option>";
|
|
||||||
$("#create-chals-select").append(option);
|
|
||||||
for (const key in data) {
|
|
||||||
const challenge = data[key];
|
|
||||||
const option = $("<option/>");
|
|
||||||
option.attr("value", challenge.type);
|
|
||||||
option.text(challenge.name);
|
|
||||||
option.data("meta", challenge);
|
|
||||||
$("#create-chals-select").append(option);
|
|
||||||
}
|
|
||||||
$("#create-chals-select-div").show();
|
|
||||||
$("#create-chals-select").val("standard");
|
|
||||||
loadChalTemplate(data["standard"]);
|
|
||||||
} else if (chal_type_amt == 1) {
|
|
||||||
const key = Object.keys(data)[0];
|
|
||||||
$("#create-chals-select").empty();
|
|
||||||
loadChalTemplate(data[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#create-chals-select").change(createChallenge);
|
$("#create-chals-select input[name=type]").change(function() {
|
||||||
|
let challenge = data[this.value];
|
||||||
|
loadChalTemplate(challenge);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,20 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
|
<style>
|
||||||
|
.card-radio:checked + .card {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-color: #a3d39c;
|
||||||
|
box-shadow: 0 0 0 0.1rem #a3d39c;
|
||||||
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
|
}
|
||||||
|
.card-radio:checked + .card .card-radio-clone{
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -73,20 +87,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-2 offset-md-2">
|
||||||
<div class="row">
|
<h5 class="text-center pb-3">
|
||||||
<div class="col-md-6 offset-md-3">
|
Challenge Types
|
||||||
<div id="create-chals-select-div" style="display: none;">
|
</h5>
|
||||||
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
|
<div id="create-chals-select">
|
||||||
<select class="form-control custom-select" id="create-chals-select">
|
{% for type in types %}
|
||||||
</select>
|
<label class="w-100">
|
||||||
|
<input type="radio" name="type" class="card-radio d-none" value="{{ type }}" {% if type == "standard" %}checked{% endif %}/>
|
||||||
|
<div class="card rounded-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="card-title">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input card-radio-clone" type="radio" style="visibility: hidden;" checked>
|
||||||
|
<label class="form-check-label">{{ type }}</label>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
</label>
|
||||||
<div id="create-chal-entry-div">
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div id="create-chal-entry-div">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row d-flex align-items-center">
|
||||||
<div class="col-md-4 text-right">
|
<div class="col-md-4 text-right">
|
||||||
<h5><b>{{ user_count }}</b> users registered</h5>
|
<h5><b>{{ user_count }}</b> users registered</h5>
|
||||||
{% if get_config('user_mode') == 'teams' %}
|
{% if get_config('user_mode') == 'teams' %}
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div id="solves-graph">
|
<div id="solves-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="score-distribution-graph">
|
<div id="score-distribution-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="solve-percentages-graph">
|
<div id="solve-percentages-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="keys-pie-graph">
|
<div id="keys-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div id="categories-pie-graph">
|
<div id="categories-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -169,18 +169,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row min-vh-25">
|
<div class="row min-vh-25">
|
||||||
{% if solves %}
|
{% if solves %}
|
||||||
<div id="keys-pie-graph" class="col-md-6">
|
<div id="keys-pie-graph" class="col-md-6 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="categories-pie-graph" class="col-md-6">
|
<div id="categories-pie-graph" class="col-md-6 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="score-graph" class="col-md-12">
|
<div id="score-graph" class="col-md-12 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -160,18 +160,18 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row min-vh-25">
|
<div class="row min-vh-25">
|
||||||
{% if solves %}
|
{% if solves %}
|
||||||
<div id="keys-pie-graph" class="col-md-6">
|
<div id="keys-pie-graph" class="col-md-6 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="categories-pie-graph" class="col-md-6">
|
<div id="categories-pie-graph" class="col-md-6 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="score-graph" class="col-md-12">
|
<div id="score-graph" class="col-md-12 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ table > thead > tr > td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fa-spin.spinner {
|
.fa-spin.spinner {
|
||||||
margin-top: 225px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
2
CTFd/themes/core/static/css/main.min.css
vendored
2
CTFd/themes/core/static/css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -22,8 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id='challenges-board'>
|
<div id='challenges-board'>
|
||||||
<div class="text-center">
|
<div class="min-vh-50 d-flex align-items-center">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<div class="text-center w-100">
|
||||||
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="pt-5 mt-5 text-center">
|
<div class="pt-5 mt-5 text-center">
|
||||||
<h1>500</h1>
|
<h1>500</h1>
|
||||||
<hr class="w-50">
|
<hr class="w-50">
|
||||||
<h2>An Internal Server Error has occurred</h2>
|
<h2>{{ error }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,23 +25,23 @@
|
|||||||
{% with form = Forms.auth.RegistrationForm() %}
|
{% with form = Forms.auth.RegistrationForm() %}
|
||||||
<form method="post" accept-charset="utf-8" autocomplete="off" role="form">
|
<form method="post" accept-charset="utf-8" autocomplete="off" role="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.name.label }}
|
<b>{{ form.name.label }}</b>
|
||||||
{{ form.name(class="form-control", value=name) }}
|
{{ form.name(class="form-control", value=name) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.email.label }}
|
<b>{{ form.email.label }}</b>
|
||||||
{{ form.email(class="form-control", value=email) }}
|
{{ form.email(class="form-control", value=email) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.password.label }}
|
<b>{{ form.password.label }}</b>
|
||||||
{{ form.password(class="form-control", value=password) }}
|
{{ form.password(class="form-control", value=password) }}
|
||||||
</div>
|
</div>
|
||||||
|
{{ form.nonce() }}
|
||||||
<div class="row pt-3">
|
<div class="row pt-3">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{{ form.submit(class="btn btn-md btn-primary btn-outlined float-right") }}
|
{{ form.submit(class="btn btn-md btn-primary btn-outlined float-right") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ form.nonce() }}
|
|
||||||
</form>
|
</form>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,12 +9,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
{% include "components/errors.html" %}
|
{% include "components/errors.html" %}
|
||||||
|
|
||||||
<div id="score-graph" class="row">
|
<div id="score-graph" class="row d-flex align-items-center">
|
||||||
<div class="col-md-12 text-center">
|
<div class="col-md-12 text-center">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if standings %}
|
||||||
<div id="scoreboard" class="row">
|
<div id="scoreboard" class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -26,37 +26,37 @@
|
|||||||
<form id="user-profile-form" method="post" accept-charset="utf-8" autocomplete="off" role="form"
|
<form id="user-profile-form" method="post" accept-charset="utf-8" autocomplete="off" role="form"
|
||||||
class="form-horizontal">
|
class="form-horizontal">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.name.label }}
|
<b>{{ form.name.label }}</b>
|
||||||
{{ form.name(class="form-control", value=name) }}
|
{{ form.name(class="form-control", value=name) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.email.label }}
|
<b>{{ form.email.label }}</b>
|
||||||
{{ form.email(class="form-control", value=email) }}
|
{{ form.email(class="form-control", value=email) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.confirm.label }}
|
<b>{{ form.confirm.label }}</b>
|
||||||
{{ form.confirm(class="form-control") }}
|
{{ form.confirm(class="form-control") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.password.label }}
|
<b>{{ form.password.label }}</b>
|
||||||
{{ form.password(class="form-control") }}
|
{{ form.password(class="form-control") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.affiliation.label }}
|
<b>{{ form.affiliation.label }}</b>
|
||||||
{{ form.affiliation(class="form-control", value=affiliation or "") }}
|
{{ form.affiliation(class="form-control", value=affiliation or "") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.website.label }}
|
<b>{{ form.website.label }}</b>
|
||||||
{{ form.website(class="form-control", value=website or "") }}
|
{{ form.website(class="form-control", value=website or "") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.country.label }}
|
<b>{{ form.country.label }}</b>
|
||||||
{{ form.country(class="form-control custom-select", value=country) }}
|
{{ form.country(class="form-control custom-select", value=country) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
{% with form = Forms.self.TokensForm() %}
|
{% with form = Forms.self.TokensForm() %}
|
||||||
<form method="POST" id="user-token-form">
|
<form method="POST" id="user-token-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{{ form.expiration.label }}
|
<b>{{ form.expiration.label }}</b>
|
||||||
{{ form.expiration(class="form-control") }}
|
{{ form.expiration(class="form-control") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -200,20 +200,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if solves %}
|
{% if solves %}
|
||||||
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div class="row">
|
||||||
<div class="text-center">
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<div id="keys-pie-graph" class="d-flex align-items-center">
|
||||||
|
<div class="text-center w-100">
|
||||||
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div id="categories-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br class="clearfix">
|
||||||
<br class="clearfix">
|
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||||
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
|
<div id="score-graph" class="w-100 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -97,20 +97,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if solves %}
|
{% if solves %}
|
||||||
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div class="row">
|
||||||
<div class="text-center">
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<div id="keys-pie-graph" class="d-flex align-items-center">
|
||||||
|
<div class="text-center w-100">
|
||||||
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div id="categories-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br class="clearfix">
|
||||||
<br class="clearfix">
|
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||||
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
|
<div id="score-graph" class="w-100 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -66,20 +66,28 @@
|
|||||||
{% include "components/errors.html" %}
|
{% include "components/errors.html" %}
|
||||||
|
|
||||||
{% if user.solves %}
|
{% if user.solves %}
|
||||||
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div class="row">
|
||||||
<div class="text-center">
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<div id="keys-pie-graph" class="d-flex align-items-center">
|
||||||
|
<div class="text-center w-100">
|
||||||
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div id="categories-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br class="clearfix">
|
||||||
<br class="clearfix">
|
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||||
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
|
<div id="score-graph" class="w-100 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -66,20 +66,28 @@
|
|||||||
{% include "components/errors.html" %}
|
{% include "components/errors.html" %}
|
||||||
|
|
||||||
{% if user.solves %}
|
{% if user.solves %}
|
||||||
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div class="row">
|
||||||
<div class="text-center">
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<div id="keys-pie-graph" class="d-flex align-items-center">
|
||||||
|
<div class="text-center w-100">
|
||||||
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-6 d-none d-md-block d-lg-block">
|
||||||
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
|
<div id="categories-pie-graph" class="d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br class="clearfix">
|
||||||
<br class="clearfix">
|
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||||
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
|
<div id="score-graph" class="w-100 d-flex align-items-center">
|
||||||
<div class="text-center">
|
<div class="text-center w-100">
|
||||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ author = u"Kevin Chung"
|
|||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
version = u""
|
version = u""
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = u"2.5.0"
|
release = u"3.0.0a2"
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ctfd",
|
"name": "ctfd",
|
||||||
"version": "2.5.0",
|
"version": "3.0.0a2",
|
||||||
"description": "CTFd is a Capture The Flag framework focusing on ease of use and customizability. It comes with everything you need to run a CTF and it's easy to customize with plugins and themes.",
|
"description": "CTFd is a Capture The Flag framework focusing on ease of use and customizability. It comes with everything you need to run a CTF and it's easy to customize with plugins and themes.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
17
tests/api/v1/users/test_users.py
Normal file
17
tests/api/v1/users/test_users.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_self_ban():
|
||||||
|
"""PATCH /api/v1/users/<user_id> should not allow a user to ban themselves"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
with login_as_user(app, name="admin") as client:
|
||||||
|
r = client.patch("/api/v1/users/1", json={"banned": True})
|
||||||
|
resp = r.get_json()
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert resp["success"] == False
|
||||||
|
assert resp["errors"] == {"id": "You cannot ban yourself"}
|
||||||
|
destroy_ctfd(app)
|
||||||
76
tests/challenges/test_challenge_types.py
Normal file
76
tests/challenges/test_challenge_types.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from CTFd.models import Challenges
|
||||||
|
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_challenge_type():
|
||||||
|
"""Test that missing challenge types don't cause total challenge rendering failure"""
|
||||||
|
app = create_ctfd(enable_plugins=True)
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app, name="admin", password="password")
|
||||||
|
|
||||||
|
challenge_data = {
|
||||||
|
"name": "name",
|
||||||
|
"category": "category",
|
||||||
|
"description": "description",
|
||||||
|
"value": 100,
|
||||||
|
"decay": 20,
|
||||||
|
"minimum": 1,
|
||||||
|
"state": "visible",
|
||||||
|
"type": "dynamic",
|
||||||
|
}
|
||||||
|
|
||||||
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||||
|
assert r.get_json().get("data")["id"] == 1
|
||||||
|
assert r.get_json().get("data")["type"] == "dynamic"
|
||||||
|
|
||||||
|
chal_count = Challenges.query.count()
|
||||||
|
assert chal_count == 1
|
||||||
|
|
||||||
|
# Delete the dynamic challenge type
|
||||||
|
from CTFd.plugins.challenges import CHALLENGE_CLASSES
|
||||||
|
|
||||||
|
del CHALLENGE_CLASSES["dynamic"]
|
||||||
|
|
||||||
|
r = client.get("/admin/challenges")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert b"dynamic" in r.data
|
||||||
|
|
||||||
|
r = client.get("/admin/challenges/1")
|
||||||
|
assert r.status_code == 500
|
||||||
|
assert b"The underlying challenge type (dynamic) is not installed" in r.data
|
||||||
|
|
||||||
|
challenge_data = {
|
||||||
|
"name": "name",
|
||||||
|
"category": "category",
|
||||||
|
"description": "description",
|
||||||
|
"value": 100,
|
||||||
|
"state": "visible",
|
||||||
|
"type": "standard",
|
||||||
|
}
|
||||||
|
r = client.post("/api/v1/challenges", json=challenge_data)
|
||||||
|
|
||||||
|
r = client.get("/challenges")
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# We should still see the one visible standard challenge
|
||||||
|
r = client.get("/api/v1/challenges")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert len(r.json["data"]) == 1
|
||||||
|
assert r.json["data"][0]["type"] == "standard"
|
||||||
|
|
||||||
|
# We cannot load the broken challenge
|
||||||
|
r = client.get("/api/v1/challenges/1")
|
||||||
|
assert r.status_code == 500
|
||||||
|
assert (
|
||||||
|
"The underlying challenge type (dynamic) is not installed"
|
||||||
|
in r.json["message"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We can load other challenges
|
||||||
|
r = client.get("/api/v1/challenges/2")
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
@@ -148,7 +148,7 @@ def test_register_admin_plugin_menu_bar():
|
|||||||
|
|
||||||
menu_item = get_admin_plugin_menu_bar()[0]
|
menu_item = get_admin_plugin_menu_bar()[0]
|
||||||
assert menu_item.title == "test_admin_plugin_name"
|
assert menu_item.title == "test_admin_plugin_name"
|
||||||
assert menu_item.route == "http://localhost/test_plugin"
|
assert menu_item.route == "/test_plugin"
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ def test_register_user_page_menu_bar():
|
|||||||
|
|
||||||
menu_item = get_user_page_menu_bar()[0]
|
menu_item = get_user_page_menu_bar()[0]
|
||||||
assert menu_item.title == "test_user_menu_link"
|
assert menu_item.title == "test_user_menu_link"
|
||||||
assert menu_item.route == "http://localhost/test_user_href"
|
assert menu_item.route == "/test_user_href"
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user