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:
Kevin Chung
2020-07-09 13:40:35 -04:00
committed by GitHub
parent 1bccbf1fdd
commit 1725e632cf
37 changed files with 399 additions and 163 deletions

View File

@@ -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**

View File

@@ -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):

View File

@@ -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)

View File

@@ -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", [])

View File

@@ -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:

View File

@@ -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()

View File

@@ -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):

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 ---------------------------------------------------

View File

@@ -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": {

View 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)

View 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)

View File

@@ -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)