Version 1.1 CTFd (#514)

* Bootstrap v4 (#490)
* Upgrading original theme to use Bootstrap v4 and overall improve use of utility classes
* Fixing graph issues. Colors per team & cleaner hover
* The solves tab now shows relative time instead of absolute time
* Redesign admin theme
* Updating modals and changing form name from desc to description
* Moving CSS config from Pages to Config page
* Adding IP address count to statistics
* Move control of certain modals (files, flags, tags, hints) to challenges page
* Expanding size of config page
* Combining statistics and graphs pages
* Moving percentage solved to the statistics page instead of the admin challenges page

* Rename Keys.key_type to Keys.type (#459) (#478)

* Rename keys.key_type to keys.type (#459)
* Fixing previous migration to not be worried about key_type v type

* Fixing loading of challenge type plugins

* Switching from Handlebars to Nunjucks (#491)

* Switching from Handlebars to Nunjucks
* Allow admins to unlock hints before CTF begins and test that this is not allowed for regular users

* Authed only (#492)

* Adding authed_only decorator and adding next to url_for

* Adding a basic preview to hints (#494)

* Hints have a preview now for creating and updating hints. HTML and markdown are still allowed.

* Ezq (#495)

* Adding ezq as a simple wrapper around bootstrap modals

* Use tabs not spaces and remove gray background on inputs

* Adding title & draft to Pages. Making page preview open a new tab (#497)

* Adding title & draft to Pages.
* Making page preview open a new tab instead of render in the existing tab
* Draft pages cannot be seen without a preview

* Update check (#499)

* Add update_check function
* Notify user that a CTFd update is available in the admin panel
* Adding update_check tests

* Ratelimit (#500)

* Implementing a ratelimit function 
* Fix error page formatting
* Add rate limiting tests
* Rate limit authentication functions and rate limit admin send email function

* Load user solves before we load challenges to avoid unstyled buttons (#502)

* Add a challenge preview (#503)

* Adding a challenge preview to the admin panel
* Change /admin/chals/<int:chalid> to /admin/chal/<int:chalid>

* Adding codecov (#504)

* Test coverage at https://codecov.io/gh/CTFd/CTFd

* Sendmail improvements (#505)

* Add get_smtp timeout, add sendmail error messages
* Adding more error handling to sendmail

* Adding Flask-Script (#507)

* Pause ctf (#508)

* Implement CTF pausing
* Test CTF pausing

* Fix loading challenges for users (#510)

* Fix loading challenges for users
* Temporarily switch themes in test

* Pause help text (#509)

* Adding pause help text

* Pages authed (#511)

* Adding authentication options to pages
* Adding tests for accessing pages while draft & auth_required

* Merging master into 1.1 (#513)

* Name the core theme and remove the original theme
selenium-screenshot-testing
Kevin Chung 2017-12-11 06:42:07 -05:00 committed by GitHub
parent 4c0ae9f3b5
commit 3af98b17d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
230 changed files with 5829 additions and 4242 deletions

View File

@ -16,3 +16,5 @@ before_script:
script:
- pep8 --ignore E501,E712 CTFd/ tests/
- nosetests -d
after_success:
- codecov

View File

@ -1,10 +1,50 @@
1.1.0 / TBD
==================
**Themes**
* The original theme has been replaced by the core theme. The core theme is written in Bootstrap v4.0.0-beta.2 and significantly reduces the amount of custom styles/classes used.
* Challenges can now be previewed from the admin panel.
* The modals to modify files, flags, tags, and hints are no longer controlled by Challenge Type Plugins and are defined in CTFd itself.
* The admin graphs and admin statistics pages have been combined.
* Percentage solved for challenges has been moved to the new statistics page.
* The scoregraph on the scoreboard has been cleaned up to better fit the page width.
* Score graphs now use user-specific colors.
* Hints can now be previewed from the admin panel.
* Various confirmation modals have been replaced with `ezq.js`, a simple Bootstrap modal wrapper.
* Fixed a bug where challenge buttons on the challenge board would load before being styled as solved.
**Database**
* `Keys.key_type` has been renamed to `Keys.type`.
* Pages Improvements:
* Page previews are now independent of the editor page.
* Pages now have a title which refer to the link's name on the navbar.
* Pages can now be drafts which cannot be seen by regular users.
* Pages can now require authentication to view.
* CSS editing has been moved to the config panel.
**Challenge Type Plugins**
* Handlebars has been replaced with Nunjucks which means Challenge Type Plugins using Handlebars must be updated to work with 1.1.0
**General**
* CTFs can now be paused to prevent solves.
* A new authed_only decorator is available to restrict pages to logged-in users.
* CTFd will now check for updates on start against `versioning.ctfd.io`. Admins will see in the admin panel that CTFd can be updated.
* A ratelimit function has been implemented. Authentication and email related functions are now ratelimited.
* Code coverage from codecov.
* Admins can now see the reason why an email to a team failed to send.
1.0.5 / 2017-10-25
==================
* Challenge Type Plugins now have a static interface which should be implemented by all challenge types.
* Challenge Type Plugins are now self-contained in the plugin system meaning you no longer need to manipulate themes in order to register Challenge Type Plugins.
* Challenge Type plugins should implement the create, read, update, delete, attempt, solve, and fail static methods.
* Challenge Type plugins now use strings for both their IDs and names.
* Challenge Type plugins now use strings for both their IDs and names.
* Challenge Type plugins now contain references to their related modal template files.
* Plugins can now register directories and files to be served by CTFd
* `CTFd.plugins.register_plugin_assets_directory` registers a directory to be served

View File

@ -10,7 +10,7 @@ from sqlalchemy_utils import database_exists, create_database
from sqlalchemy_utils.functions import get_tables
from six.moves import input
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp, update_check
from CTFd import utils
# Hack to support Unicode in Python 2 properly
@ -18,7 +18,7 @@ if sys.version_info[0] < 3:
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '1.0.5'
__version__ = '1.1.0a1'
class ThemeLoader(FileSystemLoader):
@ -33,7 +33,7 @@ class ThemeLoader(FileSystemLoader):
# Check if the template requested is for the admin panel
if template.startswith('admin/'):
template = template.lstrip('admin/')
template = template[6:] # Strip out admin/
template = "/".join(['admin', 'templates', template])
return super(ThemeLoader, self).get_source(environment, template)
@ -109,10 +109,13 @@ def create_app(config='CTFd.config.Config'):
exit()
app.db = db
app.VERSION = __version__
cache.init_app(app)
app.cache = cache
update_check()
version = utils.get_config('ctf_version')
# Upgrading from an older version of CTFd
@ -123,7 +126,7 @@ def create_app(config='CTFd.config.Config'):
exit()
if not utils.get_config('ctf_theme'):
utils.set_config('ctf_theme', 'original')
utils.set_config('ctf_theme', 'core')
from CTFd.views import views
from CTFd.challenges import challenges

View File

@ -10,7 +10,8 @@ from sqlalchemy.sql import not_
from sqlalchemy.exc import IntegrityError
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, \
DatabaseError
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
from CTFd.admin.statistics import admin_statistics
@ -22,14 +23,13 @@ from CTFd.admin.teams import admin_teams
from CTFd import utils
admin = Blueprint('admin', __name__)
@admin.route('/admin', methods=['GET'])
def admin_view():
if is_admin():
return redirect(url_for('admin_statistics.admin_graphs'))
return redirect(url_for('admin_statistics.admin_stats'))
return redirect(url_for('auth.login'))
@ -107,63 +107,55 @@ def admin_config():
freeze = int(request.form['freeze'])
try:
view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None))
view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None))
hide_scores = bool(request.form.get('hide_scores', None))
prevent_registration = bool(request.form.get('prevent_registration', None))
prevent_name_change = bool(request.form.get('prevent_name_change', None))
view_after_ctf = bool(request.form.get('view_after_ctf', None))
verify_emails = bool(request.form.get('verify_emails', None))
mail_tls = bool(request.form.get('mail_tls', None))
mail_ssl = bool(request.form.get('mail_ssl', None))
mail_useauth = bool(request.form.get('mail_useauth', None))
workshop_mode = bool(request.form.get('workshop_mode', None))
except (ValueError, TypeError):
view_challenges_unregistered = None
view_scoreboard_if_authed = None
hide_scores = None
prevent_registration = None
prevent_name_change = None
view_after_ctf = None
verify_emails = None
mail_tls = None
mail_ssl = None
mail_useauth = None
workshop_mode = None
# Set checkbox config values
view_challenges_unregistered = 'view_challenges_unregistered' in request.form
view_scoreboard_if_authed = 'view_scoreboard_if_authed' in request.form
hide_scores = 'hide_scores' in request.form
prevent_registration = 'prevent_registration' in request.form
prevent_name_change = 'prevent_name_change' in request.form
view_after_ctf = 'view_after_ctf' in request.form
verify_emails = 'verify_emails' in request.form
mail_tls = 'mail_tls' in request.form
mail_ssl = 'mail_ssl' in request.form
mail_useauth = 'mail_useauth' in request.form
workshop_mode = 'workshop_mode' in request.form
paused = 'paused' in request.form
finally:
view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
hide_scores = utils.set_config('hide_scores', hide_scores)
prevent_registration = utils.set_config('prevent_registration', prevent_registration)
prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change)
view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf)
verify_emails = utils.set_config('verify_emails', verify_emails)
mail_tls = utils.set_config('mail_tls', mail_tls)
mail_ssl = utils.set_config('mail_ssl', mail_ssl)
mail_useauth = utils.set_config('mail_useauth', mail_useauth)
workshop_mode = utils.set_config('workshop_mode', workshop_mode)
utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
utils.set_config('hide_scores', hide_scores)
utils.set_config('prevent_registration', prevent_registration)
utils.set_config('prevent_name_change', prevent_name_change)
utils.set_config('view_after_ctf', view_after_ctf)
utils.set_config('verify_emails', verify_emails)
utils.set_config('mail_tls', mail_tls)
utils.set_config('mail_ssl', mail_ssl)
utils.set_config('mail_useauth', mail_useauth)
utils.set_config('workshop_mode', workshop_mode)
utils.set_config('paused', paused)
mail_server = utils.set_config("mail_server", request.form.get('mail_server', None))
mail_port = utils.set_config("mail_port", request.form.get('mail_port', None))
utils.set_config("mail_server", request.form.get('mail_server', None))
utils.set_config("mail_port", request.form.get('mail_port', None))
if request.form.get('mail_useauth', None) and (request.form.get('mail_u', None) or request.form.get('mail_p', None)):
if len(request.form.get('mail_u')) > 0:
mail_username = utils.set_config("mail_username", request.form.get('mail_u', None))
utils.set_config("mail_username", request.form.get('mail_u', None))
if len(request.form.get('mail_p')) > 0:
mail_password = utils.set_config("mail_password", request.form.get('mail_p', None))
utils.set_config("mail_password", request.form.get('mail_p', None))
elif request.form.get('mail_useauth', None) is None:
utils.set_config("mail_username", None)
utils.set_config("mail_password", None)
ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None))
ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
utils.set_config("ctf_name", request.form.get('ctf_name', None))
utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
utils.set_config('css', request.form.get('css', None))
mailfrom_addr = utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
db_freeze = utils.set_config("freeze", freeze)
utils.set_config("freeze", freeze)
db_start = Config.query.filter_by(key='start').first()
db_start.value = start
@ -180,11 +172,13 @@ def admin_config():
cache.clear()
return redirect(url_for('admin.admin_config'))
with app.app_context():
cache.clear()
# Clear the cache so that we don't get stale values
cache.clear()
ctf_name = utils.get_config('ctf_name')
ctf_theme = utils.get_config('ctf_theme')
hide_scores = utils.get_config('hide_scores')
css = utils.get_config('css')
mail_server = utils.get_config('mail_server')
mail_port = utils.get_config('mail_port')
@ -211,6 +205,7 @@ def admin_config():
verify_emails = utils.get_config('verify_emails')
workshop_mode = utils.get_config('workshop_mode')
paused = utils.get_config('paused')
db.session.commit()
db.session.close()
@ -218,28 +213,32 @@ def admin_config():
themes = utils.get_themes()
themes.remove(ctf_theme)
return render_template('admin/config.html',
ctf_name=ctf_name,
ctf_theme_config=ctf_theme,
start=start,
end=end,
freeze=freeze,
hide_scores=hide_scores,
mail_server=mail_server,
mail_port=mail_port,
mail_useauth=mail_useauth,
mail_username=mail_username,
mail_password=mail_password,
mail_tls=mail_tls,
mail_ssl=mail_ssl,
view_challenges_unregistered=view_challenges_unregistered,
view_scoreboard_if_authed=view_scoreboard_if_authed,
prevent_registration=prevent_registration,
mailfrom_addr=mailfrom_addr,
mg_base_url=mg_base_url,
mg_api_key=mg_api_key,
prevent_name_change=prevent_name_change,
verify_emails=verify_emails,
view_after_ctf=view_after_ctf,
themes=themes,
workshop_mode=workshop_mode)
return render_template(
'admin/config.html',
ctf_name=ctf_name,
ctf_theme_config=ctf_theme,
css=css,
start=start,
end=end,
freeze=freeze,
hide_scores=hide_scores,
mail_server=mail_server,
mail_port=mail_port,
mail_useauth=mail_useauth,
mail_username=mail_username,
mail_password=mail_password,
mail_tls=mail_tls,
mail_ssl=mail_ssl,
view_challenges_unregistered=view_challenges_unregistered,
view_scoreboard_if_authed=view_scoreboard_if_authed,
prevent_registration=prevent_registration,
mailfrom_addr=mailfrom_addr,
mg_base_url=mg_base_url,
mg_api_key=mg_api_key,
prevent_name_change=prevent_name_change,
verify_emails=verify_emails,
view_after_ctf=view_after_ctf,
themes=themes,
workshop_mode=workshop_mode,
paused=paused
)

View File

@ -33,18 +33,8 @@ def admin_chals():
if request.method == 'POST':
chals = Challenges.query.add_columns('id', 'type', 'name', 'value', 'description', 'category', 'hidden', 'max_attempts').order_by(Challenges.value).all()
teams_with_points = db.session.query(Solves.teamid).join(Teams).filter(
Teams.banned == False).group_by(Solves.teamid).count()
json_data = {'game': []}
for x in chals:
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(
Solves.chalid == x[1], Teams.banned == False).count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
type_class = CHALLENGE_CLASSES.get(x.type)
type_name = type_class.name if type_class else None
@ -58,7 +48,6 @@ def admin_chals():
'max_attempts': x.max_attempts,
'type': x.type,
'type_name': type_name,
'percentage_solved': percentage,
'type_data': {
'id': type_class.id,
'name': type_class.name,
@ -70,17 +59,23 @@ def admin_chals():
db.session.close()
return jsonify(json_data)
else:
return render_template('admin/chals.html')
challenges = Challenges.query.all()
return render_template('admin/challenges.html', challenges=challenges)
@admin_challenges.route('/admin/chals/<int:chalid>', methods=['GET', 'POST'])
@admin_challenges.route('/admin/chal/<int:chalid>', methods=['GET', 'POST'])
@admins_only
def admin_chal_detail(chalid):
chal = Challenges.query.filter_by(id=chalid).first_or_404()
chal_class = get_chal_class(chal.type)
if request.method == 'POST':
pass
status, message = chal_class.attempt(chal, request)
if status:
return jsonify({'status': 1, 'message': message})
else:
return jsonify({'status': 0, 'message': message})
elif request.method == 'GET':
chal = Challenges.query.filter_by(id=chalid).first_or_404()
chal_class = get_chal_class(chal.type)
obj, data = chal_class.read(chal)
return jsonify(data)
@ -211,11 +206,11 @@ def admin_get_values(chalid, prop):
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
json_data = {'keys': []}
for x in chal_keys:
key_class = get_key_class(x.key_type)
key_class = get_key_class(x.type)
json_data['keys'].append({
'id': x.id,
'key': x.flag,
'type': x.key_type,
'type': x.type,
'type_name': key_class.name,
'templates': key_class.templates,
})

View File

@ -35,13 +35,13 @@ def admin_keys_view(keyid):
if request.method == 'GET':
if keyid:
saved_key = Keys.query.filter_by(id=keyid).first_or_404()
key_class = get_key_class(saved_key.key_type)
key_class = get_key_class(saved_key.type)
json_data = {
'id': saved_key.id,
'key': saved_key.flag,
'data': saved_key.data,
'chal': saved_key.chal,
'type': saved_key.key_type,
'type': saved_key.type,
'type_name': key_class.name,
'templates': key_class.templates,
}
@ -60,7 +60,7 @@ def admin_keys_view(keyid):
k = Keys.query.filter_by(id=keyid).first()
k.flag = flag
k.data = data
k.key_type = key_type
k.type = key_type
db.session.commit()
db.session.close()
return '1'

View File

@ -1,5 +1,5 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.utils import admins_only, is_admin, cache, markdown
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd import utils
@ -7,65 +7,88 @@ from CTFd import utils
admin_pages = Blueprint('admin_pages', __name__)
@admin_pages.route('/admin/css', methods=['GET', 'POST'])
@admins_only
def admin_css():
if request.method == 'POST':
css = request.form['css']
css = utils.set_config('css', css)
with app.app_context():
cache.clear()
return '1'
return '0'
@admin_pages.route('/admin/pages', methods=['GET', 'POST'])
@admins_only
def admin_pages_view():
route = request.args.get('route')
page_id = request.args.get('id')
page_op = request.args.get('operation')
if request.method == 'GET' and request.args.get('mode') == 'create':
if request.method == 'GET' and page_op == 'preview':
page = Pages.query.filter_by(id=page_id).first_or_404()
return render_template('page.html', content=markdown(page.html))
if request.method == 'GET' and page_op == 'create':
return render_template('admin/editor.html')
if route and request.method == 'GET':
page = Pages.query.filter_by(route=route).first()
if page_id and request.method == 'GET':
page = Pages.query.filter_by(id=page_id).first()
return render_template('admin/editor.html', page=page)
if request.method == 'POST':
page_form_id = request.form.get('id')
title = request.form['title']
html = request.form['html']
route = request.form['route'].lstrip('/')
page = Pages.query.filter_by(route=route).first()
auth_required = 'auth_required' in request.form
if page_op == 'preview':
page = Pages(title, route, html, draft=False)
return render_template('page.html', content=markdown(page.html))
page = Pages.query.filter_by(id=page_form_id).first()
errors = []
if not route:
errors.append('Missing URL route')
if errors:
page = Pages(html, route)
page = Pages(title, html, route)
return render_template('/admin/editor.html', page=page)
if page:
page.title = title
page.route = route
page.html = html
page.auth_required = auth_required
if page_op == 'publish':
page.draft = False
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin_pages.admin_pages_view'))
page = Pages(route, html)
cache.clear()
return jsonify({
'result': 'success',
'operation': page_op
})
if page_op == 'publish':
page = Pages(title, route, html, draft=False, auth_required=auth_required)
elif page_op == 'save':
page = Pages(title, route, html, auth_required=auth_required)
db.session.add(page)
db.session.commit()
db.session.close()
with app.app_context():
cache.clear()
return redirect(url_for('admin_pages.admin_pages_view'))
cache.clear()
return jsonify({
'result': 'success',
'operation': page_op
})
pages = Pages.query.all()
return render_template('admin/pages.html', routes=pages, css=utils.get_config('css'))
return render_template('admin/pages.html', pages=pages)
@admin_pages.route('/admin/pages/delete', methods=['POST'])
@admins_only
def delete_page():
route = request.form['route']
page = Pages.query.filter_by(route=route).first_or_404()
id = request.form['id']
page = Pages.query.filter_by(id=id).first_or_404()
db.session.delete(page)
db.session.commit()
db.session.close()

View File

@ -20,7 +20,9 @@ def admin_scoreboard_view():
def admin_scores():
score = db.func.sum(Challenges.value).label('score')
quickest = db.func.max(Solves.date).label('quickest')
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest)
teams = db.session.query(Solves.teamid, Teams.name, score)\
.join(Teams).join(Challenges).filter(Teams.banned == False)\
.group_by(Solves.teamid).order_by(score.desc(), quickest)
db.session.close()
json_data = {'teams': []}
for i, x in enumerate(teams):

View File

@ -7,12 +7,6 @@ from CTFd import utils
admin_statistics = Blueprint('admin_statistics', __name__)
@admin_statistics.route('/admin/graphs')
@admins_only
def admin_graphs():
return render_template('admin/graphs.html')
@admin_statistics.route('/admin/graphs/<graph_type>')
@admins_only
def admin_graph(graph_type):
@ -32,15 +26,47 @@ def admin_graph(graph_type):
for chal, count, name in solves:
json_data[name] = count
return jsonify(json_data)
elif graph_type == "solve-percentages":
chals = Challenges.query.add_columns('id', 'name', 'hidden', 'max_attempts').order_by(Challenges.value).all()
teams_with_points = db.session.query(Solves.teamid)\
.join(Teams)\
.filter(Teams.banned == False)\
.group_by(Solves.teamid)\
.count()
percentage_data = []
for x in chals:
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id)\
.filter(Solves.chalid == x[1], Teams.banned == False)\
.count()
if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points))
else:
percentage = 0.0
percentage_data.append({
'id': x.id,
'name': x.name,
'percentage': percentage,
})
percentage_data = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
json_data = {'percentages': percentage_data}
return jsonify(json_data)
@admin_statistics.route('/admin/statistics', methods=['GET'])
@admins_only
def admin_stats():
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
wrong_count = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
ip_count = db.session.query(db.func.count(Tracking.ip.distinct())).first()[0]
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
@ -61,13 +87,17 @@ def admin_stats():
db.session.commit()
db.session.close()
return render_template('admin/statistics.html', team_count=teams_registered,
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
solve_data=solve_data,
most_solved=most_solved,
least_solved=least_solved)
return render_template(
'admin/statistics.html',
team_count=teams_registered,
ip_count=ip_count,
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
solve_data=solve_data,
most_solved=most_solved,
least_solved=least_solved
)
@admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])

View File

@ -1,5 +1,5 @@
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from CTFd.utils import admins_only, is_admin, cache
from CTFd.utils import admins_only, is_admin, cache, ratelimit
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_
@ -184,13 +184,21 @@ def admin_team(teamid):
@admin_teams.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only
@ratelimit(method="POST", limit=10, interval=60)
def email_user(teamid):
message = request.form.get('msg', None)
team = Teams.query.filter(Teams.id == teamid).first()
if message and team:
if utils.sendmail(team.email, message):
return '1'
return '0'
msg = request.form.get('msg', None)
team = Teams.query.filter(Teams.id == teamid).first_or_404()
if msg and team:
result, response = utils.sendmail(team.email, msg)
return jsonify({
'result': result,
'message': response
})
else:
return jsonify({
'result': False,
'message': "Missing information"
})
@admin_teams.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@ -236,6 +244,7 @@ def delete_team(teamid):
def admin_solves(teamid="all"):
if teamid == "all":
solves = Solves.query.all()
awards = []
else:
solves = Solves.query.filter_by(teamid=teamid).all()
awards = Awards.query.filter_by(teamid=teamid).all()

View File

@ -9,12 +9,14 @@ from passlib.hash import bcrypt_sha256
from CTFd.models import db, Teams
from CTFd import utils
from CTFd.utils import ratelimit
auth = Blueprint('auth', __name__)
@auth.route('/confirm', methods=['POST', 'GET'])
@auth.route('/confirm/<data>', methods=['GET'])
@ratelimit(method="POST", limit=10, interval=60)
def confirm_user(data=None):
if not utils.get_config('verify_emails'):
# If the CTF doesn't care about confirming email addresses then redierct to challenges
@ -75,6 +77,7 @@ def confirm_user(data=None):
@auth.route('/reset_password', methods=['POST', 'GET'])
@auth.route('/reset_password/<data>', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=60)
def reset_password(data=None):
logger = logging.getLogger('logins')
if data is not None and request.method == "GET":
@ -115,16 +118,8 @@ def reset_password(data=None):
'reset_password.html',
errors=['If that account exists you will receive an email, please check your inbox']
)
s = TimedSerializer(app.config['SECRET_KEY'])
token = s.dumps(team.name)
text = """
Did you initiate a password reset?
{0}/{1}
""".format(url_for('auth.reset_password', _external=True), utils.base64encode(token, urlencode=True))
utils.sendmail(email, text)
utils.forgot_password(email, team.name)
return render_template(
'reset_password.html',
@ -134,6 +129,7 @@ Did you initiate a password reset?
@auth.route('/register', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=5)
def register():
logger = logging.getLogger('regs')
if not utils.can_register():
@ -209,6 +205,7 @@ def register():
@auth.route('/login', methods=['POST', 'GET'])
@ratelimit(method="POST", limit=10, interval=5)
def login():
logger = logging.getLogger('logins')
if request.method == 'POST':

View File

@ -18,8 +18,9 @@ challenges = Blueprint('challenges', __name__)
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
def hints_view(hintid):
if not utils.ctf_started():
abort(403)
if utils.ctf_started() is False:
if utils.is_admin() is False:
abort(403)
hint = Hints.query.filter_by(id=hintid).first_or_404()
chal = Challenges.query.filter_by(id=hint.chal).first()
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
@ -37,7 +38,7 @@ def hints_view(hintid):
})
elif request.method == 'POST':
if unlock is None: # The user does not have an unlock.
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()):
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()) or utils.is_admin() is True:
# It's ctftime or the CTF has ended (but we allow views after)
team = Teams.query.filter_by(id=session['id']).first()
if team.score() < hint.cost:
@ -68,9 +69,12 @@ def hints_view(hintid):
@challenges.route('/challenges', methods=['GET'])
def challenges_view():
infos = []
errors = []
start = utils.get_config('start') or 0
end = utils.get_config('end') or 0
if utils.ctf_paused():
infos.append('{} is paused'.format(utils.ctf_name()))
if not utils.is_admin(): # User is not an admin
if not utils.ctftime():
# It is not CTF time
@ -81,7 +85,7 @@ def challenges_view():
errors.append('{} has not started yet'.format(utils.ctf_name()))
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
errors.append('{} has ended'.format(utils.ctf_name()))
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
if utils.get_config('verify_emails'):
if utils.authed():
@ -93,7 +97,7 @@ def challenges_view():
errors.append('{} has not started yet'.format(utils.ctf_name()))
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
errors.append('{} has ended'.format(utils.ctf_name()))
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
else:
return redirect(url_for('auth.login', next='challenges'))
@ -311,6 +315,11 @@ def who_solved(chalid):
@challenges.route('/chal/<int:chalid>', methods=['POST'])
def chal(chalid):
if utils.ctf_paused():
return jsonify({
'status': 3,
'message': '{} is paused'.format(utils.ctf_name())
})
if utils.ctf_ended() and not utils.view_after_ctf():
abort(403)
if not utils.user_can_view_challenges():

View File

@ -121,6 +121,11 @@ class Config(object):
else:
CACHE_TYPE = 'simple'
'''
UPDATE_CHECK specifies whether or not CTFd will check whether or not there is a new version of CTFd
'''
UPDATE_CHECK = True
class TestingConfig(Config):
SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA'
@ -129,3 +134,6 @@ class TestingConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
SERVER_NAME = 'localhost'
UPDATE_CHECK = False
CACHE_REDIS_URL = None
CACHE_TYPE = 'simple'

View File

@ -30,12 +30,18 @@ db = SQLAlchemy()
class Pages(db.Model):
id = db.Column(db.Integer, primary_key=True)
route = db.Column(db.String(80), unique=True)
auth_required = db.Column(db.Boolean)
title = db.Column(db.String(80))
route = db.Column(db.Text, unique=True)
html = db.Column(db.Text)
draft = db.Column(db.Boolean)
def __init__(self, route, html):
def __init__(self, title, route, html, draft=True, auth_required=False):
self.title = title
self.route = route
self.html = html
self.draft = draft
self.auth_required = auth_required
def __repr__(self):
return "<Pages route {0}>".format(self.route)
@ -131,14 +137,14 @@ class Files(db.Model):
class Keys(db.Model):
id = db.Column(db.Integer, primary_key=True)
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
key_type = db.Column(db.String(80))
type = db.Column(db.String(80))
flag = db.Column(db.Text)
data = db.Column(db.Text)
def __init__(self, chal, flag, key_type):
def __init__(self, chal, flag, type):
self.chal = chal
self.flag = flag
self.key_type = key_type
self.type = type
def __repr__(self):
return "<Flag {0} for challenge {1}>".format(self.flag, self.chal)

View File

@ -9,11 +9,12 @@ from CTFd.utils import (
admins_only as admins_only_wrapper,
override_template as utils_override_template,
register_plugin_script as utils_register_plugin_script,
register_plugin_stylesheet as utils_register_plugin_stylesheet
register_plugin_stylesheet as utils_register_plugin_stylesheet,
pages as db_pages
)
Menu = namedtuple('Menu', ['name', 'route'])
Menu = namedtuple('Menu', ['title', 'route'])
ADMIN_PLUGIN_MENU_BAR = []
USER_PAGE_MENU_BAR = []
@ -82,7 +83,7 @@ def register_plugin_stylesheet(*args, **kwargs):
utils_register_plugin_stylesheet(*args, **kwargs)
def register_admin_plugin_menu_bar(name, route):
def register_admin_plugin_menu_bar(title, route):
"""
Registers links on the Admin Panel menubar/navbar
@ -90,7 +91,7 @@ def register_admin_plugin_menu_bar(name, route):
:param route: A string that is the href used by the link
:return:
"""
am = Menu(name=name, route=route)
am = Menu(title=title, route=route)
ADMIN_PLUGIN_MENU_BAR.append(am)
@ -103,7 +104,7 @@ def get_admin_plugin_menu_bar():
return ADMIN_PLUGIN_MENU_BAR
def register_user_page_menu_bar(name, route):
def register_user_page_menu_bar(title, route):
"""
Registers links on the User side menubar/navbar
@ -111,7 +112,7 @@ def register_user_page_menu_bar(name, route):
:param route: A string that is the href used by the link
:return:
"""
p = Menu(name=name, route=route)
p = Menu(title=title, route=route)
USER_PAGE_MENU_BAR.append(p)
@ -121,7 +122,7 @@ def get_user_page_menu_bar():
:return: Returns a list of Menu namedtuples. They have name, and route attributes.
"""
return USER_PAGE_MENU_BAR
return db_pages() + USER_PAGE_MENU_BAR
def init_plugins(app):

View File

@ -14,10 +14,10 @@ class BaseChallenge(object):
class CTFdStandardChallenge(BaseChallenge):
id = "standard" # Unique identifier used to register challenges
name = "standard" # Name of a challenge type
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
'create': '/plugins/challenges/assets/standard-challenge-create.hbs',
'update': '/plugins/challenges/assets/standard-challenge-update.hbs',
'modal': '/plugins/challenges/assets/standard-challenge-modal.hbs',
templates = { # Nunjucks templates used for each aspect of challenge editing & viewing
'create': '/plugins/challenges/assets/standard-challenge-create.njk',
'update': '/plugins/challenges/assets/standard-challenge-update.njk',
'modal': '/plugins/challenges/assets/standard-challenge-modal.njk',
}
scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/challenges/assets/standard-challenge-create.js',
@ -33,12 +33,10 @@ class CTFdStandardChallenge(BaseChallenge):
:param request:
:return:
"""
files = request.files.getlist('files[]')
# Create challenge
chal = Challenges(
name=request.form['name'],
description=request.form['desc'],
description=request.form['description'],
value=request.form['value'],
category=request.form['category'],
type=request.form['chaltype']
@ -63,6 +61,7 @@ class CTFdStandardChallenge(BaseChallenge):
db.session.commit()
files = request.files.getlist('files[]')
for f in files:
utils.upload_file(file=f, chalid=chal.id)
@ -105,7 +104,7 @@ class CTFdStandardChallenge(BaseChallenge):
:return:
"""
challenge.name = request.form['name']
challenge.description = request.form['desc']
challenge.description = request.form['description']
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
challenge.category = request.form['category']
@ -146,7 +145,7 @@ class CTFdStandardChallenge(BaseChallenge):
provided_key = request.form['key'].strip()
chal_keys = Keys.query.filter_by(chal=chal.id).all()
for chal_key in chal_keys:
if get_key_class(chal_key.key_type).compare(chal_key.flag, provided_key):
if get_key_class(chal_key.type).compare(chal_key.flag, provided_key):
return True, 'Correct'
return False, 'Incorrect'

View File

@ -1,111 +0,0 @@
<div class="col-md-6 col-md-offset-3">
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Name
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li role="presentation" class="active"><a href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
<li role="presentation"><a href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="new-desc-editor" class="form-control" name="desc" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-8">
<label for="key">Key
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is the flag/solution for the challenge."></i>
</label>
<input type="text" class="form-control" name="key" placeholder="Enter key">
</div>
<div class="form-vertical">
<div class="col-md-2">
<div class="radio">
<label>
<input type="radio" name="key_type[0]" value="static" checked>
Static
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="key_type[0]" value="regex">
Regex
</label>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-9">
<div class="checkbox">
<label>
<input name="hidden" type="checkbox">
Hide challenge on creation
</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" id="solve-attempts-checkbox">
Limit amount of solve attempts
</label>
</div>
<div id="solve-attempts-input" style="display: none;">
<label for="max_attempts">Maximum Attempts
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
</label>
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
</div>
</div>
<div class="row">
<div class="form-group">
<div class="col-md-9">
<label>Upload Challenge Files
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
</label>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<input type="file" name="files[]" multiple="multiple">
</div>
</div>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<input type="hidden" value="standard" name="chaltype" id="chaltype">
<div style="text-align:center">
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
</div>
</form>
</div>

View File

@ -0,0 +1,101 @@
<div class="col-md-12">
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Name
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
</label>
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
</div>
<div class="form-group">
<label>Flag
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is the flag or solution for your challenge. You can choose whether your flag is a static string or a regular expression."></i>
</label>
<input type="text" class="form-control" name="key" placeholder="Enter flag">
</div>
<div class="form-group">
<select class="custom-select" name="key_type[0]">
<option value="static">Static</option>
<option value="regex">Regex</option>
</select>
</div>
<div class="form-check">
<div class="checkbox">
<label class="form-check-label">
<input class="form-check-input" name="hidden" type="checkbox">
Hide challenge on creation
</label>
</div>
</div>
<div class="form-check">
<div class="checkbox">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" id="solve-attempts-checkbox">
Limit amount of solve attempts
</label>
</div>
</div>
<div class="form-group">
<div id="solve-attempts-input" style="display: none;">
<label for="max_attempts">Maximum Attempts
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
</label>
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
</div>
</div>
<div class="form-group">
<label>Upload Challenge Files
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
</label>
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<input type="hidden" value="standard" name="chaltype" id="chaltype">
<div class="form-group">
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
</div>
</form>
</div>

View File

@ -1,94 +0,0 @@
<div class="modal-dialog" role="document">
<div class="modal-content content">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#challenge" aria-controls="challenge" role="tab">Challenge</a></li>
{{#if_eq solves '-1 Solves'}}
{{else}}
<li role="presentation"><a href="#solves" aria-controls="solves" class="chal-solves" role="tab">{{solves}}</a></li>
{{/if_eq}}
</ul>
<div class="modal-body">
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="challenge">
<h3 class='chal-name text-center'>{{ name }}</h3>
<h4 class="chal-value text-center">{{ value }}</h4>
<div class="chal-tags text-center">
{{#each tags }}
<span class='label label-primary chal-tag'>{{this}}</span>
{{/each}}
</div>
<span class="chal-desc">{{ desc }}</span>
<div class="chal-hints file-row row">
{{#each hints}}
<div class='col-md-12 file-button-wrapper text-center'>
<a onclick="javascript:loadhint({{this.id}})">
{{#if this.hint }}
<label class='challenge-wrapper file-wrapper hide-text'>
View Hint
</label>
{{else}}
{{#if this.cost}}
<label class='challenge-wrapper file-wrapper hide-text'>
Unlock Hint for {{this.cost}} points
</label>
{{else}}
<label class='challenge-wrapper file-wrapper hide-text'>
View Hint
</label>
{{/if}}
{{/if}}
</a>
</div>
{{/each}}
</div>
<div class="chal-files file-row row text-center">
{{#each files}}
<div class='col-md-3 file-button-wrapper'>
<a class='file-button' href='files/{{this}}'>
<label class='challenge-wrapper file-wrapper hide-text'>
{{splitSlash this}}
</label>
</a>
</div>
{{/each}}
</div>
<div class="row submit-row">
<div class="col-md-9" style="padding-right:0px;padding-left:10px;">
<span class="input">
<input class="input-field" type="text" name="answer" id="answer-input" placeholder="Key" />
</span>
<input id="chal-id" type="hidden" value="{{id}}">
</div>
<div class="col-md-3 key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-theme btn-outlined pull-right" style="height:46.375px">Submit</button>
</div>
</div>
<div class="row notification-row">
<div id="result-notification" class="alert alert-dismissable text-center" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,103 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#challenge">Challenge</a>
</li>
{% if solves == '-1 Solves' %}
{% else %}
<li class="nav-item">
<a class="nav-link chal-solves" href="#solves">{{solves}}</a>
</li>
{% endif %}
</ul>
<div role="tabpanel">
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
<h3 class="chal-value text-center">{{ value }}</h3>
<div class="chal-tags text-center">
{% for tag in tags %}
<span class='badge badge-info chal-tag'>{{tag}}</span>
{% endfor %}
</div>
<span class="chal-desc">{{ desc }}</span>
<div class="chal-hints hint-row row">
{% for hint in hints %}
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
<a class="btn btn-info btn-hint" href="javascript:;" onclick="javascript:loadhint({{hint.id}})">
{% if hint.hint %}
<small>
View Hint
</small>
{% else %}
{% if hint.cost %}
<small>
Unlock Hint for {{hint.cost}} points
</small>
{% else %}
<small>
View Hint
</small>
{% endif %}
{% endif %}
</a>
</div>
{% endfor %}
</div>
<div class="row chal-files text-center pb-3">
{% for file in files %}
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='files/{{file}}'>
<small>
{{ file.split('/')[1] }}
</small>
</a>
</div>
{% endfor %}
</div>
<div class="row submit-row">
<div class="col-md-9 form-group">
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
<input id="chal-id" type="hidden" value="{{id}}">
</div>
<div class="col-md-3 form-group key-submit">
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
</div>
</div>
<div class="row notification-row">
<div class="col-md-12">
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
<strong id="result-message"></strong>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" id="solves">
<div class="row">
<div class="col-md-12">
<table class="table table-striped text-center">
<thead>
<tr>
<td><b>Name</b>
</td>
<td><b>Date</b>
</td>
</tr>
</thead>
<tbody id="chal-solves-names">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,267 +0,0 @@
<div id="update-challenge" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 class="chal-title text-center"></h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/chal/update">
<input name='nonce' type='hidden' value="{{ nonce }}">
<div class="form-group">
<label for="name">Name
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li role="presentation" class="active"><a href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
<li role="presentation"><a href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="desc-editor" class="form-control chal-desc" name="desc" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
</label>
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
</div>
<div class="checkbox">
<label>
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox">
Limit challenge attempts
</label>
</div>
<div class="form-group" id="chal-attempts-group" style="display:none;">
<label for="value">Max Attempts</label>
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value">
</div>
<input class="chal-id" type='hidden' name='id' placeholder='ID'>
<div class="checkbox">
<label>
<input class="chal-hidden" name="hidden" type="checkbox">
Hidden
</label>
</div>
<div class="form-group">
<a href="#" data-toggle="modal" data-target="#update-tags" class="btn btn-primary">Tags</a>
<a href="#" data-toggle="modal" data-target="#update-files" class="btn btn-primary">Files</a>
<a href="#" data-toggle="modal" data-target="#update-hints" class="btn btn-primary">Hints</a>
<a href="#" data-toggle="modal" data-target="#update-keys" class="btn btn-primary">Keys</a>
<a href="#" data-toggle="modal" data-target="#delete-chal" class="btn btn-danger">Delete</a>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div style="text-align:center">
<button class="btn btn-theme btn-outlined update-challenge-submit" type="submit">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="update-tags" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Tags</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="tag-insert">Value</label>
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
</div>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="tags-chal" name='chal' type='hidden'>
<div id="current-tags">
</div>
<br/>
<div id="chal-tags">
</div>
<div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" id="submit-tags">Update</button>
</div>
</div>
</div>
</div>
</div>
<div id="update-files" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Files</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/files/" enctype="multipart/form-data">
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="files-chal" name='chal' type='hidden'>
<input name='method' type='hidden' value='upload'>
<div id="current-files"></div>
<input type="hidden" name="method" value="upload">
<input type="file" name="files[]" multiple="multiple">
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" id="submit-files">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="update-hints" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Hints</h3>
</div>
<div class="modal-body">
<div class="row" style="text-align:center">
<a href="#" id="create-hint" class="btn btn-primary" style="margin-bottom:15px;">New Hint</a>
</div>
<div class='current-hints'>
<table id="hintsboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>Hint</b></td>
<td class="text-center"><b>Cost</b></td>
<td class="text-center"><b>Settings</b></td>
</tr>
</thead>
<tbody class="text-center">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div id="update-keys" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Keys</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/keys" style="text-align:center">
<a href="#" id="create-key" class="btn btn-primary" style="margin-bottom:15px;">New Key</a>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="keys-chal" name='chal' type='hidden'>
<div id="current-keys" class="row"></div>
</form>
</div>
</div>
</div>
</div>
<div id="delete-chal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Delete Challenge</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/chal/delete">
<input type="hidden" name="nonce" value="{{ nonce }}">
<input type="hidden" name="id" class="chal-id">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to delete this challenge?</p>
<p>Solves, wrong keys, files, tags will all be deleted.</p>
<a onclick="$('#delete-chal').modal('hide')" class="btn btn-primary">No</a>
<button class="btn btn-danger" id="delete-user" type="submit">Delete</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="create-keys" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<input type="hidden" class="chal-id" name="chal-id">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Create Key</h3>
</div>
<div class="modal-body">
<div class="create-keys-select-div">
<label for="create-keys-select" class="control-label">Choose Key Type</label>
<select class="form-control" id="create-keys-select">
</select>
</div>
<br>
<div id="create-keys-entry-div">
</div>
<br>
<div style="text-align:center;display:none;" id="create-keys-button-div">
<button id="create-keys-submit" class="btn btn-theme btn-outlined">Create Key</button>
</div>
</div>
</div>
</div>
</div>
<div id="edit-keys" class="modal fade" tabindex="-1">
</div>
<div id="hint-modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="hint-modal-title"></h3>
</div>
<div class="modal-body">
<form id="hint-modal-submit" method='POST'>
<input type="hidden" class="chal-id" name="chal">
<div class="form-group">
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint" placeholder="Hint"></textarea>
</div>
<div class="form-group">
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
</div>
<div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" id="hint-modal-button">Add Hint</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,232 +1,13 @@
//http://stackoverflow.com/a/2648463 - wizardry!
String.prototype.format = String.prototype.f = function() {
var s = this,
i = arguments.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
}
return s;
};
function load_hint_modal(method, hintid){
$('#hint-modal-hint').val('');
$('#hint-modal-cost').val('');
if (method == 'create'){
$('#hint-modal-submit').attr('action', '/admin/hints');
$('#hint-modal-title').text('Create Hint');
$("#hint-modal").modal();
} else if (method == 'update'){
$.get(script_root + '/admin/hints/' + hintid, function(data){
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
$('#hint-modal-hint').val(data.hint);
$('#hint-modal-cost').val(data.cost);
$('#hint-modal-title').text('Update Hint');
$("#hint-modal-button").text('Update Hint');
$("#hint-modal").modal();
});
}
}
function submitkey(chal, key) {
$.post(script_root + "/admin/chal/" + chal, {
key: key,
nonce: $('#nonce').val()
}, function (data) {
alert(data)
})
}
function create_key(chal, key, key_type) {
$.post(script_root + "/admin/keys", {
chal: chal,
key: key,
key_type: key_type,
nonce: $('#nonce').val()
}, function (data) {
if (data == "1"){
loadkeys(chal);
$("#create-keys").modal('toggle');
}
});
}
function loadkeys(chal){
$.get(script_root + '/admin/chal/' + chal + '/keys', function(data){
$('#keys-chal').val(chal);
keys = $.parseJSON(JSON.stringify(data));
keys = keys['keys'];
$('#current-keys').empty();
$.get(script_root + "/themes/admin/static/js/templates/admin-keys-table.hbs", function(data){
var template = Handlebars.compile(data);
var wrapper = {keys: keys, script_root: script_root};
$('#current-keys').append(template(wrapper));
});
});
}
function updatekeys(){
keys = [];
vals = [];
chal = $('#keys-chal').val()
$('.current-key').each(function(){
keys.push($(this).val());
})
$('#current-keys input[name*="key_type"]:checked').each(function(){
vals.push($(this).val());
})
$.post(script_root + '/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
loadchal(chal, true)
$('#update-keys').modal('hide');
}
function deletekey(key_id){
$.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){
if (data == "1") {
$('tr[name={0}]'.format(key_id)).remove();
}
});
}
function updatekey(){
var key_id = $('#key-id').val();
var chal = $('#keys-chal').val();
var key_data = $('#key-data').val();
var key_type = $('#key-type').val();
var nonce = $('#nonce').val();
$.post(script_root + '/admin/keys/'+key_id, {
'chal':chal,
'key':key_data,
'key_type': key_type,
'nonce': nonce
}, function(data){
if (data == "1") {
loadkeys(chal);
$('#edit-keys').modal('toggle');
}
});
}
function loadtags(chal){
$('#tags-chal').val(chal)
$('#current-tags').empty()
$('#chal-tags').empty()
$.get(script_root + '/admin/tags/'+chal, function(data){
tags = $.parseJSON(JSON.stringify(data))
tags = tags['tags']
for (var i = 0; i < tags.length; i++) {
tag = "<span class='label label-primary chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'>&#215;</a></span>"
$('#current-tags').append(tag)
};
$('.delete-tag').click(function(e){
deletetag(e.target.name)
$(e.target).parent().remove()
});
});
}
function deletetag(tagid){
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
}
// function deletechal(chalid){
// $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
// }
function edithint(hintid){
$.get(script_root + '/admin/hints/' + hintid, function(data){
console.log(data);
})
}
function deletehint(hintid){
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
if (jqXHR.status == 204){
var chalid = $('.chal-id').val();
loadhints(chalid);
}
});
}
function loadhints(chal){
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
var table = $('#hintsboard > tbody');
table.empty();
for (var i = 0; i < data.hints.length; i++) {
var hint = data.hints[i]
var hint_row = "<tr>" +
"<td class='hint-entry'>{0}</td>".format(hint.hint) +
"<td class='hint-cost'>{0}</td>".format(hint.cost) +
"<td class='hint-settings'><span>" +
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
"</span></td>" +
"</tr>";
table.append(hint_row);
}
});
}
function deletechal(chalid){
$.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
}
function updatetags(){
tags = [];
chal = $('#tags-chal').val()
$('#chal-tags > span > span').each(function(i, e){
tags.push($(e).text())
});
$.post(script_root + '/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()});
$('#update-tags').modal('toggle');
}
function updatefiles(){
chal = $('#files-chal').val();
var form = $('#update-files form')[0];
var formData = new FormData(form);
$.ajax({
url: script_root + '/admin/files/'+chal,
data: formData,
type: 'POST',
cache: false,
contentType: false,
processData: false,
success: function(data){
form.reset();
loadfiles(chal);
$('#update-files').modal('hide');
}
});
}
function loadfiles(chal){
$('#update-files form').attr('action', script_root+'/admin/files/'+chal)
$.get(script_root + '/admin/files/' + chal, function(data){
$('#files-chal').val(chal)
files = $.parseJSON(JSON.stringify(data));
files = files['files']
$('#current-files').empty()
for(x=0; x<files.length; x++){
filename = files[x].file.split('/')
filename = filename[filename.length - 1]
$('#current-files').append('<div class="row" style="margin:5px 0px;">'+'<a style="position:relative;top:10px;" href='+script_root+'/files/'+files[x].file+'>'+filename+'</a><a href="#" class="btn btn-danger" onclick="deletefile('+chal+','+files[x].id+', $(this))" value="'+files[x].id+'" style="float:right;">Delete</a></div>')
}
});
}
function deletefile(chal, file, elem){
$.post(script_root + '/admin/files/' + chal,{
'nonce': $('#nonce').val(),
'method': 'delete',
'file': file
}, function (data){
if (data == "1") {
elem.parent().remove()
}
});
}
$('#submit-key').click(function (e) {
submitkey($('#chalid').val(), $('#answer').val())
@ -237,42 +18,9 @@ $('#submit-keys').click(function (e) {
$('#update-keys').modal('hide');
});
$('#submit-tags').click(function (e) {
e.preventDefault();
updatetags()
});
$('#submit-files').click(function (e) {
e.preventDefault();
updatefiles()
});
$('#delete-chal form').submit(function(e){
e.preventDefault();
$.post(script_root + '/admin/chal/delete', $(this).serialize(), function(data){
console.log(data)
if (data){
loadchals();
}
else {
alert('There was an error');
}
})
$("#delete-chal").modal("hide");
$("#update-challenge").modal("hide");
});
$(".tag-insert").keyup(function (e) {
if (e.keyCode == 13) {
tag = $('.tag-insert').val()
tag = tag.replace(/'/g, '');
if (tag.length > 0){
tag = "<span class='label label-primary chal-tag'><span>"+tag+"</span><a class='delete-tag' onclick='$(this).parent().remove()'>&#215;</a></span>"
$('#chal-tags').append(tag)
}
$('.tag-insert').val("")
}
});
$('#limit_max_attempts').change(function() {
if(this.checked) {
@ -295,67 +43,11 @@ $('#new-desc-edit').on('shown.bs.tab', function (event) {
}
});
// Open New Challenge modal when New Challenge button is clicked
// $('.create-challenge').click(function (e) {
// $('#create-challenge').modal();
// });
$('#create-key').click(function(e){
$.get(script_root + '/admin/key_types', function(data){
$("#create-keys-select").empty();
var option = "<option> -- </option>";
$("#create-keys-select").append(option);
for (var key in data){
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
$("#create-keys-select").append(option);
}
$("#create-keys").modal();
});
});
$('#create-keys-select').change(function(){
var key_type_name = $(this).find("option:selected").text();
$.get(script_root + '/admin/key_types/' + key_type_name, function(key_data){
$.get(script_root + key_data.templates.create, function(template_data){
var template = Handlebars.compile(template_data);
$("#create-keys-entry-div").html(template());
$("#create-keys-button-div").show();
});
})
});
$('#create-keys-submit').click(function (e) {
e.preventDefault();
var chalid = $('#create-keys').find('.chal-id').val();
var key_data = $('#create-keys').find('input[name=key]').val();
var key_type = $('#create-keys-select').val();
create_key(chalid, key_data, key_type);
});
$('#create-hint').click(function(e){
e.preventDefault();
load_hint_modal('create');
});
$('#hint-modal-submit').submit(function (e) {
e.preventDefault();
var params = {}
$(this).serializeArray().map(function(x){
params[x.name] = x.value;
});
$.post(script_root + $(this).attr('action'), params, function(data){
loadhints(params['chal']);
});
$("#hint-modal").modal('hide');
});
function loadchal(id, update) {
$.get(script_root + '/admin/chals/' + id, function(obj){
$('#desc-write-link').click() // Switch to Write tab
$.get(script_root + '/admin/chal/' + id, function(obj){
$('#desc-write-link').click(); // Switch to Write tab
$('.chal-title').text(obj.name);
$('.chal-name').val(obj.name);
$('.chal-desc').val(obj.description);

View File

@ -0,0 +1,93 @@
<div id="update-challenge" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="chal-title text-center"></h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/chal/update">
<input name='nonce' type='hidden' value="{{ nonce }}">
<div class="form-group">
<label for="name">Name
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
</label>
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name">
</div>
<div class="form-group">
<label for="category">Category
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
</label>
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
Write
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
Preview
</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message:
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
</label>
<textarea id="desc-editor" class="form-control chal-desc" name="description" rows="10"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<label for="value">Value
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
</label>
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
</div>
<div class="checkbox">
<label>
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox">
Limit challenge attempts
</label>
</div>
<div class="form-group" id="chal-attempts-group" style="display:none;">
<label for="value">Max Attempts</label>
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value">
</div>
<input class="chal-id" type='hidden' name='id' placeholder='ID'>
<div class="checkbox">
<label>
<input class="chal-hidden" name="hidden" type="checkbox">
Hidden
</label>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div style="text-align:center">
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -18,9 +18,9 @@ class BaseKey(object):
class CTFdStaticKey(BaseKey):
id = 0
name = "static"
templates = { # Handlebars templates used for key editing & viewing
'create': '/plugins/keys/assets/static/create-static-modal.hbs',
'update': '/plugins/keys/assets/static/edit-static-modal.hbs',
templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/keys/assets/static/create-static-modal.njk',
'update': '/plugins/keys/assets/static/edit-static-modal.njk',
}
@staticmethod
@ -36,9 +36,9 @@ class CTFdStaticKey(BaseKey):
class CTFdRegexKey(BaseKey):
id = 1
name = "regex"
templates = { # Handlebars templates used for key editing & viewing
'create': '/plugins/keys/assets/regex/create-regex-modal.hbs',
'update': '/plugins/keys/assets/regex/edit-regex-modal.hbs',
templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/keys/assets/regex/create-regex-modal.njk',
'update': '/plugins/keys/assets/regex/edit-regex-modal.njk',
}
@staticmethod

View File

@ -1,17 +1,26 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Regex Key</h3>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Regex Key</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}" style="text-align:center">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
<input type="hidden" id="key-type" name="key_type" value="regex">
<input type="hidden" id="key-id">
<hr>
<div class="row">
<div class="form-group">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
<button id="submit-keys" class="btn btn-success float-right">Update</button>
</div>
</form>
</div>

View File

@ -1,17 +1,26 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Static Key</h3>
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Static Key</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}" style="text-align:center">
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data">
<input type="hidden" id="key-type" name="key_type" value="static">
<input type="hidden" id="key-id">
<hr>
<div class="row">
<div class="form-group">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
<button id="submit-keys" class="btn btn-success float-right">Update</button>
</div>
</form>
</div>

View File

@ -0,0 +1,144 @@
html, body, .container {
font-family: 'Lato', 'LatoOffline', sans-serif;
}
h1, h2 {
font-family: 'Raleway', 'RalewayOffline', sans-serif;
font-weight: 500;
letter-spacing: 2px;
}
a {
color: #337ab7;
text-decoration: none;
}
table > thead > tr > td {
/* Remove border line from thead of all tables */
/* It can overlap with other element styles */
border-top: none !important;
}
pre {
white-space: pre-wrap;
}
.fa.spinner {
margin-top: 225px;
text-align: center;
opacity: 0.5;
}
.spinner-error {
padding-top: 20vh;
text-align: center;
opacity: 0.5;
}
.jumbotron {
/*background-color: #343a40;*/
/*color: #FFF;*/
border-radius: 0;
text-align: center;
}
.form-control {
position: relative;
display: block;
/*padding: 0.8em;*/
border-radius: 0;
/*background: #f0f0f0;*/
/*color: #aaa;*/
font-weight: 400;
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-appearance: none;
}
.form-control:focus {
background-color: transparent;
border-color: #a3d39c;
box-shadow: 0 0 0 0.2rem #a3d39c;
transition: background-color 0.3s, border-color 0.3s;
}
.input-filled-valid {
background-color: transparent;
border-color: #a3d39c;
box-shadow: 0 0 0 0.2rem #a3d39c;
transition: background-color 0.3s, border-color 0.3s;
}
.btn-outlined.btn-theme {
background: none;
color: #545454;
border-color: #545454;
border: 3px solid;
}
.btn-outlined {
border-radius: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.btn {
letter-spacing: 1px;
text-decoration: none;
-moz-user-select: none;
border-radius: 0;
cursor: pointer;
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
white-space: nowrap;
font-size: 14px;
line-height: 20px;
font-weight: 700;
padding: 8px 20px;
}
.btn-info {
background-color: #5B7290 !important;
}
.alert {
border-radius: 0 !important;
padding: 0.8em;
}
#score-graph {
height: 450px;
display: block;
clear: both;
}
#solves-graph {
display: block;
height: 350px;
}
#keys-pie-graph {
height: 400px;
display: block;
}
#categories-pie-graph {
height: 400px;
display: block;
}
#solve-percentages-graph {
height: 400px;
display: block;
}
.no-decoration {
color: inherit !important;
text-decoration: none !important;
}
.no-decoration:hover {
color: inherit !important;
text-decoration: none !important;
}

View File

@ -0,0 +1,53 @@
.chal-desc {
padding-left: 30px;
padding-right: 30px;
font-size: 14px;
}
.modal-content {
border-radius: 0px;
max-width: 1000px;
padding: 1em;
margin: 0 auto;
}
.btn-info {
background-color: #5B7290 !important;
}
.badge-info {
background-color: #5B7290 !important;
}
.btn-file:nth-child(1):before {
content: "\f0ed";
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.solved-challenge {
background-color: #37d63e !important;
opacity: 0.4;
border: none;
}
.solved-challenge p:after {
content: "\f00c";
display: inline-block;
font: normal normal normal 18px/1 FontAwesome;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin-top: -10px;
margin-right: 25px;
position: absolute;
right: 0;
}
.key-submit .btn {
height: 51px;
}

View File

@ -0,0 +1,4 @@
/* Move down content because we have a fixed navbar that is 3.5rem tall */
body {
padding-top: 3.5rem;
}

View File

@ -0,0 +1,19 @@
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px; /* Margin bottom by footer height */
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
/*background-color: #f5f5f5;*/
}

View File

@ -18,6 +18,10 @@ a {
outline: 0;
}
img {
max-width: 100%;
}
.body-container {
min-height: 100%;
position: relative;
@ -106,6 +110,12 @@ table{
line-height: 66px;
}
#score-graph{
height: 450px;
display: block;
clear: both;
}
.fa.spinner {
margin-top: 225px;
text-align: center;
@ -113,16 +123,11 @@ table{
}
.spinner-error {
padding-top: 20vh;
text-align: center;
opacity: 0.5;
}
#score-graph{
height: 450px;
display: block;
clear: both;
}
#keys-pie-graph{
width: 50%;
float: left;
@ -385,118 +390,6 @@ table{
margin: 0px;
}
/* END OF INPUT CSS */
/* ADMIN CSS */
#submit-key{
display: none;
}
#chal > h1{
text-align: center
}
#chal > form{
width: 400px;
margin: 0 auto;
}
#chal > form > h3,h4{
text-align: center;
}
#chal > form > input{
display: none;
}
table{
width: 100%;
}
/*Not sure why foundation needs these two...*/
.top-bar input{
height: auto;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
font-size: 0.75rem;
}
.top-bar .button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
.dropdown{
background-color: #333 !important;
padding: 5px;
}
.dropdown button{
padding-top: 0.45rem;
padding-bottom: 0.35rem;
margin-bottom: 0;
font-size: 0.75rem;
}
#challenges button{
margin: 5px;
}
.row h1{
text-align: center;
}
.textbox{
height: 150px;
}
.chal-tag{
margin: 0 5px 0 5px;
}
#score-graph{
clear:both;
}
#keys-pie-graph{
width: 50%;
float: left;
}
#categories-pie-graph{
width: 50%;
float: left;
}
.chal-button{
width: 150px;
color: white;
border: None;
}
.chal-button > .chal-name {
margin-bottom: 0px;
height: 20px;
font-weight: bold;
}
.chal-button > .chal-points {
margin: -20px 0 0 0;
height: 20px;
}
.chal-button > .chal-percent{
font-size: 12px;
}
.remove-key{
float:right;
}
.footer {
position: absolute;
left: 0;
@ -506,6 +399,4 @@ table{
width: 100%;
}
.gray-text{
color: lightgray;
}
/* END OF INPUT CSS */

File diff suppressed because one or more lines are too long

View File

@ -1,98 +0,0 @@
//http://stackoverflow.com/a/2648463 - wizardry!
String.prototype.format = String.prototype.f = function() {
var s = this,
i = arguments.length;
while (i--) {
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
}
return s;
};
//http://stackoverflow.com/a/7616484
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length == 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
function load_edit_key_modal(key_id, key_type_name) {
$.get(script_root + '/admin/keys/' + key_id, function(key_data){
$.get(script_root + key_data.templates.update, function(template_data){
$('#edit-keys').empty();
var template = Handlebars.compile(template_data);
key_data['script_root'] = script_root;
key_data['nonce'] = $('#nonce').val();
$('#edit-keys').append(template(key_data));
$('#key-id').val(key_id);
$('#submit-keys').click(function (e) {
e.preventDefault();
updatekey()
});
$('#edit-keys').modal();
});
});
}
function load_chal_template(id, success_cb){
var obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0];
$.get(script_root + obj.type_data.templates.update, function(template_data){
var template = Handlebars.compile(template_data);
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
$.ajax({
url: script_root + obj.type_data.scripts.update,
dataType: "script",
success: success_cb,
cache: false,
});
});
}
function loadchals(){
$('#challenges').empty();
$.post(script_root + "/admin/chals", {
'nonce': $('#nonce').val()
}, function (data) {
categories = [];
challenges = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) {
if ($.inArray(challenges['game'][i].category, categories) == -1) {
categories.push(challenges['game'][i].category)
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-").hashCode() + '"><td class="col-md-1"><h3>' + challenges['game'][i].category + '</h3></td></tr>'))
}
};
for (var i = 0; i <= challenges['game'].length - 1; i++) {
var chal = challenges['game'][i];
var chal_button = $('<button class="chal-button col-md-2 theme-background" value="{0}"><h5>{1}</h5><p class="chal-points">{2}</p><span class="chal-percent">{3}% solved</span></button>'.format(chal.id, chal.name, chal.value, Math.round(chal.percentage_solved * 100)));
$('#' + challenges['game'][i].category.replace(/ /g,"-").hashCode()).append(chal_button);
};
$('#challenges button').click(function (e) {
id = this.value
load_chal_template(id, function(){
openchal(id);
});
});
// $('.create-challenge').click(function (e) {
// $('#new-chal-category').val($($(this).siblings()[0]).text().trim());
// $('#new-chal-title').text($($(this).siblings()[0]).text().trim());
// $('#new-challenge').modal();
// });
});
}
$(function(){
loadchals();
})

View File

@ -1,8 +1,9 @@
$.ajaxSetup({ cache: false });
function load_chal_template(challenge){
$.get(script_root + challenge.templates.create, function(template_data){
var template = Handlebars.compile(template_data);
$("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root}));
var template = nunjucks.compile(template_data);
$("#create-chal-entry-div").html(template.render({'nonce':nonce, 'script_root':script_root}));
$.getScript(script_root + challenge.scripts.create, function(){
console.log('loaded');
});
@ -23,9 +24,10 @@ $.get(script_root + '/admin/chal_types', function(data){
option.data('meta', challenge);
$("#create-chals-select").append(option);
}
$("#create-chals-select-div").show();
} else if (chal_type_amt == 1) {
var key = Object.keys(data)[0];
$("#create-chals-select").parent().parent().parent().empty();
$("#create-chals-select").empty();
load_chal_template(data[key]);
}
});

View File

@ -0,0 +1,182 @@
var challenges = {};
function load_chal_template(id, success_cb){
var obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0];
$.get(script_root + obj.type_data.templates.update, function(template_data){
var template = nunjucks.compile(template_data);
$("#update-modals-entry-div").html(template.render({'nonce':$('#nonce').val(), 'script_root':script_root}));
$.ajax({
url: script_root + obj.type_data.scripts.update,
dataType: "script",
success: success_cb,
cache: false,
});
});
}
function get_challenge(id){
var obj = $.grep(challenges['game'], function (e) {
return e.id == id;
})[0];
return obj;
}
function load_challenge_preview(id){
var chal = get_challenge(id);
var modal_template = chal.type_data.templates.modal;
var modal_script = chal.type_data.scripts.modal;
render_challenge_preview(chal, modal_template, modal_script)
}
function render_challenge_preview(chal, modal_template, modal_script){
var preview_window = $('#challenge-preview');
preview_window.empty();
$.get(script_root + modal_template, function (template_data) {
var template = nunjucks.compile(template_data);
var data = {
id: chal.id,
name: chal.name,
value: chal.value,
tags: chal.tags,
desc: chal.description,
files: chal.files,
hints: chal.hints
};
var challenge = template.render(data);
preview_window.append(challenge);
$.getScript(script_root + modal_script, function () {
preview_window.modal();
});
});
}
function loadchals(cb){
$.post(script_root + "/admin/chals", {
'nonce': $('#nonce').val()
}, function (data) {
var categories = [];
challenges = $.parseJSON(JSON.stringify(data));
for (var i = challenges['game'].length - 1; i >= 0; i--) {
if ($.inArray(challenges['game'][i].category, categories) == -1) {
categories.push(challenges['game'][i].category)
}
}
if (cb) {
cb();
}
});
}
loadchals(function(){
$('.edit-challenge').click(function (e) {
var id = $(this).attr('chal-id');
load_chal_template(id, function () {
openchal(id);
});
});
});
function submitkey(chal, key, nonce){
$.post(script_root + "/admin/chal/" + chal, {
key: key,
nonce: nonce
}, function (data) {
console.log(data);
var result = $.parseJSON(JSON.stringify(data));
var result_message = $('#result-message');
var result_notification = $('#result-notification');
var answer_input = $("#answer-input");
result_notification.removeClass();
result_message.text(result.message);
if (result.status == -1) {
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash
return
}
else if (result.status == 0) { // Incorrect key
result_notification.addClass('alert alert-danger alert-dismissable text-center');
result_notification.slideDown();
answer_input.removeClass("correct");
answer_input.addClass("wrong");
setTimeout(function () {
answer_input.removeClass("wrong");
}, 3000);
}
else if (result.status == 1) { // Challenge Solved
result_notification.addClass('alert alert-success alert-dismissable text-center');
result_notification.slideDown();
answer_input.val("");
answer_input.removeClass("wrong");
answer_input.addClass("correct");
}
else if (result.status == 2) { // Challenge already solved
result_notification.addClass('alert alert-info alert-dismissable text-center');
result_notification.slideDown();
answer_input.addClass("correct");
}
else if (result.status == 3) { // Keys per minute too high
result_notification.addClass('alert alert-warning alert-dismissable text-center');
result_notification.slideDown();
answer_input.addClass("too-fast");
setTimeout(function () {
answer_input.removeClass("too-fast");
}, 3000);
}
setTimeout(function () {
$('.alert').slideUp();
$('#submit-key').removeClass("disabled-button");
$('#submit-key').prop('disabled', false);
}, 3000);
});
}
$('.delete-challenge').click(function (e) {
var chal_id = $(this).attr('chal-id');
ezq({
title: "Delete Challenge",
body: "Are you sure you want to delete this challenge?",
success: function () {
$.post(script_root + '/admin/chal/delete', {'id': chal_id, 'nonce': $('#nonce').val()}, function (data) {
if (data == 1) {
location.reload();
}
else {
ezal({
title: "Error",
body: "There was an error"
});
}
});
}
});
});
$('.preview-challenge').click(function (e) {
var chal_id = $(this).attr('chal-id');
load_challenge_preview(chal_id);
});

View File

@ -0,0 +1,54 @@
var modal = '<div class="modal fade" tabindex="-1" role="dialog">' +
' <div class="modal-dialog" role="document">' +
' <div class="modal-content">' +
' <div class="modal-header">' +
' <h5 class="modal-title">\{0\}</h5>' +
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' +
' </button>' +
' </div>' +
' <div class="modal-body">' +
' <p>\{1\}</p>' +
' </div>' +
' <div class="modal-footer">' +
' </div>' +
' </div>' +
' </div>' +
'</div>';
function ezal(args){
var res = modal.format(args.title, args.body);
var obj = $(res);
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(args.button);
obj.find('.modal-footer').append(button);
$('main').append(obj);
obj.modal('show');
$(obj).on('hidden.bs.modal', function (e) {
$(this).modal('dispose');
})
}
function ezq(args){
var res = modal.format(args.title, args.body);
var obj = $(res);
var deny = '<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>';
var confirm = $('<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>');
obj.find('.modal-footer').append(deny);
obj.find('.modal-footer').append(confirm);
$('main').append(obj);
$(obj).on('hidden.bs.modal', function (e) {
$(this).modal('dispose');
});
$(confirm).click(function(){
args.success();
});
obj.modal('show');
}

View File

@ -0,0 +1,71 @@
function updatefiles(){
chal = $('#files-chal').val();
var form = $('#update-files form')[0];
var formData = new FormData(form);
$.ajax({
url: script_root + '/admin/files/'+chal,
data: formData,
type: 'POST',
cache: false,
contentType: false,
processData: false,
success: function(data){
form.reset();
loadfiles(chal);
}
});
}
function loadfiles(chal, cb){
$('#update-files form').attr('action', script_root+'/admin/files/'+chal)
$.get(script_root + '/admin/files/' + chal, function(data){
$('#files-chal').val(chal);
files = $.parseJSON(JSON.stringify(data));
files = files['files'];
$('#current-files').empty();
for(x=0; x<files.length; x++){
filename = files[x].file.split('/');
filename = filename[filename.length - 1];
var curr_file = '<div class="col-md-12"><a href="{2}/files/{3}">{4}</a> <i class="fa fa-times float-right" onclick="deletefile({0}, {1}, $(this))" value="{2}" ></i></div>'.format(
chal,
files[x].id,
script_root,
files[x].file,
filename
);
$('#current-files').append(curr_file);
}
if (cb){
cb();
}
});
}
function deletefile(chal, file, elem){
$.post(script_root + '/admin/files/' + chal,{
'nonce': $('#nonce').val(),
'method': 'delete',
'file': file
}, function (data){
if (data == "1") {
elem.parent().remove();
}
});
}
$('.edit-files').click(function (e) {
var chal_id = $(this).attr('chal-id');
loadfiles(chal_id, function () {
$('#update-files').modal();
});
});
$('#submit-files').click(function (e) {
e.preventDefault();
updatefiles()
});

View File

@ -0,0 +1,95 @@
function load_hint_modal(method, hintid){
$('#hint-modal-hint').val('');
$('#hint-modal-cost').val('');
if (method == 'create'){
$('#hint-modal-submit').attr('action', '/admin/hints');
$('#hint-modal-title').text('Create Hint');
$("#hint-modal").modal();
} else if (method == 'update'){
$.get(script_root + '/admin/hints/' + hintid, function(data){
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
$('#hint-modal-hint').val(data.hint);
$('#hint-modal-cost').val(data.cost);
$('#hint-modal-title').text('Update Hint');
$("#hint-modal-button").text('Update Hint');
$("#hint-modal").modal();
});
}
}
function edithint(hintid){
$.get(script_root + '/admin/hints/' + hintid, function(data){
console.log(data);
})
}
function deletehint(hintid){
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
if (jqXHR.status == 204){
var chalid = $("#update-hints").attr('chal-id');
loadhints(chalid);
}
});
}
function loadhints(chal, cb){
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
var table = $('#hintsboard > tbody');
table.empty();
for (var i = 0; i < data.hints.length; i++) {
var hint = data.hints[i]
var hint_row = "<tr>" +
"<td class='hint-entry d-table-cell w-75'><pre>{0}</pre></td>".format(htmlentities(hint.hint)) +
"<td class='hint-cost d-table-cell text-center'>{0}</td>".format(hint.cost) +
"<td class='hint-settings d-table-cell text-center'><span>" +
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
"</span></td>" +
"</tr>";
table.append(hint_row);
}
if (cb) {
cb();
}
});
}
$('.edit-hints').click(function (e) {
var chal_id = $(this).attr('chal-id');
loadhints(chal_id, function () {
$("#chal-id-for-hint").val(chal_id); // Preload the challenge ID so the form submits properly. Remove in later iterations
$("#update-hints").attr('chal-id', chal_id);
$('#update-hints').modal();
});
});
$('#create-hint').click(function (e) {
e.preventDefault();
load_hint_modal('create');
});
$('#hint-modal-submit').submit(function (e) {
e.preventDefault();
var params = {};
$(this).serializeArray().map(function (x) {
params[x.name] = x.value;
});
$.post(script_root + $(this).attr('action'), params, function (data) {
loadhints(params['chal']);
});
$("#hint-modal").modal('hide');
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr("href");
if (target == '#hint-preview') {
var obj = $('#hint-modal-hint');
var data = marked(obj.val());
$(event.target.hash).html(data, {'gfm': true, 'breaks': true});
}
});

View File

@ -0,0 +1,137 @@
function load_edit_key_modal(key_id, key_type_name) {
$.get(script_root + '/admin/keys/' + key_id, function (key_data) {
$.get(script_root + key_data.templates.update, function (template_data) {
$('#edit-keys').empty();
var template = nunjucks.compile(template_data);
key_data['script_root'] = script_root;
key_data['nonce'] = $('#nonce').val();
$('#edit-keys').append(template.render(key_data));
$('#key-id').val(key_id);
$('#submit-keys').click(function (e) {
e.preventDefault();
updatekey()
});
$('#edit-keys').modal();
});
});
}
function create_key(chal, key, key_type) {
$.post(script_root + "/admin/keys", {
chal: chal,
key: key,
key_type: key_type,
nonce: $('#nonce').val()
}, function (data) {
if (data == "1"){
loadkeys(chal);
$("#create-keys").modal('toggle');
}
});
}
function loadkeys(chal, cb){
$.get(script_root + '/admin/chal/' + chal + '/keys', function(data){
$('#keys-chal').val(chal);
var keys = $.parseJSON(JSON.stringify(data));
keys = keys['keys'];
$('#current-keys').empty();
$.get(script_root + "/themes/admin/static/js/templates/admin-keys-table.njk", function(data){
var template = nunjucks.compile(data);
var wrapper = {keys: keys, script_root: script_root};
$('#current-keys').append(template.render(wrapper));
if (cb) {
cb();
}
});
});
}
function updatekeys(){
keys = [];
vals = [];
chal = $('#keys-chal').val()
$('.current-key').each(function(){
keys.push($(this).val());
})
$('#current-keys input[name*="key_type"]:checked').each(function(){
vals.push($(this).val());
})
$.post(script_root + '/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
loadchal(chal, true)
$('#update-keys').modal('hide');
}
function deletekey(key_id){
$.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){
if (data == "1") {
$('tr[name={0}]'.format(key_id)).remove();
}
});
}
function updatekey(){
var key_id = $('#key-id').val();
var chal = $("#update-keys").attr('chal-id');
var key_data = $('#key-data').val();
var key_type = $('#key-type').val();
var nonce = $('#nonce').val();
$.post(script_root + '/admin/keys/'+key_id, {
'chal':chal,
'key':key_data,
'key_type': key_type,
'nonce': nonce
}, function(data){
if (data == "1") {
loadkeys(chal);
$('#edit-keys').modal('toggle');
}
});
}
$('.edit-keys').click(function (e) {
var chal_id = $(this).attr('chal-id');
loadkeys(chal_id, function () {
$("#update-keys").attr('chal-id', chal_id);
$('#update-keys').modal();
});
});
$('#create-key').click(function (e) {
$.get(script_root + '/admin/key_types', function (data) {
$("#create-keys-select").empty();
var option = "<option> -- </option>";
$("#create-keys-select").append(option);
for (var key in data) {
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
$("#create-keys-select").append(option);
}
$("#create-keys").modal();
});
});
$('#create-keys-select').change(function () {
var key_type_name = $(this).find("option:selected").text();
$.get(script_root + '/admin/key_types/' + key_type_name, function (key_data) {
$.get(script_root + key_data.templates.create, function (template_data) {
var template = nunjucks.compile(template_data);
$("#create-keys-entry-div").html(template.render());
$("#create-keys-button-div").show();
});
})
});
$('#create-keys-submit').click(function (e) {
e.preventDefault();
var chalid = $("#update-keys").attr('chal-id');
var key_data = $('#create-keys').find('input[name=key]').val();
var key_type = $('#create-keys-select').val();
create_key(chalid, key_data, key_type);
});

View File

@ -0,0 +1,29 @@
$(".form-control").bind({
focus: function() {
$(this).addClass('input-filled-valid' );
},
blur: function() {
if ($(this).val() === '') {
$(this).removeClass('input-filled-valid' );
}
}
});
$('.modal').on('show.bs.modal', function (e) {
$('.form-control').each(function () {
if ($(this).val()) {
$(this).addClass("input-filled-valid");
}
});
});
$(function () {
$('.form-control').each(function () {
if ($(this).val()) {
$(this).addClass("input-filled-valid");
}
});
$('[data-toggle="tooltip"]').tooltip()
})

View File

@ -0,0 +1,63 @@
function loadtags(chal, cb){
$('#tags-chal').val(chal)
$('#current-tags').empty()
$('#chal-tags').empty()
$.get(script_root + '/admin/tags/'+chal, function(data){
tags = $.parseJSON(JSON.stringify(data))
tags = tags['tags']
for (var i = 0; i < tags.length; i++) {
tag = "<span class='badge badge-primary mx-1 chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'> &#215;</a></span>"
$('#current-tags').append(tag)
};
$('.delete-tag').click(function(e){
deletetag(e.target.name)
$(e.target).parent().remove()
});
if (cb) {
cb();
}
});
}
function deletetag(tagid){
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
}
function updatetags(){
tags = [];
chal = $('#tags-chal').val();
$('#chal-tags > span > span').each(function(i, e){
tags.push($(e).text());
});
$.post(script_root + '/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()});
$('#update-tags').modal('toggle');
}
$('.edit-tags').click(function (e) {
var chal_id = $(this).attr('chal-id');
loadtags(chal_id, function(){
$('#update-tags').modal();
});
});
$(".tag-insert").keyup(function (e) {
if (e.keyCode == 13) {
var tag = $('.tag-insert').val()
tag = tag.replace(/'/g, '');
if (tag.length > 0) {
tag = "<span class='badge badge-primary mx-1 chal-tag'><span>" + tag + "</span><a class='delete-tag' onclick='$(this).parent().remove()'> &times;</a></span>"
$('#chal-tags').append(tag);
}
$('.tag-insert').val("");
}
});
$('#submit-tags').click(function (e) {
e.preventDefault();
updatetags()
});

View File

@ -13,15 +13,16 @@ function cumulativesum (arr) {
}
function scoregraph () {
var times = []
var scores = []
var times = [];
var scores = [];
var teamname = $('#team-id').text()
$.get(script_root + '/admin/solves/'+teamid(), function( data ) {
var solves = $.parseJSON(JSON.stringify(data));
solves = solves['solves'];
if (solves.length == 0)
if (solves.length == 0) {
return;
}
for (var i = 0; i < solves.length; i++) {
var date = moment(solves[i].time * 1000);
@ -34,13 +35,34 @@ function scoregraph () {
{
x: times,
y: scores,
type: 'scatter'
type: 'scatter',
marker: {
color: colorhash(teamname + teamid())
},
line: {
color: colorhash(teamname + teamid())
}
}
];
var layout = {
title: 'Score over Time'
title: 'Score over Time',
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
hovermode: 'closest',
xaxis: {
showgrid: false,
showspikes: true,
},
yaxis: {
showgrid: false,
showspikes: true,
},
legend: {
"orientation": "h"
}
};
$('#score-graph').empty();
Plotly.newPlot('score-graph', data, layout);
});

View File

@ -7,16 +7,16 @@
</tr>
</thead>
<tbody>
{{#each keys}}
<tr name="{{this.id}}">
<td class="key-type">{{this.type_name}}</td>
<td class="key-value">{{this.key}}</td>
{% for key in keys %}
<tr name="{{key.id}}">
<td class="key-type">{{key.type_name}}</td>
<td class="key-value">{{key.key}}</td>
<td class="text-center"><span>
<i role="button" class="fa fa-pencil-square-o" onclick="javascript:load_edit_key_modal({{this.id}}, '{{this.type_name}}')"></i>
<i role="button" class="fa fa-times" onclick="javascript:deletekey({{this.id}})"></i>
<i role="button" class="fa fa-pencil-square-o" onclick="javascript:load_edit_key_modal({{key.id}}, '{{key.type_name}}')"></i>
<i role="button" class="fa fa-times" onclick="javascript:deletekey({{key.id}})"></i>
</span>
</td>
</tr>
{{/each}}
{% endfor %}
</tbody>
</table>

View File

@ -40,28 +40,23 @@ String.prototype.hashCode = function() {
return hash;
};
function colorhash (x) {
color = ""
for (var i = 20; i <= 60; i+=20){
x += i
x *= i
color += x.toString(16)
};
return "#" + color.substring(0, 6)
function colorhash(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var colour = '#';
for (var i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xFF;
colour += ('00' + value.toString(16)).substr(-2);
}
return colour;
}
function htmlentities(string) {
return $('<div/>').text(string).html();
}
Handlebars.registerHelper('if_eq', function(a, b, opts) {
if (a == b) {
return opts.fn(this);
} else {
return opts.inverse(this);
}
});
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
jQuery.each(["put", "delete"], function(i, method) {
jQuery[method] = function(url, data, callback, type) {

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

File diff suppressed because one or more lines are too long

View File

@ -2,96 +2,123 @@
<html>
<head>
<title>Admin Panel</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css" />
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/style.css">
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/style.css">
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/handlebars.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>
{% block stylesheets %} {% endblock %}
<title>Admin Panel</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css" />
<link rel="stylesheet" href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css'>
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/jumbotron.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/sticky-footer.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/base.css">
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/nunjucks.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>
{% block stylesheets %} {% endblock %}
</head>
<body>
<div class="body-container">
<div class="navbar navbar-inverse home">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse" type="button">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
</div>
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0px">
<ul class="nav navbar-nav navbar-nav-right">
<li><a href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">Pages <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ request.script_root }}/admin/pages">All Pages</a></li>
<li><a href="{{ request.script_root }}/admin/pages?mode=create">New Page</a></li>
</ul>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<div class="container">
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars"
aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="base-navbars">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="true">Pages</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages">All Pages</a>
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages?mode=create">New Page</a>
</div>
</li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="true">Submissions</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ request.script_root }}/admin/correct_keys/1">Correct Submissions</a>
<a class="dropdown-item" href="{{ request.script_root }}/admin/wrong_keys/1">Wrong Submissions</a>
</div>
</li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/config">Config</a></li>
{% set plugin_menu = get_admin_plugin_menu_bar() %}
{% set plugins = get_configurable_plugins() %}
{% if plugin_menu or plugins %}
<li class="nav-item">
<a class="nav-link d-none d-md-block d-lg-block">|</a>
</li>
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>
{% set plugin_menu = get_admin_plugin_menu_bar() %}
{% set plugins = get_configurable_plugins() %}
{% if plugin_menu or plugins %}
<li><a style="padding-left:0px;padding-right:0px;">|</a></li>
{% for menu in plugin_menu %}
<li><a href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a></li>
{% endfor %}
{% if plugins %}
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for plugin in plugins %}
<li><a href="{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
{% endfor %}
</ul>
</li>
{% for menu in plugin_menu %}
{% if request.script_root %}
{% set route = '/' + request.script_root + '/' + menu.route %}
{% else %}
{% set route = '/' + menu.route %}
{% endif %}
<li class="nav-item"><a class="nav-link" href="{{ route }}">{{ menu.title }}</a></li>
{% endfor %}
{% if plugins %}
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for plugin in plugins %}
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endif %}
</ul>
</div>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container main-container">
{% block content %}
{% endblock %}
</div>
<div class="navbar navbar-bottom footer">
<p class="text-muted text-center">
<a style="text-decoration:none;" href="https://ctfd.io"><sub>Powered by CTFd</sub></a>
</p>
{% if get_config('version_latest') %}
<div class="container-fluid bg-warning text-center py-3">
<div class="row">
<div class="col-md-12">
<a class="btn btn-warning" href="{{ get_config('version_latest') }}">
A new CTFd version is available!
</a>
</div>
</div>
</div>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
{% block scripts %} {% endblock %}
{% endif %}
<main role="main">
{% block content %}
{% endblock %}
</main>
<footer class="footer">
<div class="container text-center">
<a href="https://ctfd.io">
<small class="text-muted">Powered by CTFd</small>
</a>
</div>
</footer>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/ezq.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
{% block scripts %} {% endblock %}
</body>
</html>

View File

@ -0,0 +1,126 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
</style>
{% endblock %}
{% block content %}
{% include "admin/modals/challenges/challenges.html" %}
{% include "admin/modals/tags/tags.html" %}
{% include "admin/modals/files/files.html" %}
{% include "admin/modals/hints/hints.html" %}
{% include "admin/modals/keys/keys.html" %}
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>
<input type="hidden" value="{{ nonce }}" id="nonce">
<div id="create-challenge" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>New Challenge</h3>
</div>
</div>
</div>
</div>
<div id="update-modals-entry-div">
</div>
<div class="jumbotron">
<div class="container">
<h1>Challenges
<a class="no-decoration" href="{{ request.script_root }}/admin/chal/new">
<i class="fa fa-plus-circle" role="button" data-toggle="tooltip" title="Create Challenge"></i>
</a>
</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<div>
<table id="challenges" class="table table-striped">
<thead>
<tr>
<td><b>ID</b></td>
<td><b>Name</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Category</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Value</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Type</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Status</b></td>
<td style="width: 200px;"><b>Settings</b></td>
</tr>
</thead>
<tbody>
{% for challenge in challenges %}
<tr>
<td>{{ challenge.id }}</td>
<td>{{ challenge.name }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.category }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.value }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.type }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">
{% if challenge.hidden %}
<button class="btn-sm btn-danger" type="submit" disabled>Hidden</button>
{% else %}
<button class="btn-sm btn-success" type="submit" disabled>Visible</button>
{% endif %}
</td>
<td>
<i class="fa fa-pencil edit-challenge" aria-hidden="true" data-toggle="tooltip"
data-placement="top" chal-id="{{ challenge.id }}"
title="Edit {{ challenge.name }}"></i>
<i class="fa fa-file-text-o preview-challenge" aria-hidden="true" data-toggle="tooltip"
data-placement="top" chal-id="{{ challenge.id }}"
title="Preview {{ challenge.name }}"></i>
<span>&nbsp; &nbsp;</span>
<i class="fa fa-flag edit-keys" aria-hidden="true" data-toggle="tooltip" data-placement="top"
title="Edit Flags" chal-id="{{ challenge.id }}"></i>
<i class="fa fa-cloud-download edit-files" aria-hidden="true" data-toggle="tooltip"
data-placement="top" title="Edit Files" chal-id="{{ challenge.id }}"></i>
<i class="fa fa-tags edit-tags" aria-hidden="true" data-toggle="tooltip" data-placement="top"
title="Edit Tags" chal-id="{{ challenge.id }}"></i>
<i class="fa fa-question-circle edit-hints" aria-hidden="true" data-toggle="tooltip"
data-placement="top" title="Edit Hints" chal-id="{{ challenge.id }}"></i>
<span>&nbsp; &nbsp;</span>
<i class="fa fa-times delete-challenge" aria-hidden="true" data-toggle="tooltip"
data-placement="top" title="Delete {{ challenge.name }}" chal-id="{{ challenge.id }}"></i>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/challenges/chalboard.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/files/files-utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/hints/hints-utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/keys/keys-utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/tags/tags-utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/multi-modal.js"></script>
{% endblock %}

View File

@ -1,56 +0,0 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary { background-color: #337ab7; }
.btn-danger { background-color: #d9534f; }
.col-md-4 { margin-bottom: 15px; }
.key-remove-button { margin-top: 10px; }
.delete-tag { color: white; margin-left: 3px; cursor: pointer; }
</style>
{% endblock %}
{% block content %}
<div id="email-user" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>
<input type="hidden" value="{{ nonce }}" id="nonce">
<div id="create-challenge" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>New Challenge</h3>
</div>
</div>
</div>
</div>
<div id="update-modals-entry-div">
</div>
<div style="text-align:center">
<br>
<h1 class="text-center">Challenges</h1>
<a class="btn btn-theme btn-outlined create-challenge" href="{{ request.script_root }}/admin/chal/new" role="button">New Challenge</a>
<div>
<table id='challenges'>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/chalboard.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/multi-modal.js"></script>
{% endblock %}

View File

@ -1,33 +1,34 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary { background-color: #337ab7; }
.btn-danger { background-color: #d9534f; }
.col-md-4 { margin-bottom: 15px; }
.key-remove-button { margin-top: 10px; }
.delete-tag { color: white; margin-left: 3px; cursor: pointer; }
</style>
{% endblock %}
{% block content %}
<div id="create-challenge" class="container main-container">
<h1 class="text-center">Create Challenge</h1>
<div class="jumbotron">
<div class="container">
<h1>Create Challenge</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="create-chals-select-div">
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
<select class="form-control" id="create-chals-select">
</select>
<div class="col-md-12">
<div class="row">
<div class="col-md-6 offset-md-3">
<div id="create-chals-select-div" style="display: none;">
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
<select class="form-control" id="create-chals-select">
</select>
</div>
<br>
<div id="create-chal-entry-div">
</div>
</div>
</div>
</div>
</div>
<br>
<div class="row">
<div id="create-chal-entry-div">
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
@ -36,5 +37,6 @@
<script>
nonce = "{{ nonce }}";
</script>
<script src="{{ request.script_root }}/themes/admin/static/js/chal-new.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/challenges/chal-new.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
{% endblock %}

View File

@ -1,31 +1,34 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<h1>Config</h1>
<form method="POST" autocomplete="off">
<div class="col-md-2 col-md-offset-2">
<ul class="nav nav-pills nav-stacked">
<li role="presentation" class="active">
<a href="#appearance-section" aria-controls="appearance-section" role="tab" data-toggle="tab">Appearance</a>
<div class="jumbotron">
<div class="container">
<h1>Configuration</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link rounded-0 active" href="#appearance-section" aria-controls="appearance-section" role="tab" data-toggle="tab">Appearance</a>
</li>
<li role="presentation">
<a href="#accounts-section" aria-controls="accounts-section" role="tab" data-toggle="tab">Accounts</a>
<li class="nav-item">
<a class="nav-link rounded-0" href="#accounts-section" aria-controls="accounts-section" role="tab" data-toggle="tab">Accounts</a>
</li>
<li role="presentation">
<a href="#email-section" aria-controls="email-section" role="tab" data-toggle="tab">Email</a>
<li class="nav-item">
<a class="nav-link rounded-0" href="#email-section" aria-controls="email-section" role="tab" data-toggle="tab">Email</a>
</li>
<li role="presentation">
<a href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">CTF Time</a>
<li class="nav-item">
<a class="nav-link rounded-0" href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">Time</a>
</li>
<li role="presentation">
<a href="#backup-section" aria-controls="backup-section" role="tab" data-toggle="tab">Backup</a>
<li class="nav-item">
<a class="nav-link rounded-0" href="#backup-section" aria-controls="backup-section" role="tab" data-toggle="tab">Backup</a>
</li>
</ul>
<br><br>
<button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-theme btn-outlined pull-left">Update</button>
</div>
<div class="col-md-6">
<div class="col-md-9">
<form method="POST" autocomplete="off" class="w-100">
{% for error in errors %}
<div class="alert alert-danger alert-dismissable" role="alert">
<span class="sr-only">Error:</span>
@ -53,7 +56,7 @@
</select>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="workshop_mode" name="workshop_mode" type="checkbox"
{% if workshop_mode %}checked{% endif %}>
@ -63,29 +66,44 @@
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="paused" name="paused" type="checkbox"
{% if paused %}checked{% endif %}>
Pause CTF <i class="fa fa-question-circle gray-text" data-toggle="tooltip"
data-placement="right"
title="Prevent users from submitting answers until unpaused. Challenges can still be viewed."></i>
</label>
</div>
<div class="form-check">
<label>
<input id="hide_scores" name="hide_scores" type="checkbox" {% if hide_scores %}checked{% endif %}>
Hide Scores from public view
</label>
</div>
<div class="form-group">
<label>CSS editor</label>
<textarea class="form-control" id="css-editor" name="css" rows="7">{{ css }}</textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="accounts-section">
<div class="checkbox">
<div class="form-check">
<label>
<input id="verify_emails" name="verify_emails" type="checkbox" {% if verify_emails %}checked{% endif %}>
Only allow users with verified emails
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="view_challenges_unregistered" name="view_challenges_unregistered" type="checkbox" {% if view_challenges_unregistered %}checked{% endif %}>
Unregistered users can view challenges
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="view_scoreboard_if_authed" name="view_scoreboard_if_authed" type="checkbox"
{% if view_scoreboard_if_authed %}checked{% endif %}>
@ -93,14 +111,14 @@
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="prevent_registration" name="prevent_registration" type="checkbox" {% if prevent_registration %}checked{% endif %}>
Prevent public registration
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="prevent_name_change" name="prevent_name_change" type="checkbox" {% if prevent_name_change %}checked{% endif %}>
Prevent team name changes
@ -108,17 +126,17 @@
</div>
</div>
<div role="tabpanel" class="tab-pane" id="email-section">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item" class="active">
<a class="nav-link active" href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
</li>
<li role="presentation">
<a href="#mailgun" aria-controls="mailgun" role="tab" data-toggle="tab">Mailgun</a>
<li class="nav-item">
<a class="nav-link" href="#mailgun" aria-controls="mailgun" role="tab" data-toggle="tab">Mailgun</a>
</li>
</ul>
<div class="form-group">
<label for="start">Mail From Address:</label>
<label>Mail From Address:</label>
<input class="form-control" id='mailfrom_addr' name='mailfrom_addr' type='text'
placeholder="Email address used to send email"
{% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
@ -128,19 +146,19 @@
<div role="tabpanel" class="tab-pane active" id="mailserver">
<div class="form-group">
<label for="start">Mail Server Address:</label>
<label>Mail Server Address:</label>
<input class="form-control" id='mail_server' name='mail_server' type='text'
placeholder="Mail Server Address"
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mail Server Port:</label>
<label>Mail Server Port:</label>
<input class="form-control" id='mail_port' name='mail_port' type='text'
placeholder="Mail Server Port"
{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="mail_useauth" name="mail_useauth" type="checkbox" {% if mail_useauth %}checked{% endif %}>
Use Mail Server Username and Password
@ -148,7 +166,7 @@
</div>
<div id="mail_username_password">
<div class="form-group">
<label for="start">Username:</label>
<label>Username:</label>
{% if mail_username is defined and mail_username != None %}
<label for="mail_u"><sup>A mail server username is currently set</sup></label>
{% endif %}
@ -163,17 +181,17 @@
<input class="form-control" id='mail_p' name='mail_p' autocomplete='off' type='password'
placeholder="Password">
</div>
<sup>Uncheck setting and update to remove username and password</sup>
<br>
<br>
<sup>Uncheck setting and update to remove username and password</sup>
<br>
<br>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="mail_ssl" name="mail_ssl" type="checkbox" {% if mail_ssl %}checked{% endif %}>
SSL
</label>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="mail_tls" name="mail_tls" type="checkbox" {% if mail_tls %}checked{% endif %}>
TLS
@ -183,13 +201,13 @@
<div role="tabpanel" class="tab-pane" id="mailgun">
<div class="form-group">
<label for="start">Mailgun API Base URL:</label>
<label>Mailgun API Base URL:</label>
<input class="form-control" id='mg_base_url' name='mg_base_url' type='text'
placeholder="Mailgun API Base URL"
{% if mg_base_url is defined and mg_base_url != None %}value="{{ mg_base_url }}"{% endif %}>
</div>
<div class="form-group">
<label for="start">Mailgun API Key:</label>
<label>Mailgun API Key:</label>
<input class="form-control" id='mg_api_key' name='mg_api_key' type='text'
placeholder="Mailgun API Key"
{% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
@ -198,48 +216,54 @@
</div>
</div>
<div role="tabpanel" class="tab-pane" id="ctftime-section">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a>
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a class="nav-link active" href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a>
</li>
<li role="presentation">
<a href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a>
<li class="nav-item">
<a class="nav-link" href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a>
</li>
<li role="presentation">
<a href="#freeze-date" aria-controls="freeze-date" role="tab" data-toggle="tab">Freeze Time</a>
<li class="nav-item">
<a class="nav-link" href="#freeze-date" aria-controls="freeze-date" role="tab" data-toggle="tab">Freeze Time</a>
</li>
<sub style="float:right;">* All time fields required</sub>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="start-date">
<div class="row" id="start-date">
<div class="form-group col-xs-2">
<div class="col-md-12">
<p>This is the time when the competition will begin. Challenges will automatically
unlock and users will be able to submit answers.</p>
<sub class="text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">
<label for="start-month">Month:</label>
<input class="form-control start-date" id='start-month' name='start-month' min="0" max="12" type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="start-day">Day:</label>
<input class="form-control start-date" id='start-day' name='start-day' min="0" max="31" type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-3">
<label for="start-year">Year:</label>
<input class="form-control start-date" id='start-year' name='start-year' type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="start-hour">Hour:</label>
<input class="form-control start-date" id='start-hour' name='start-hour' min="0" max="23" type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-3">
<label for="start-minute">Minute:</label>
<input class="form-control start-date" id='start-minute' name='start-minute' min="0" max="59" type='number'>
</div>
<div class="form-group col-xs-4">
<div class="form-group col-md-12">
<label for="start-timezone">Timezone:</label>
<select class="form-control start-date" id="start-timezone">
<script>
@ -252,20 +276,20 @@
</select>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="start-local">Local Time:</label>
<input class="form-control" id='start-local' type='text'
placeholder="Start Date (Local Time)" readonly>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="start-zonetime">Timezone Time:</label>
<input class="form-control" id='start-zonetime' type='text'
placeholder="Start Date (Timezone Time)" readonly>
</div>
<div class="form-group col-xs-12">
<label for="start">UTC Timestamp:</label>
<div class="form-group col-md-12">
<label>UTC Timestamp:</label>
<input class="form-control" id='start' name='start' type='text'
placeholder="Start Date (UTC timestamp)"
{% if start is defined and start != None %}value="{{ start }}"{% endif %} readonly>
@ -274,36 +298,42 @@
</div>
<div role="tabpanel" class="tab-pane" id="end-date">
<div class="row" id="end-date">
<div class="form-group col-xs-2">
<div class="col-md-12">
<p>This is the time when the competition will end. Challenges will automatically
close and users won't be able to submit answers.</p>
<sub class="text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">
<label for="end-month">Month:</label>
<input class="form-control end-date" id='end-month' name='end-month' min="0" max="12"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="end-day">Day:</label>
<input class="form-control end-date" id='end-day' name='end-day' min="0" max="31"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-4">
<label for="end-year">Year:</label>
<input class="form-control end-date" id='end-year' name='end-year' type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="end-hour">Hour:</label>
<input class="form-control end-date" id='end-hour' name='end-hour' min="0" max="23"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="end-minute">Minute:</label>
<input class="form-control end-date" id='end-minute' name='end-minute' min="0"
max="59" type='number'>
</div>
<div class="form-group col-xs-4">
<div class="form-group col-md-12">
<label for="end-timezone">Timezone:</label>
<select class="form-control end-date" id="end-timezone">
<script>
@ -316,19 +346,19 @@
</select>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="end-local">Local Time:</label>
<input class="form-control" id='end-local' type='text'
placeholder="End Date (Local Time)" readonly>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="end-zonetime">Timezone Time:</label>
<input class="form-control" id='end-zonetime' type='text'
placeholder="End Date (Timezone Time)" readonly>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="end">UTC Timestamp:</label>
<input class="form-control" id='end' name='end' type='text'
placeholder="End Date (UTC timestamp)"
@ -338,36 +368,42 @@
</div>
<div role="tabpanel" class="tab-pane" id="freeze-date">
<div class="row" id="freeze-date">
<div class="form-group col-xs-2">
<div class="col-md-12">
<p>Freeze time specifies the timestamp that the competition will be frozen to.
All solves before the freeze time will be shown, but new solves won't be shown to users. </p>
<sub class="text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">
<label for="freeze-month">Month:</label>
<input class="form-control freeze-date" id='freeze-month' name='freeze-month' min="0" max="12"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="freeze-day">Day:</label>
<input class="form-control freeze-date" id='freeze-day' name='freeze-day' min="0" max="31"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-4">
<label for="freeze-year">Year:</label>
<input class="form-control freeze-date" id='freeze-year' name='freeze-year' type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="freeze-hour">Hour:</label>
<input class="form-control freeze-date" id='freeze-hour' name='freeze-hour' min="0" max="23"
type='number'>
</div>
<div class="form-group col-xs-2">
<div class="form-group col-md-2">
<label for="freeze-minute">Minute:</label>
<input class="form-control freeze-date" id='freeze-minute' name='freeze-minute' min="0"
max="59" type='number'>
</div>
<div class="form-group col-xs-4">
<div class="form-group col-md-12">
<label for="freeze-timezone">Timezone:</label>
<select class="form-control freeze-date" id="freeze-timezone">
<script>
@ -380,19 +416,19 @@
</select>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="freeze-local">Local Time:</label>
<input class="form-control" id='freeze-local' type='text'
placeholder="Freeze Date (Local Time)" readonly>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="freeze-zonetime">Timezone Time:</label>
<input class="form-control" id='freeze-zonetime' type='text'
placeholder="Freeze Date (Timezone Time)" readonly>
</div>
<div class="form-group col-xs-12">
<div class="form-group col-md-12">
<label for="freeze">UTC Timestamp:</label>
<input class="form-control" id='freeze' name='freeze' type='text'
placeholder="Freeze Date (UTC timestamp)"
@ -402,91 +438,87 @@
</div>
</div>
<div class="checkbox">
<div class="form-check">
<label>
<input id="view_after_ctf" name="view_after_ctf" type="checkbox" {% if view_after_ctf %}checked{% endif %}>
Allow challenges to be viewed after CTF end
</label>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="backup-section">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#export-ctf" aria-controls="export-ctf" role="tab" data-toggle="tab">Export</a>
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item active">
<a class="nav-link active" href="#export-ctf" aria-controls="export-ctf" role="tab" data-toggle="tab">Export</a>
</li>
<li role="presentation">
<a href="#import-ctf" aria-controls="import-ctf" role="tab" data-toggle="tab">Import</a>
<li class="nav-item">
<a class="nav-link" href="#import-ctf" aria-controls="import-ctf" role="tab" data-toggle="tab">Import</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="export-ctf">
<div class="row">
<div class="form-group col-xs-8">
<div class="checkbox">
<label>
<input class="export-config" value="challenges" type="checkbox" checked>Challenges
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="teams" type="checkbox" checked>Teams
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="checkbox">
<label>
<input class="export-config" value="metadata" type="checkbox" checked>Configuration
</label>
</div>
<a href="{{ request.script_root }}/admin/export" id="export-button" class="btn btn-default">Export</a>
<div class="form-group">
<div class="form-check">
<label>
<input class="export-config" value="challenges" type="checkbox" checked> Challenges
</label>
</div>
<div class="form-check">
<label>
<input class="export-config" value="teams" type="checkbox" checked> Teams
</label>
</div>
<div class="form-check">
<label>
<input class="export-config" value="both" type="checkbox" checked> Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="form-check">
<label>
<input class="export-config" value="metadata" type="checkbox" checked> Configuration
</label>
</div>
<a href="{{ request.script_root }}/admin/export" id="export-button" class="btn btn-warning">Export</a>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="import-ctf">
<div class="row" id="import-div">
<br>
<div class="form-group col-xs-8">
<label for="container-files">Import File
<input type="file" name="backup" id="import-file" accept=".zip">
<div class="form-group">
<label for="container-files">Import File</label>
<input class="form-control-file" type="file" name="backup" id="import-file" accept=".zip">
</div>
<div class="form-group">
<div class="form-check">
<label>
<input class="import-config" value="challenges" type="checkbox" checked> Challenges
</label>
</div>
<div class="form-group col-xs-8">
<div class="checkbox">
<label>
<input class="import-config" value="challenges" type="checkbox" checked>Challenges
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="teams" type="checkbox" checked>Teams
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="checkbox">
<label>
<input class="import-config" value="metadata" type="checkbox" checked>Configuration
</label>
</div>
<input id="import-button" type="submit" class="btn btn-default" value="Import">
<div class="form-check">
<label>
<input class="import-config" value="teams" type="checkbox" checked> Teams
</label>
</div>
<div class="form-check">
<label>
<input class="import-config" value="both" type="checkbox" checked> Solves, Wrong Keys, Unlocks
</label>
</div>
<div class="form-check">
<label>
<input class="import-config" value="metadata" type="checkbox" checked> Configuration
</label>
</div>
<input id="import-button" type="submit" class="btn btn-warning" value="Import">
</div>
</div>
</div>
</div>
<button type="submit" id="submit" tabindex="5"
class="btn btn-md btn-primary float-right">Update
</button>
</div>
</form>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,110 +1,104 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary { background-color: #337ab7; }
.btn-danger { background-color: #d9534f; }
</style>
{% endblock %}
{% block content %}
<div class="row">
<h1>Correct Key Submissions</h1>
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Delete Key</h3>
</div>
<div class="modal-body">
<form method="POST" action="">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to delete successful key submission for team: <strong id="confirm-team-name"></strong> in challenge: <strong id="confirm-chal-name"></strong>?</p>
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
</div>
</form>
</div>
<div class="jumbotron">
<div class="container">
<h1>Correct Submissions</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="teamsboard" class=" table table-striped">
<thead>
<tr>
<td class="text-center"><b>ID</b></td>
<td><b>Team</b></td>
<td><b>Challenge</b></td>
<td><b>Submission</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr>
<td class="text-center" id="{{ solve.id }}">
{{ solve.id }}
</td>
<td class="team" id="{{ solve.teamid }}">
<a href="{{ request.script_root }}/admin/team/{{ solve.teamid }}">{{ solve.team_name }}</a>
</td>
<td class="chal" id="{{ solve.chalid }}">
{{ solve.chal_name }}
</td>
<td class="flag" id="{{ solve.id }}">
<pre class="mb-0">{{ solve.flag }}</pre>
</td>
<td class="text-center solve-time">
<small>
<script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</small>
</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/correct_keys/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page + 1 }}">&gt;&gt;&gt;</a>{% endif %}
</div>
{% endif %}
</div>
</div>
<table id="teamsboard" class=" table table-striped">
<thead>
<tr>
<td class="text-center"><b>Team</b>
</td>
<td class="text-center"><b>Challenge</b>
</td>
<td class="text-center"><b>Date</b>
</td>
<td class="text-center"><b>Key Submitted</b>
</td>
<td class="text-center"><b>Delete</b>
</td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr>
<td class="text-center team" id="{{ solve.teamid }}"><a href="{{ request.script_root }}/admin/team/{{ solve.teamid }}">{{ solve.team_name }}</a>
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal_name }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<td class="text-center flag" id="{{ solve.id }}">{{ solve.flag }}</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/correct_keys/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page+1 }}">&gt;&gt;&gt;</a>{% endif %}
<a href="{{ request.script_root }}">
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script>
$('#delete-solve').click(function(e){
e.preventDefault();
var solve = $('#confirm input[name="solve"]').val()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
if (data == "1"){
location.reload()
}
})
});
<script>
var nonce = "{{ nonce }}";
function load_confirm_modal(key_id, team_name, chal_name){
var modal = $('#confirm')
modal.find('#confirm-team-name').text(team_name)
modal.find('#confirm-chal-name').text(chal_name)
$('#confirm form').attr('action', '{{ request.script_root }}/admin/solves/'+key_id+'/delete');
$('#confirm').modal('show');
}
$('.fa-times').click(function(){
var elem = $(this).parent().parent();
var chal = elem.find('.chal').attr('id');
var chal_name = elem.find('.chal').text().trim();
var team = elem.find('.team').attr('id');
var team_name = elem.find('.team').text().trim();
var key_id = elem.find('.flag').attr('id');
$('.fa-times').click(function(){
var elem = $(this).parent().parent();
var chal = elem.find('.chal').attr('id');
var chal_name = elem.find('.chal').text().trim();
var team = elem.find('.team').attr('id');
var team_name = elem.find('.team').text().trim();
var key_id = elem.find('.flag').attr('id');
load_confirm_modal(key_id, team_name, chal_name)
ezq({
title: 'Delete Submission',
body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format(
"<strong>" + htmlentities(team_name) + "</strong>",
"<strong>" + htmlentities(chal_name) + "</strong>"
),
success: function () {
var route = script_root + '/admin/solves/' + key_id + '/delete';
$.post(route, {
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload();
}
});
}
});
</script>
});
</script>
{% endblock %}

View File

@ -2,14 +2,6 @@
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/vendor/codemirror.min.css">
<style>
.row-fluid { margin: 25px; padding-bottom: 25px; }
.media-item-wrapper { height: 120px; margin: 5px;
float: left; border: 1px solid #eee; text-align: center; text-overflow: ellipsis; overflow: hidden;}
.media-item-wrapper > a > i {line-height: 90px;}
.media-item-title{ font-size: 10px; }
#media-item{display: none;}
</style>
{% endblock %}
{% block content %}
@ -18,43 +10,60 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 class="text-center">Media Library</h3>
<div class="row">
<div class="col-md-8 col-md-offset-1" id="media-library-list">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Media Library</h3>
</div>
</div>
<div class="col-md-2">
<h4 class="text-center">Media Details</h4>
<div id="media-item">
<div class="row text-center" id="media-icon">
</div>
<br>
<div class="row text-center" id="media-filename">
</div>
<br>
<div class="row form-group">
Link: <input class="form-control" type="text" id="media-link">
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="modal-header">
<div class="container">
<div class="row mh-100">
<div class="col-md-8" id="media-library-list">
</div>
<div class="col-md-4">
<h4 class="text-center">Media Details</h4>
<div id="media-item">
<div class="text-center" id="media-icon">
</div>
<br>
<div class="text-center" id="media-filename">
</div>
<br>
<div class="form-group">
Link: <input class="form-control" type="text" id="media-link" readonly>
</div>
<div class="row form-group text-center">
<button class="btn btn-success" id="media-insert">Insert</button>
<div class="form-group text-center">
<button class="btn btn-success w-100" id="media-insert">Insert</button>
</div>
</div>
</div>
</div>
</div>
</div>
<hr>
<form id="media-library-upload">
<div class="form-group">
<label for="media-files">Upload Files
<label for="media-files">
Upload Files
</label>
<input type="file" name="files[]" id="media-files" multiple>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<input type="file" name="files[]" id="media-files" class="form-control-file" multiple>
<sub class="help-block">
Attach multiple files using Control+Click or Cmd+Click.
</sub>
</div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
</form>
</div>
<div class="modal-footer">
<div class="pull-right">
<button type="submit" class="btn btn-primary" onclick="uploadfiles();">Upload</button>
</div>
@ -64,72 +73,108 @@
</div>
</div>
<div class="row">
<div class="row">
{% for error in errors %}
<div class="large-8 large-centered columns">
<div data-alert class="alert-box alert radius centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<form id="page-edit" method="POST">
<div class="row-fluid">
<div class="col-md-12">
<h3>Route: </h3>
<p class="help-block">This is the URL route that your page will be at (e.g. /page)</p>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
</div>
</div>
<br>
<div class="row-fluid">
<div class="col-md-12">
<h3>Content: </h3>
<p class="help-block">This is the HTML content of your page</p>
<ul class="nav nav-tabs" role="tablist" id="content-edit">
<li role="presentation" class="active"><a href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
<li role="presentation"><a href="#content-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:400px">
<div class="form-group">
<br>
<div class="btn-group btn-group-sm" role="group" aria-label="...">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default" id="media-button"><i class="fa fa-camera-retro" aria-hidden="true"></i> Media Library</button>
</div>
</div>
<br>
<br>
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
<div class="container">
<div class="row pt-5">
<div class="col-md-12">
<div class="row">
{% for error in errors %}
<div class="large-12 large-centered columns">
<div data-alert class="alert-box alert centered text-center">
<span>{{ error }}</span>
<a href="#" class="close">×</a>
</div>
</div>
{% endfor %}
</div>
<button class="btn btn-theme btn-outlined create-challenge pull-right">
{% if page is defined %}
Update
{% else %}
Create
{% endif %}
</button>
</div>
</form>
</div>
<br>
<br> <!-- Don't compete with footer for bottom of page -->
<br>
<form id="page-edit" method="POST">
<div class="form-group">
<div class="col-md-12">
<h3>Title</h3>
<p class="text-muted">This is the title shown on the navigation bar</p>
<input class="form-control radius" id="route" type="text" name="title"
value="{% if page is defined %}{{ page.title }}{% endif %}" placeholder="Title">
</div>
</div>
<div class="form-group">
<div class="col-md-12">
<h3>Route</h3>
<p class="text-muted">This is the URL route that your page will be at (e.g. /page). You can also enter links to link to that page.</p>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
<div class="form-check">
<label class="form-check-label float-right">
<input class="form-check-input" type="checkbox" name="auth_required" {% if page is defined and page.auth_required %}checked{% endif %}>
Users must be logged in to see this page
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-12">
<h3>Content</h3>
<p class="text-muted">This is the HTML content of your page</p>
<ul class="nav nav-tabs" role="tablist" id="content-edit">
<li class="nav-item" role="presentation" class="active">
<a class="nav-link active" href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" onclick="preview_page(); return true;" href="#"">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:400px">
<br>
<div class="form-inline">
<div class="btn-group btn-group-sm">
<div class="btn-group">
<button type="button" class="btn btn-primary" id="media-button">
<i class="fa fa-camera-retro"></i>
Media Library
</button>
</div>
</div>
</div>
<div class="form-group">
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
{% if page is defined %}
<input name='id' type='hidden' value="{{ page.id }}">
{% endif %}
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
</div>
</div>
</div>
</div>
<div class="form-group float-right">
<button class="btn btn-success" id="publish-page">
Publish
</button>
<button class="btn btn-primary" id="save-page">
{% if page is defined %}
Update
{% else %}
Save
{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
@ -206,15 +251,17 @@
var ext = f.location.split('.').pop();
var fname = f.location.split('/')[1];
var wrapper = $('<div>').attr('class', 'media-item-wrapper col-md-2')
var wrapper = $('<div>').attr('class', 'media-item-wrapper');
var link = $('<a>');
link.attr('href', '##');
if (mapping[ext] == undefined)
link.append('<i class="fa fa-file-o fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
link.append('<i class="fa fa-file-o" aria-hidden="true"></i> '.format(mapping[ext]));
else
link.append('<i class="fa {0} fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
link.append('<i class="fa {0}" aria-hidden="true"></i> '.format(mapping[ext]));
link.append($('<small>').attr('class', 'media-item-title').text(fname));
link.click(function(e){
var media_div = $(this).parent();
@ -222,14 +269,18 @@
var f_loc = media_div.attr('data-location');
var fname = media_div.attr('data-filename');
$('#media-link').val(f_loc);
$('#media-filename').text(fname);
$('#media-filename').html(
$('<a>').attr('href', f_loc).attr('target', '_blank').text(fname)
);
$('#media-icon').empty()
$('#media-icon').empty();
if ($(icon).hasClass('fa-file-image-o')){
$('#media-icon').append($('<img>').attr('src', f_loc).attr('width', '100%'));
$('#media-icon').append($('<img>').attr('src', f_loc).css({'max-width':'100%', 'max-height': '100%', 'object-fit': 'contain'}));
} else {
// icon is empty so we need to pull outerHTML
$('#media-icon').append(icon.outerHTML);
var copy_icon = $(icon).clone();
$(copy_icon).addClass('fa-4x');
$('#media-icon').append(copy_icon);
}
$('#media-item').show();
});
@ -239,7 +290,7 @@
wrapper.attr('data-filename', fname);
list.append(wrapper);
wrapper.append($('<span>').attr('class', 'media-item-title').text(fname));
}
}
@ -271,8 +322,46 @@
insert_at_cursor(editor, entry);
});
$('#page-edit').submit(function (e){
$(this).attr('action', '{{ request.script_root }}/admin/pages');
function submit_form(){
editor.save(); // Save the CodeMirror data to the Textarea
$.ajax({
type: "POST",
url: $("#page-edit").attr('action'),
data: $("#page-edit").serialize(),
success: function (data) {
if (data.result == 'success'){
if (data.operation == 'publish'){
window.location = script_root + '/admin/pages'
} else if (data.operation == 'save'){
ezal({
title: 'Saved',
body: 'Your changes have been saved',
button: 'Okay'
})
}
}
}
});
}
function preview_page(){
editor.save(); // Save the CodeMirror data to the Textarea
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=preview');
$('#page-edit').attr('target', '_blank');
$('#page-edit').submit();
}
$('#publish-page').click(function (e) {
e.preventDefault();
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=publish');
submit_form();
});
$('#save-page').click(function (e) {
e.preventDefault();
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=save');
submit_form();
});
// Markdown Preview

View File

@ -1,193 +0,0 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<div id="solves-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
<div class="row">
<div>
<div id="keys-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
<div>
<div id="categories-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
#solves-graph{
display: block;
height: 500px;
}
#keys-pie-graph{
height: 400px;
display: block;
}
#categories-pie-graph{
height: 400px;
display: block;
}
</style>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
<script type="text/javascript">
// $.distint(array)
// Unique elements in array
$.extend({
distinct : function(anArray) {
var result = [];
$.each(anArray, function(i,v){
if ($.inArray(v, result) == -1) result.push(v);
});
return result;
}
});
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
Array.max = function( array ){
return Math.max.apply( Math, array );
};
function colorhash (x) {
color = ""
for (var i = 20; i <= 60; i+=20){
x += i
x *= i
color += x.toString(16)
};
return "#" + color.substring(0, 6)
}
function solves_graph() {
$.get('{{ request.script_root }}/admin/graphs/solves', function (data) {
var solves = $.parseJSON(JSON.stringify(data));
var chals = [];
var counts = [];
var colors = [];
var annotations = [];
var i = 1;
solves_order = Object.keys(solves).sort(function (a, b) {
return solves[b] - solves[a]
});
$.each(solves_order, function (key, value) {
chals.push(value);
counts.push(solves[value]);
var result = {
x: value,
y: solves[value],
text: solves[value],
xanchor: 'center',
yanchor: 'bottom',
showarrow: false
};
annotations.push(result);
colors.push(colorhash(i++));
});
var data = [{
type: 'bar',
x: chals,
y: counts,
text: counts,
orientation: 'v'
}];
var layout = {
title: 'Score Counts',
annotations: annotations
};
$('#solves-graph').empty()
Plotly.newPlot('solves-graph', data, layout);
});
}
function keys_percentage_graph(){
// Solves and Fails pie chart
$.get('{{ request.script_root }}/admin/fails/all', function(data){
var res = $.parseJSON(JSON.stringify(data));
var solves = res['solves'];
var fails = res['fails'];
var data = [{
values: [solves, fails],
labels: ['Solves', 'Fails'],
marker: {
colors: [
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
},
hole: .4,
type: 'pie'
}];
var layout = {
title: 'Key Percentages'
};
$('#keys-pie-graph').empty();
Plotly.newPlot('keys-pie-graph', data, layout);
});
}
function category_breakdown_graph(){
$.get('{{ request.script_root }}/admin/graphs/categories', function(data){
res = $.parseJSON(JSON.stringify(data));
res = res['categories']
categories = [];
count = [];
for (var i = 0; i < res.length; i++) {
categories.push(res[i].category)
count.push(res[i].count)
};
var data = [{
values: count,
labels: categories,
hole: .4,
type: 'pie'
}];
var layout = {
title: 'Category Breakdown'
};
$('#categories-pie-graph').empty();
Plotly.newPlot('categories-pie-graph', data, layout);
});
}
function update(){
solves_graph();
keys_percentage_graph();
category_breakdown_graph();
}
$(function() {
update();
window.onresize = function () {
console.log('resizing')
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
Plotly.Plots.resize(document.getElementById('solves-graph'));
};
});
setInterval(update, 300000);
</script>
{% endblock %}

View File

@ -0,0 +1,2 @@
<div id="challenge-preview" class="modal fade" tabindex="-1">
</div>

View File

@ -0,0 +1,47 @@
<div id="update-files" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Files</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form method="POST" action="{{ request.script_root }}/admin/files/" enctype="multipart/form-data">
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-12">
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="files-chal" name='chal' type='hidden'>
<input name='method' type='hidden' value='upload'>
<div id="current-files" class="pb-3">
</div>
<input type="hidden" name="method" value="upload">
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
<sub>Attach multiple files using Control+Click or Cmd+Click</sub>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="container">
<div class="row">
<div class="col-md-12">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-primary float-right" id="submit-files">Upload</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,111 @@
<div id="hint-modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 id="hint-modal-title"></h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="hint-modal-submit" method='POST'>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-12">
<ul class="nav nav-tabs" role="tablist" id="new-hint-edit">
<li class="nav-item">
<a class="nav-link active" href="#hint-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#hint-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="hint-write">
<div class="form-group">
<label class="text-muted">
<small>Markdown &amp; HTML are supported</small>
</label>
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint"
placeholder="Hint" rows="7"></textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="hint-preview"
style="height:234px; overflow-y: scroll;">
</div>
</div>
<div class="form-group">
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
</div>
<input type="hidden" id="chal-id-for-hint" name="chal">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="container">
<div class="row">
<div class="col-md-12">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-primary float-right" id="hint-modal-button">Add Hint</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div id="update-hints" class="modal fade" tabindex="-1" chal-id="">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Hints</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container text-center">
<a href="#" id="create-hint" class="btn btn-primary text-center">New Hint</a>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class='current-hints'>
<table id="hintsboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>Hint</b></td>
<td class="text-center"><b>Cost</b></td>
<td class="text-center"><b>Settings</b></td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,71 @@
<div id="update-keys" class="modal fade" tabindex="-1" chal-id="">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Flags</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row text-center">
<div class="col-md-12">
<button id="create-key" class="btn btn-primary d-inline-block">New Flag</button>
<form method="POST" action="{{ request.script_root }}/admin/keys">
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="keys-chal" name='chal' type='hidden'>
<div id="current-keys" class="row"></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="create-keys" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<input type="hidden" class="chal-id" name="chal-id">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3>Create Flag</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="create-keys-select-div">
<label for="create-keys-select" class="control-label">Choose Flag Type</label>
<select class="form-control" id="create-keys-select">
</select>
</div>
<br>
<div id="create-keys-entry-div">
</div>
<br>
<div style="display:none;" id="create-keys-button-div" class="text-center">
<button id="create-keys-submit" class="btn btn-success">Create Flag</button>
</div>
</div>
</div>
</div>
</div>
<div id="edit-keys" class="modal fade" tabindex="-1">
</div>

View File

@ -0,0 +1,49 @@
<div id="update-tags" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<div class="container">
<div class="row">
<div class="col-md-12">
<h3 class="text-center">Tags</h3>
</div>
</div>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="tag-insert">Value</label>
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
</div>
<input name='nonce' type='hidden' value="{{ nonce }}">
<input id="tags-chal" name='chal' type='hidden'>
<div id="current-tags">
</div>
<br/>
<div id="chal-tags">
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="container">
<div class="row">
<div class="col-md-12">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-primary float-right" id="submit-tags">Update</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,75 +2,107 @@
<html>
<head>
<title>Admin Panel</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css" />
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/style.css">
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/lato.css' rel='stylesheet' type='text/css'>
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/raleway.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/style.css">
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/handlebars.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>
{% block stylesheets %} {% endblock %}
<title>Admin Panel</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico"
type="image/x-icon">
<link rel="icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
<link rel="stylesheet"
href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css"/>
<link rel="stylesheet" href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css'>
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/jumbotron.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/sticky-footer.css">
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/base.css">
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/handlebars.min.js"></script>
<script type="text/javascript">
var script_root = "{{ request.script_root }}";
</script>
{% block stylesheets %} {% endblock %}
</head>
<body>
<div class="body-container">
<div class="navbar navbar-inverse home">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse" type="button">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
</div>
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0px">
<ul class="nav navbar-nav navbar-nav-right">
<li><a href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false">Pages <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ request.script_root }}/admin/pages">All Pages</a></li>
<li><a href="{{ request.script_root }}/admin/pages?mode=create">New Page</a></li>
</ul>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<div class="container">
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars"
aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="base-navbars">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="true">Pages</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages">All Pages</a>
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages?mode=create">New Page</a>
</div>
</li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li class="nav-item"><a class="nav-link"
href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/chals">Challenges</a>
</li>
<li class="nav-item"><a class="nav-link"
href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/config">Config</a></li>
{% set plugin_menu = get_admin_plugin_menu_bar() %}
{% set plugins = get_configurable_plugins() %}
{% if plugin_menu or plugins %}
<li class="nav-item">
<a class="nav-link d-none d-md-block d-lg-block">|</a>
</li>
{% for menu in plugin_menu %}
<li class="nav-item"><a class="nav-link"
href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a>
</li>
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>
{% endfor %}
{% if plugins %}
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for plugin in get_configurable_plugins() %}
<li><a href="{{ request.script_root }}/admin/plugins/{{ plugin }}">{{ plugin }}</a></li>
{% endfor %}
{% for plugin in plugins %}
<li class="nav-item"><a class="nav-link" href="
{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
{% endfor %}
</ul>
</li>
</ul>
</div>
</div>
{% endif %}
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container">
{{ content | safe }}
</div>
</div>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.min.js"></script>
{% block scripts %} {% endblock %}
<main role="main">
{% block content %}
{% endblock %}
</main>
<footer class="footer">
<div class="container text-center">
<a href="https://ctfd.io">
<small class="text-muted">Powered by CTFd</small>
</a>
</div>
</footer>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
{% block scripts %} {% endblock %}
</body>
</html>

View File

@ -1,78 +1,65 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/vendor/codemirror.min.css">
<style>
.CodeMirror {
padding-left: 15px;
margin-bottom: 10px;
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<h1>Pages</h1>
<div id="confirm" class="modal fade" tabindex="-1" data-width="760">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="text-center">Delete Page</h2>
</div>
<div class="modal-body" style="height:110px">
<div class="row-fluid">
<div class="col-md-12">
<form method="POST">
<input type="hidden" name="route">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to delete <strong id="confirm-route-name"></strong>?</p>
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
<button type="button" id="delete-route" class="btn btn-theme btn-outlined">Yes</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="jumbotron">
<div class="container">
<h1>Pages
<a class="no-decoration" href="{{ request.script_root }}/admin/pages?operation=create">
<i class="fa fa-plus-circle create-page" role="button" data-toggle="tooltip" title="Create Page"></i>
</a>
</h1>
</div>
</div>
<div class="col-md-6">
<h3>Pages</h3>
<table id="pages" class="table table-striped">
<thead>
<tr>
<td><b>Route</b></td>
<td class="text-center" style="width: 150px;"><b>Settings</b></td>
</tr>
</thead>
<tbody>
{% for route in routes %}
<tr name="{{ route.route }}">
<td class="route-name"><a
href="{{ request.script_root }}/admin/pages?route={{ route.route }}">{{ route.route }}</a></td>
<td class="text-center"><i class="fa fa-times"></i></td>
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="pages" class="table table-striped">
<thead>
<tr>
<td><b>Title</b></td>
<td><b>Route</b></td>
<td><b>Authentication</b></td>
<td class="text-center"><b>Published</b></td>
<td class="text-center" width="10px"><b>Settings</b></td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{{ request.script_root }}/admin/pages?mode=create">
<button type="button" id="submit" class="btn btn-md btn-primary btn-theme btn-outlined">
Add Page
</button>
</a>
</div>
<div class="col-md-6">
<h3>CSS editor</h3>
<form method="POST">
<textarea id="pages-editor" name="css" style="padding-left:20px; visibility: hidden;">{{ css }}</textarea>
<button onclick="save_css()" type="button" id="submit" class="btn btn-md btn-primary btn-theme btn-outlined">
Update CSS
</button>
</form>
</thead>
<tbody>
{% for page in pages %}
<tr>
<td class="page-title">
{{ page.title }}
</td>
<td class="page-route" page-id="{{ page.id }}" page-route="{{ page.route }}">
<a href="{{ request.script_root }}/admin/pages?id={{ page.id }}">
{{ page.route }}
</a>
</td>
<td class="page-authentication">
{% if page.auth_required %}
Required
{% else %}
{% endif %}
</td>
<td class="text-center">
{% if page.draft %}
Draft
{% else %}
Published
{% endif %}
</td>
<td class="text-center">
<a class="no-decoration" href="{{ request.script_root }}/admin/pages?id={{ page.id }}"><i class="fa fa-edit"></i></a>
<i class="fa fa-times" page-id="{{ page.id }}" page-route="{{ page.route }}" page-title="{{ page.title }}"></i>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
@ -80,46 +67,31 @@
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/codemirror.min.js"></script>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("pages-editor"), {
lineNumbers: true,
lineWrapping: true,
mode: "css",
htmlMode: false,
theme: 'elegant'
});
$('#delete-route').click(function(e){
e.preventDefault();
var route = $('#confirm input[name="route"]').val()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
if (data == "1"){
location.reload()
}
})
});
function load_confirm_modal(route){
var modal = $('#confirm')
modal.find('input[name=route]').val(route)
modal.find('#confirm-route-name').text(route)
$('#confirm form').attr('action', '{{ request.script_root }}/admin/pages/delete');
$('#confirm').modal();
}
function save_css(){
var css = editor.getValue();
var nonce = $('#nonce').val();
$.post(script_root + '/admin/css', {'css':css, 'nonce':nonce}, function(){
location.reload();
});
}
var nonce = "{{ nonce }}";
$('.fa-times').click(function(){
var elem = $(this).parent().parent();
var name = elem.find('.route-name').text().trim();
load_confirm_modal(name)
var elem = $(this);
var name = elem.attr("page-route");
var page_id = elem.attr("page-id");
ezq({
title: 'Delete '+ name,
body: "Are you sure you want to delete {0}?".format(
"<strong>" + htmlentities(name) + "</strong>"
),
success: function(){
var page_delete_route = '{{ request.script_root }}/admin/pages/delete';
$.post(page_delete_route, {
route: name,
id: page_id,
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload();
}
});
}
});
});
</script>
{% endblock %}

View File

@ -1,40 +1,48 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="row">
<h1>Scoreboard</h1>
<table id="scoreboard" class="table table-striped">
<thead>
<tr>
<td width="10px"><b>Place</b></td>
<td><b>Team</b></td>
<td><b>Score</b></td>
<td><b>Status</b></td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ loop.index }}</td>
<td><a href="{{ request.script_root }}/admin/team/{{ team.teamid }}">{{ team.name }}</a></td>
<td>{{ team.score }}</td>
<td>
{% if not team.banned %}
<form method="POST" style="margin:0;" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/ban">
<a onclick="$(this).parent().submit()">Hide</a>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{%else %}
<form method="POST" style="margin:0;" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/unban">
<a onclick="$(this).parent().submit()">Unhide</a>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="jumbotron">
<div class="container">
<h1>Scoreboard</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="scoreboard" class="table table-striped">
<thead>
<tr>
<td width="10px"><b>Place</b></td>
<td><b>Team</b></td>
<td><b>Score</b></td>
<td><b>Visibility</b></td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ loop.index }}</td>
<td><a href="{{ request.script_root }}/admin/team/{{ team.teamid }}">{{ team.name }}</a></td>
<td>{{ team.score }}</td>
<td>
{% if not team.banned %}
<form method="POST" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/ban">
<button class="btn-sm btn-success" type="submit">Visible</button>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{%else %}
<form method="POST" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/unban">
<button class="btn-sm btn-danger" type="submit">Hidden</button>
<input type="hidden" value="{{ nonce }}" name="nonce">
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,24 +1,274 @@
{% extends "admin/base.html" %}
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>Statistics</h1>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-4 text-right">
<h5><b>{{ team_count }}</b> teams registered</h5>
<h5><b>{{ ip_count }}</b> IP addresses</h5>
<hr>
<h5><b>{{ challenge_count }}</b> challenges</h5>
{% if most_solved %}
<h5><b>{{ most_solved }}</b> has the most solves with <br>{{ solve_data[most_solved] }} solves</h5>
{% endif %}
{% if least_solved %}
<h5><b>{{ least_solved }}</b> has the least solves with <br>{{ solve_data[least_solved] }} solves</h5>
{% endif %}
</div>
<div class="row" style="text-align:center">
<h1>Statistics</h1>
<div class="col-md-8">
<div id="solves-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-12">
<div id="solve-percentages-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<div id="keys-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div class="text-center">
<h5><b>{{ solve_count }}</b> right submissions</h5>
<h5><b>{{ wrong_count }}</b> wrong submissions</h5>
</div>
</div>
<div class="col-md-6">
<div id="categories-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<h3><b>{{ team_count }}</b> teams registered</h3>
<h3><b>{{ wrong_count }}</b> <a href="{{ request.script_root }}/admin/wrong_keys/1">wrong keys</a> submitted</h3>
<h3><b>{{ solve_count }}</b> <a href="{{ request.script_root }}/admin/correct_keys/1">right keys</a> submitted</h3>
<h3><b>{{ challenge_count }}</b> challenges</h3>
{% if most_solved %}
<h3>Most solved: <b>{{ most_solved }}</b> with {{ solve_data[most_solved] }}</b> solves</h3>
{% endif %}
{% if least_solved %}
<h3>Least solved: <b>{{ least_solved }}</b> with {{ solve_data[least_solved] }}</b> solves</h3>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
<script type="text/javascript">
// $.distint(array)
// Unique elements in array
$.extend({
distinct: function (anArray) {
var result = [];
$.each(anArray, function (i, v) {
if ($.inArray(v, result) == -1) result.push(v);
});
return result;
}
});
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
Array.max = function (array) {
return Math.max.apply(Math, array);
};
function solves_graph() {
$.get('{{ request.script_root }}/admin/graphs/solves', function (data) {
var solves = $.parseJSON(JSON.stringify(data));
var chals = [];
var counts = [];
var colors = [];
var annotations = [];
var i = 1;
var solves_order = Object.keys(solves).sort(function (a, b) {
return solves[b] - solves[a]
});
$.each(solves_order, function (key, value) {
chals.push(value);
counts.push(solves[value]);
{# colors.push(colorhash(value));#}
var result = {
x: value,
y: solves[value],
text: solves[value],
xanchor: 'center',
yanchor: 'bottom',
showarrow: false,
};
annotations.push(result);
});
var data = [{
type: 'bar',
x: chals,
y: counts,
text: counts,
orientation: 'v',
{# marker: {#}
{# color: colors#}
{# },#}
}];
var layout = {
title: 'Solve Counts',
annotations: annotations,
xaxis: {
title: 'Challenge Name'
},
yaxis: {
title: 'Amount of Solves'
}
};
$('#solves-graph').empty();
Plotly.newPlot('solves-graph', data, layout);
});
}
function keys_percentage_graph() {
// Solves and Fails pie chart
$.get('{{ request.script_root }}/admin/fails/all', function (data) {
var res = $.parseJSON(JSON.stringify(data));
var solves = res['solves'];
var fails = res['fails'];
var data = [{
values: [solves, fails],
labels: ['Solves', 'Fails'],
marker: {
colors: [
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
},
hole: .4,
type: 'pie'
}];
var layout = {
title: 'Submission Percentages'
};
$('#keys-pie-graph').empty();
Plotly.newPlot('keys-pie-graph', data, layout);
});
}
function category_breakdown_graph() {
$.get('{{ request.script_root }}/admin/graphs/categories', function (data) {
var res = $.parseJSON(JSON.stringify(data));
res = res['categories'];
var categories = [];
var count = [];
for (var i = 0; i < res.length; i++) {
categories.push(res[i].category);
count.push(res[i].count);
}
var data = [{
values: count,
labels: categories,
hole: .4,
type: 'pie'
}];
var layout = {
title: 'Category Breakdown'
};
$('#categories-pie-graph').empty();
Plotly.newPlot('categories-pie-graph', data, layout);
});
}
function solve_percentages_graph() {
$.get('{{ request.script_root }}/admin/graphs/solve-percentages', function (data) {
var res = $.parseJSON(JSON.stringify(data));
res = res['percentages'];
var names = [];
var percents = [];
var labels = [];
var annotations = [];
for (var key in res) {
names.push(res[key].name);
percents.push( (res[key].percentage * 100) );
var result = {
x: res[key].name,
y: (res[key].percentage * 100),
text: Math.round(res[key].percentage * 100) + '%',
xanchor: 'center',
yanchor: 'bottom',
showarrow: false,
};
annotations.push(result);
}
var data = [{
type: 'bar',
x: names,
y: percents,
orientation: 'v'
}];
var layout = {
title: 'Solve Percentages per Challenge',
xaxis: {
title: 'Challenge Name'
},
yaxis: {
title: 'Percentage of Teams (%)',
range: [0, 100]
},
annotations: annotations
};
$('#solve-percentages-graph').empty();
Plotly.newPlot('solve-percentages-graph', data, layout);
});
}
function update() {
solves_graph();
keys_percentage_graph();
category_breakdown_graph();
solve_percentages_graph();
}
$(function () {
update();
window.onresize = function () {
console.log('resizing');
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
Plotly.Plots.resize(document.getElementById('solves-graph'));
Plotly.Plots.resize(document.getElementById('solve-percentages-graph'));
};
});
setInterval(update, 300000);
</script>
{% endblock %}

View File

@ -1,374 +1,404 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary {
background-color: #337ab7;
}
.btn-danger {
background-color: #d9534f;
}
</style>
{% endblock %}
{% block content %}
<div class="modal fade" id="create-award-modal" tabindex="-1" role="dialog" aria-labelledby="create-award-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="create-award-label">Create Award</h4>
</div>
<div class="modal-body">
<form id="award-create-form" method="POST" action="{{ request.script_root }}/admin/awards/add">
<div class="form-group">
<label for="award-name-input">Name</label>
<input type="text" class="form-control" id="award-name-input" name="name" placeholder="Enter award name">
</div>
<div class="form-group">
<label for="award-value-input">Value</label>
<input type="number" class="form-control" id="award-value-input" name="value" placeholder="Enter award value">
</div>
<div class="form-group">
<label for="award-category-input">Category</label>
<input type="text" class="form-control" id="award-category-input" name="category" placeholder="Enter award category">
</div>
<div class="form-group">
<label for="award-description-input" class="control-label">Description</label>
<textarea id="award-description-input" class="form-control" name="description" rows="10" placeholder="Description of what the award is for"></textarea>
</div>
<div class="form-group">
<label for="award-icon-input">Award Icon</label>
<input type="file" id="award-icon-input" name="icon">
</div>
<input type="hidden" value="{{ team.id }}" name="teamid" id="teamid">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div class="modal-footer">
<button type="submit" id="award-create-button" class="btn btn-primary">Create</button>
</div>
</form>
</div>
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="create-award-label">Create Award</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
<form id="award-create-form" method="POST" action="{{ request.script_root }}/admin/awards/add">
<div class="form-group">
<label for="award-name-input">Name</label>
<input type="text" class="form-control" id="award-name-input" name="name" placeholder="Enter award name">
</div>
<div class="form-group">
<label for="award-value-input">Value</label>
<input type="number" class="form-control" id="award-value-input" name="value" placeholder="Enter award value">
</div>
<div class="form-group">
<label for="award-category-input">Category</label>
<input type="text" class="form-control" id="award-category-input" name="category" placeholder="Enter award category">
</div>
<div class="form-group">
<label for="award-description-input" class="control-label">Description</label>
<textarea id="award-description-input" class="form-control" name="description" rows="10" placeholder="Description of what the award is for"></textarea>
</div>
<div class="form-group">
<label for="award-icon-input">Award Icon</label>
<input type="file" id="award-icon-input" name="icon">
</div>
<input type="hidden" value="{{ team.id }}" name="teamid" id="teamid">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<div class="modal-footer">
<button type="submit" id="award-create-button" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row container-fluid">
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3 id="confirm-title">Delete Key</h3>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.script_root }}/admin/chal/delete">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p id="confirm-description"></p>
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<h1 id="team-id">{{ team.name }}</h1>
<div class="jumbotron">
<div class="container">
<h1 id="team-id" class="text-center">{{ team.name }}</h1>
<h2 id="team-email" class="text-center">{{ team.email }}</h2>
<h2 id="team-place" class="text-center">
{%if place %}
{{ place }} <small>place</small>
{% endif %}
{% if place %}
{{ place }}
<small>place</small>
{% endif %}
</h2>
<h2 id="team-score" class="text-center">
{%if score %}
{{ score }} <small>points</small>
{% endif %}
{% if score %}
{{ score }}
<small>points</small>
{% endif %}
</h2>
</div>
</div>
<div class="container">
<div class="row">
{% if solves %}
<div id="keys-pie-graph" class="col-md-6">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="categories-pie-graph" class="col-md-6">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="score-graph" class="col-md-12">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
{% else %}
<div class="row">
<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>
</div>
{% endif %}
</div>
{% if solves %}
<div id="keys-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="categories-pie-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="score-graph">
<div class="text-center">
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
{% else %}
<div class="row">
<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>
</div>
{% endif %}
<nav class="nav nav-tabs nav-fill" id="myTab" role="tablist">
<a class="nav-item nav-link active" id="nav-solves-tab" data-toggle="tab" href="#nav-solves" role="tab"
aria-controls="nav-solves" aria-selected="true">Solves</a>
<table class="table table-striped">
<h3>IP Addresses</h3>
<thead>
<tr>
<td class="text-center"><b>IP Address</b></td>
<td class="text-center"><b>Last Seen</b></td>
</tr>
</thead>
<tbody>
{% for addr in addrs %}
<tr>
<td class="text-center">{{ addr[0] }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ addr[1]|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<a class="nav-item nav-link" id="nav-wrong-tab" data-toggle="tab" href="#nav-wrong" role="tab"
aria-controls="nav-wrong" aria-selected="false">Wrong</a>
<a class="nav-item nav-link" id="nav-awards-tab" data-toggle="tab" href="#nav-awards" role="tab"
aria-controls="nav-awards" aria-selected="false">Awards</a>
<a class="nav-item nav-link" id="nav-missing-tab" data-toggle="tab" href="#nav-missing" role="tab"
aria-controls="nav-missing" aria-selected="false">Missing</a>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-solves" role="tabpanel" aria-labelledby="nav-solves-tab">
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Solves</h3>
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr class="chal-solve">
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal.name }}</td>
<td class="flag" id="{{ solve.id }}"><pre>{{ solve.flag }}</pre></td>
<td class="text-center">{{ solve.chal.category }}</td>
<td class="text-center">{{ solve.chal.value }}</td>
<td class="text-center solve-time">
<script>document.write(moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<table class="table table-striped">
<h3>Awards</h3>
<thead>
<tr>
<td class="text-center"><b>Name</b></td>
<td class="text-center"><b>Description</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Icon</b></td>
<td class="text-center"><b>Delete</b></td>
<div class="tab-pane fade" id="nav-wrong" role="tabpanel" aria-labelledby="nav-wrong-tab">
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Wrong Keys</h3>
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for wrong_key in wrong_keys %}
<tr class="chal-wrong">
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal.name }}</td>
<td class="flag" id="{{ wrong_key.id }}"><pre>{{ wrong_key.flag }}</pre></td>
<td class="text-center solve-time">
<script>document.write(moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
</thead>
<tbody id="awards-body">
{% for award in awards %}
<tr class="award-row">
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-awards" role="tabpanel" aria-labelledby="nav-awards-tab">
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Awards</h3>
<thead>
<tr>
<td class="text-center"><b>Name</b></td>
<td class="text-center"><b>Description</b></td>
<td class="text-center"><b>Date</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Icon</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody id="awards-body">
{% for award in awards %}
<tr class="award-row">
<td class="text-center chal" id="{{ award.id }}">{{ award.name }}</td>
<td class="text-center">{{ award.description }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ award.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<td class=""><pre>{{ award.description }}</pre></td>
<td class="text-center solve-time">
<script>document.write(moment({{ award.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</td>
<td class="text-center">{{ award.value }}</td>
<td class="text-center">{{ award.category }}</td>
<td class="text-center">{{ award.icon }}</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row container-fluid">
<button type="button" data-toggle="modal" data-target="#create-award-modal" class="btn btn-primary pull-right">Create Award</button>
<button type="button" data-toggle="modal" data-target="#create-award-modal"
class="btn btn-primary float-right d-block">Create Award
</button>
</div>
</div>
</div>
<table class="table table-striped">
<h3>Solves</h3>
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for solve in solves %}
<tr class="chal-solve">
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal.name }}</td>
<td class="text-center flag" id="{{ solve.id }}">{{ solve.flag }}</td>
<td class="text-center">{{ solve.chal.category }}</td>
<td class="text-center">{{ solve.chal.value }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="table table-striped">
<h3>Missing</h3>
<thead>
<tr>
<div class="tab-pane fade" id="nav-missing" role="tabpanel" aria-labelledby="nav-missing-tab">
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3 class="text-center py-3 d-block">Missing</h3>
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Category</b></td>
<td class="text-center"><b>Value</b></td>
<td class="text-center"><b>Mark Solved</b></td>
</tr>
</thead>
<tbody>
{% for chal in missing %}
<tr class="chal-solve">
<td class="text-center chal" id="{{ chal.id }}">{{ chal.name }}</td>
<td class="text-center">{{ chal.category }}</td>
<td class="text-center">{{ chal.value }}</td>
<td class="text-center"><i class="fa fa-check mark-correct"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<h3>IP Addresses</h3>
<thead>
<tr>
<td class="text-center"><b>IP Address</b></td>
<td class="text-center"><b>Last Seen</b></td>
</tr>
</thead>
<tbody>
{% for chal in missing %}
<tr class="chal-solve">
<td class="text-center chal" id="{{ chal.id }}">{{ chal.name }}</td>
<td class="text-center">{{ chal.category }}</td>
<td class="text-center">{{ chal.value }}</td>
<td class="text-center"><i class="fa fa-check"></i></td>
</tr>
{% for addr in addrs %}
<tr>
<td class="text-center">{{ addr[0] }}</td>
<td class="text-center solve-time">
<script>document.write(moment({{ addr[1]|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</table>
</div>
</div>
<table class="table table-striped">
<h3>Wrong Keys</h3>
<thead>
<tr>
<td class="text-center"><b>Challenge</b></td>
<td class="text-center"><b>Submitted</b></td>
<td class="text-center"><b>Time</b></td>
<td class="text-center"><b>Delete</b></td>
</tr>
</thead>
<tbody>
{% for wrong_key in wrong_keys %}
<tr class="chal-wrong">
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal.name }}</td>
<td class="text-center flag" id="{{ wrong_key.id }}">{{ wrong_key.flag }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/team.js"></script>
<script>
$('#delete-solve').click(function (e) {
e.preventDefault();
var solve = $('#confirm input[name="solve"]').val()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function (data) {
var data = $.parseJSON(JSON.stringify(data))
if (data == "1") {
location.reload()
}
})
});
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
<script src="{{ request.script_root }}/themes/admin/static/js/team.js"></script>
<script>
var nonce = "{{ nonce }}";
function load_confirm_modal(msg) {
var title = msg.title;
var description = msg.description;
var action = msg.action;
$('#confirm-title').text(title);
$('#confirm-description').html(description);
$('#confirm form').attr('action', action);
$('#confirm').modal('show');
$('.fa-times').click(function () {
var elem = $(this).parent().parent();
var type = elem.attr('class');
var chal_name = elem.find('.chal').text().trim();
var team_name = $("#team-id").text();
var key_id = elem.find('.flag').attr('id');
if (type == 'chal-solve'){
var title = 'Delete Solve';
var description = "<span>Are you sure you want to delete " +
"<strong>correct</strong> " +
"submission from " +
"<strong id='confirm-team-name'></strong> " +
"for challenge: " +
"<strong id='confirm-chal-name'></strong>?</span>"
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html();
var action = '{{ request.script_root }}/admin/solves/' + key_id + '/delete';
} else if (type == 'chal-wrong') {
var title = 'Delete Wrong Key';
var description = "<span>Are you sure you want to delete " +
"<strong>incorrect</strong> " +
"submission from " +
"<strong id='confirm-team-name'></strong> " +
"for <strong id='confirm-chal-name'></strong>?</span>";
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html();
var action = '{{ request.script_root }}/admin/wrong_keys/' + key_id + '/delete';
} else if (type == 'award-row') {
var title = 'Delete Award';
var award_id = elem.find('.chal').attr('id');
var description = "<span>Are you sure you want to delete the " +
"<strong>{0}</strong> award?</span>".format(chal_name);
var action = '{{ request.script_root }}/admin/awards/{0}/delete'.format(award_id);
}
var msg = {
title : title,
description : description,
action : action,
};
ezq({
title: title,
body: description,
success: function(){
var route = action;
$.post(route, {
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload();
}
});
}
});
});
$('.fa-times').click(function () {
var elem = $(this).parent().parent();
var type = elem.attr('class');
var chal_name = elem.find('.chal').text().trim();
var team_name = $("#team-id").text();
var key_id = elem.find('.flag').attr('id');
$('.mark-correct').click(function () {
var elem = $(this).parent().parent();
var type = elem.attr('class');
var chal = elem.find('.chal').attr('id');
var team = window.location.pathname.split('/').pop();
if (type == 'chal-solve'){
var title = 'Delete Solve';
var description = "<span>Are you sure you want to delete " +
"<strong>correct</strong> " +
"key submission for team: " +
"<strong id='confirm-team-name'></strong> " +
"in challenge: " +
"<strong id='confirm-chal-name'></strong>?</span>"
var chal_name = htmlentities(elem.find('.chal').text().trim());
var team_name = htmlentities($("#team-id").text());
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html()
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html();
var action = '{{ request.script_root }}/admin/solves/' + key_id + '/delete';
} else if (type == 'chal-wrong') {
var title = 'Delete Wrong Key';
var description = "<span>Are you sure you want to delete " +
"<strong>incorrect</strong> " +
"key submission for team: " +
"<strong id='confirm-team-name'></strong> " +
"in challenge: " +
"<strong id='confirm-chal-name'></strong>?</span>"
var action = '{{request.script_root }}/admin/solves/' + team + '/' + chal + '/solve';
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html()
var title = 'Mark ' + chal_name + ' solved for ' + team_name;
var description = "<span>Are you sure you want to mark " +
"<strong>{0}</strong> ".format(team_name) +
"as solved for team " +
"<strong>{0}</strong>?</span>".format(chal_name);
var action = '{{ request.script_root }}/admin/wrong_keys/' + key_id + '/delete';
} else if (type == 'award-row') {
var title = 'Delete Award';
var award_id = elem.find('.chal').attr('id');
var description = "<span>Are you sure you want to delete the " +
"<strong>{0}</strong> award?</span>".format(chal_name);
var action = '{{ request.script_root }}/admin/awards/{0}/delete'.format(award_id);
}
ezq({
title: title,
body: description,
success: function(){
var route = script_root + '/admin/solves/' + team + '/' + chal + '/solve';
$.post(route, {
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload();
}
});
}
})
});
var msg = {
title : title,
description : description,
action : action,
}
$('#award-create-form').submit(function(e){
$.post($(this).attr('action'), $(this).serialize(), function(res){
if (res == '1'){
var award = $('#award-create-form').serializeObject();
var award_text = '<td class="text-center">{0}</td>'.format(award.name) +
'<td class="text-center">{0}</td>'.format(award.description) +
'<td class="text-center solve-time">{0}</td>'.format(moment().local().format('MMMM Do, h:mm:ss A')) +
'<td class="text-center">{0}</td>'.format(award.value) +
'<td class="text-center">{0}</td>'.format(award.category) +
'<td class="text-center">{0}</td>'.format('None') +
'<td class="text-center"><i class="fa fa-times"></i></td>'
$('#awards-body').append(award_text);
$('#create-award-modal').modal('hide');
}
})
e.preventDefault();
});
load_confirm_modal(msg)
});
$('.fa-check').click(function () {
var elem = $(this).parent().parent();
var type = elem.attr('class');
var chal = elem.find('.chal').attr('id');
var chal_name = elem.find('.chal').text().trim();
var team = window.location.pathname.split('/').pop();
var team_name = $("#team-id").text();
var title = 'Mark '+chal_name+' Solved';
var description = "<span>Are you sure you want to mark " +
"<strong id='confirm-chal-name'></strong> " +
"as solved for team " +
"<strong id='confirm-team-name'></strong> " +
"?</span>";
var description = $($.parseHTML(description));
description.find('#confirm-team-name').text(team_name);
description.find('#confirm-chal-name').text(chal_name);
description = description.html()
var action = '{{request.script_root }}/admin/solves/' + team + '/' + chal + '/solve';
var msg = {
title : title,
description : description,
action : action,
};
load_confirm_modal(msg);
});
$('#award-create-form').submit(function(e){
$.post($(this).attr('action'), $(this).serialize(), function(res){
if (res == '1'){
var award = $('#award-create-form').serializeObject();
var award_text = '<td class="text-center">{0}</td>'.format(award.name) +
'<td class="text-center">{0}</td>'.format(award.description) +
'<td class="text-center solve-time">{0}</td>'.format(moment().local().format('MMMM Do, h:mm:ss A')) +
'<td class="text-center">{0}</td>'.format(award.value) +
'<td class="text-center">{0}</td>'.format(award.category) +
'<td class="text-center">{0}</td>'.format('None') +
'<td class="text-center"><i class="fa fa-times"></i></td>'
$('#awards-body').append(award_text);
$('#create-award-modal').modal('hide');
}
})
e.preventDefault();
});
</script>
</script>
{% endblock %}

View File

@ -1,214 +1,216 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
td { text-align:center; }
.checkbox { margin: 0px !important; }
input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
</style>
{% endblock %}
{% block content %}
<div class="row">
<h1>
Teams
<i class="fa fa-plus-circle create-team" role="button" data-toggle="tooltip" title="Create Team"></i>
</h1>
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h2 class="text-center">Delete User</h2>
</div>
<div class="modal-body" style="height:110px">
<div class="row-fluid">
<div class="col-md-12">
<form method="POST">
<input type="hidden" name="route">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to delete <strong id="confirm-team-name"></strong>?</p>
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
<button type="button" id="delete-user" class="btn btn-theme btn-outlined">Yes</button>
</div>
</form>
</div>
<div class="jumbotron">
<div class="container">
<h1>Teams <i class="fa fa-plus-circle create-team" role="button" data-toggle="tooltip" title="Create Team"></i></h1>
</div>
</div>
<div id="email-user" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2>Email Team</h2>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form method="POST">
<input type="hidden" name="id">
<input type="hidden" name="nonce" value="{{ nonce }}">
<textarea class="form-control" name="msg" placeholder="Enter your message here"
rows="15"></textarea>
<br>
<div id="email-user-errors"></div>
<button type="button" id="send-user-email" class="btn btn-primary float-right">
Send Message
</button>
</form>
</div>
</div>
</div>
</div>
<div id="update-user-modal" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-action">Edit Team</h2>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body clearfix">
<form method="POST" action="{{ request.script_root }}/admin/teams/">
<input type="hidden" name="nonce" value="{{ nonce }}">
<input type="hidden" name="id">
<div class="form-group">
<label for="name">Team Name</label>
<input type="text" class="form-control" name="name" id="name"
placeholder="Enter new team name" required>
</div>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" name="email" id="email"
placeholder="Enter new email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password"/>
</div>
<div class="form-group">
<label for="website">Website</label>
<input type="text" class="form-control" name="website" id="website"
placeholder="Enter website">
</div>
<div class="form-group">
<label for="affiliation">Affiliation</label>
<input type="text" class="form-control" name="affiliation" id="affiliation"
placeholder="Enter affiliation">
</div>
<div class="form-group">
<label for="affiliation">Country</label>
<input type="text" class="form-control" name="country" id="country"
placeholder="Enter country">
</div>
<div id="results">
</div>
<button id="update-user" type="submit"
class="btn btn-primary btn-outlined pull-right modal-action">Update
</button>
</form>
</div>
</div>
</div>
<div id="email-user" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h2 class="text-center">Email Team</h2>
</div>
<div class="modal-body">
<form method="POST">
<input type="hidden" name="id">
<input type="hidden" name="nonce" value="{{ nonce }}">
<textarea class="form-control" name="msg" placeholder="Enter your message here" rows="15"></textarea>
<br>
<div id="email-user-errors"></div>
<button type="button" id="send-user-email" class="btn btn-theme btn-outlined">Send Message</button>
</form>
</div>
</div>
</div>
</div>
<div id="user" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h2 class="text-center modal-action">Edit Team</h2>
</div>
<div class="modal-body clearfix" style="padding:20px;">
<form method="POST" action="{{ request.script_root }}/admin/teams/">
<input type="hidden" name="nonce" value="{{ nonce }}">
<input type="hidden" name="id">
<div class="form-group">
<label for="name">Team Name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="Enter new team name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" class="form-control" name="email" id="email" placeholder="Enter new email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" />
</div>
<div class="form-group">
<label for="website">Website</label>
<input type="text" class="form-control" name="website" id="website" placeholder="Enter website">
</div>
<div class="form-group">
<label for="affiliation">Affiliation</label>
<input type="text" class="form-control" name="affiliation" id="affiliation" placeholder="Enter affiliation">
</div>
<div class="form-group">
<label for="affiliation">Country</label>
<input type="text" class="form-control" name="country" id="country" placeholder="Enter country">
</div>
<div id="results">
</div>
<button id="update-user" type="submit" class="btn btn-theme btn-outlined pull-right modal-action">Update</button>
</form>
</div>
</div>
</div>
</div>
{% if q and field%}
<h4 class="text-center">Searching for teams with {{field}} matching {{q}}</h4>
{% endif %}
</div>
<div class="container">
<div class="row">
<form method="GET">
<div class="col-md-2 form-group">
<label for="sel1">Search Field</label>
<select name="field" class="form-control" id="sel1">
<option value="id" {% if field == 'id' %}selected{% endif %}>ID</option>
<option value="name" {% if field == 'name' %}selected{% endif %}>Name</option>
<option value="email" {% if field == 'email' %}selected{% endif %}>Email</option>
<option value="affiliation" {% if field == 'affiliation' %}selected{% endif %}>Affiliation</option>
<option value="country" {% if field == 'country' %}selected{% endif %}>Country</option>
</select>
</div>
<div class="col-md-8 form-group">
<label for="team-name-search">Parameter</label>
<input type="text" class="form-control" id="team-name-search" name="q" placeholder="Search for matching team names" {% if q %}value="{{q}}"{% endif %}>
</div>
<div class="col-md-2 form-group">
<label for="team-name-search">&nbsp;</label>
<input type="submit" class="form-control btn btn-default" value="Search">
</div>
</form>
</div>
<table id="teamsboard" class="table table-striped">
<thead>
<tr>
<td width="10px" class="text-center"><b>ID</b>
</td>
<td class="text-center"><b>Team</b>
</td>
<td class="text-center"><b>Email</b>
</td>
<td class="text-center"><b>Website</b>
</td>
<td class="text-center"><b>Affiliation</b>
</td>
<td class="text-center"><b>Country</b>
</td>
<td class="text-center"><b>Admin</b>
</td>
<td class="text-center"><b>Verified</b>
</td>
<td class="text-center"><b>Settings</b>
</td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr name="{{ team.id }}">
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td>
<td class="team-email" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
<td class="team-website">{% if team.website %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %}
</td>
<td class="team-affiliation" value="{{ team.affiliation if team.affiliation is not none }}"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
</td>
<td class="team-country" value="{{ team.country if team.country is not none }}"><span>{% if team.country %}{{ team.country }}{% endif %}</span>
</td>
<td class="team-admin">
<div class="center-block checkbox text-center">
<input type="checkbox" {% if team.admin %}checked{% endif %}>
</div>
</td>
<td class="team-verified">
<div class="center-block checkbox text-center">
<input type="checkbox" {% if team.verified %}checked{% endif %}>
</div>
</td>
<td class="text-center"><span>
<i class="fa fa-pencil-square-o" data-toggle="tooltip" data-placement="top" title="Modify {{ team.name }}"></i>
{% if can_send_mail() %}<i class="fa fa-envelope"></i>{% endif %}
<i class="fa fa-times" data-toggle="tooltip" data-placement="top" title="Delete {{ team.name }}"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/teams/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
<div class="col-md-12">
{% if q and field%}
<h4 class="text-center">Searching for teams with {{field}} matching {{q}}</h4>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page+1 }}">&gt;&gt;&gt;</a>{% endif %}
<form method="GET" class="form-inline">
<div class="form-group col-md-2">
<label for="sel1" class="sr-only" >Search Field</label>
<select name="field" class="form-control w-100" id="sel1">
<option value="name" {% if field == 'name' %}selected{% endif %}>Name</option>
<option value="id" {% if field == 'id' %}selected{% endif %}>ID</option>
<option value="email" {% if field == 'email' %}selected{% endif %}>Email</option>
<option value="affiliation" {% if field == 'affiliation' %}selected{% endif %}>Affiliation</option>
<option value="country" {% if field == 'country' %}selected{% endif %}>Country</option>
</select>
</div>
<div class="form-group col-md-8">
<label for="team-name-search" class="sr-only">Parameter</label>
<input type="text" class="form-control w-100" id="team-name-search" name="q" placeholder="Search for matching team names" {% if q %}value="{{q}}"{% endif %}>
</div>
<div class="form-group col-md-2">
<label for="team-name-search" class="sr-only">Search</label>
<button type="submit" class="btn btn-primary w-100"><i class="fa fa-search" aria-hidden="true"></i></button>
</div>
</form>
<br>
<table id="teamsboard" class="table table-striped">
<thead>
<tr>
<td width="10px" class="text-center"><b>ID</b>
</td>
<td class="text-center"><b>Team</b>
</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Email</b>
</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Website</b>
</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Affiliation</b>
</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Country</b>
</td>
<td class="text-center"><b>Admin</b>
</td>
<td class="text-center"><b>Verified</b>
</td>
<td class="text-center"><b>Settings</b>
</td>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr name="{{ team.id }}">
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
</td>
<td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
<td class="team-website d-none d-md-table-cell d-lg-table-cell text-center">
{% if team.website %}
<a href="{{ team.website }}" target="_blank">
<i class="fa fa-globe fa-2" data-toggle="tooltip" data-placement="top"
title="{{ team.website }}" aria-hidden="true"></i>
</a>
{% endif %}
</td>
<td class="team-affiliation d-none d-md-table-cell d-lg-table-cell" value="{{ team.affiliation if team.affiliation is not none }}">
<span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
</td>
<td class="team-country d-none d-md-table-cell d-lg-table-cell" value="{{ team.country if team.country is not none }}">
<span>{% if team.country %}{{ team.country }}{% endif %}</span>
</td>
<td class="team-admin">
<div class="center-block checkbox text-center">
<input type="checkbox" {% if team.admin %}checked{% endif %}>
</div>
</td>
<td class="team-verified">
<div class="center-block checkbox text-center">
<input type="checkbox" {% if team.verified %}checked{% endif %}>
</div>
</td>
<td class="text-center"><span>
<i class="fa fa-pencil-square-o" data-toggle="tooltip" data-placement="top" title="Modify {{ team.name }}"></i>
{% if can_send_mail() %}
<i class="fa fa-envelope" data-toggle="tooltip" data-placement="top" title="Email {{ team.name }}"></i>
{% endif %}
<i class="fa fa-times" data-toggle="tooltip" data-placement="top" title="Delete {{ team.name }}"></i>
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/teams/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page + 1 }}">&gt;&gt;&gt;</a>{% endif %}
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
var nonce = "{{ nonce }}";
function load_update_modal(id, name, email, website, affiliation, country){
var modal_form = $('#user form');
var modal_form = $('#update-user-modal form');
modal_form.find('input[name=name]').val(name);
modal_form.find('input[name=id]').val(id);
@ -219,26 +221,26 @@ function load_update_modal(id, name, email, website, affiliation, country){
modal_form.find('input[name=password]').val('');
if (id == 'new'){
$('#user .modal-action').text('Create Team');
$('#update-user-modal .modal-action').text('Create Team');
} else {
$('#user .modal-action').text('Edit Team');
$('#update-user-modal .modal-action').text('Edit Team');
}
$('#results').empty();
$('#user form').attr('action', '{{ request.script_root }}/admin/team/'+id);
$('#user').modal("show");
$('#update-user-modal form').attr('action', '{{ request.script_root }}/admin/team/'+id);
$('#update-user-modal').modal("show");
}
$('#update-user').click(function(e){
e.preventDefault();
var id = $('#user input[name="id"]').val()
var user_data = $('#user form').serializeArray()
var id = $('#update-user-modal input[name="id"]').val()
var user_data = $('#update-user-modal form').serializeArray()
$('#results').empty();
$.post($('#user form').attr('action'), $('#user form').serialize(), function(data){
$.post($('#update-user-modal form').attr('action'), $('#update-user-modal form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
for (var i = 0; i < data['data'].length; i++) {
if (data['data'][i] == 'success'){
$('#user').modal('hide');
$('#update-user-modal').modal('hide');
location.reload();
}
else{
@ -270,34 +272,30 @@ $('.team-verified input').on('change', function () {
console.log(verified);
$.post('{{ request.script_root }}/admin/team/' + id, {'verified': verified, 'nonce': nonce});
})
});
$('#send-user-email').click(function(e){
e.preventDefault();
var id = $('#email-user input[name="id"]').val();
var email_data = $('#email-user form').serializeArray();
$.post($('#email-user form').attr('action'), $('#email-user form').serialize(), function(data){
if (data == "1"){
$('#email-user').modal('hide');
if (data.result){
var error = $('<div class="alert alert-success alert-dismissable">\n' +
' <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>\n' +
' {0}\n'.format(data.message) +
'</div>');
$('#email-user-errors').append(error);
}
else{
$('#email-user-errors').append("<b>Failed to send email</b>");
var error = $('<div class="alert alert-danger alert-dismissable">\n' +
' <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>\n' +
' {0}\n'.format(data.message) +
'</div>');
$('#email-user-errors').append(error);
}
});
});
$('#delete-user').click(function(e){
e.preventDefault();
var id = $('#confirm input[name="id"]').val()
var user_data = $('#confirm form').serializeArray()
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
var data = $.parseJSON(JSON.stringify(data))
if (data == "1"){
location.reload()
}
})
});
$('.fa-pencil-square-o').click(function(){
var elem = $(this).parent().parent().parent();
var id = elem.find('.team-id').attr('value') || '';
@ -314,26 +312,32 @@ $('.create-team').click(function(){
load_update_modal('new', '', '', '', '', '');
});
function load_confirm_modal(id, name){
var modal = $('#confirm')
modal.find('input[name=id]').val(id)
modal.find('#confirm-team-name').text(name)
$('#confirm form').attr('action', '{{ request.script_root }}/admin/team/'+id+'/delete');
$('#confirm').modal();
}
$('.fa-times').click(function(){
var elem = $(this).parent().parent().parent();
var id = elem.find('.team-id').text().trim();
var team_id = elem.find('.team-id').text().trim();
var name = elem.find('.team-name').text().trim();
load_confirm_modal(id, name)
ezq({
title: "Delete User",
body: "Are you sure you want to delete {0}".format("<strong>"+name+"</strong>"),
success: function(){
var route = script_root + '/admin/team/' + team_id + '/delete';
$.post(route, {
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload();
}
});
}
})
});
function load_email_modal(id){
var modal = $('#email-user')
modal.find('textarea').val("")
modal.find('input[name=id]').val(id)
$('#email-user-errors').empty()
var modal = $('#email-user');
modal.find('textarea').val("");
modal.find('input[name=id]').val(id);
$('#email-user-errors').empty();
$('#email-user form').attr('action', '{{ request.script_root }}/admin/team/'+id+'/mail');
$('#email-user').modal();
}

View File

@ -1,117 +1,108 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.btn-primary {
background-color: #337ab7;
}
.btn-danger {
background-color: #d9534f;
}
</style>
{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="container">
<h1>Incorrect Submissions</h1>
</div>
</div>
<div class="row">
<h1>Incorrect Key Submissions</h1>
<div id="confirm" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h3>Delete Key</h3>
</div>
<div class="modal-body">
<form method="POST" action="">
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
<div class="small-6 small-centered text-center columns">
<p>Are you sure you want to delete incorrect key submission for team: <strong
id="confirm-team-name"></strong> in challenge: <strong
id="confirm-chal-name"></strong>?</p>
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
</div>
</form>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<table id="teamsboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>ID</b>
</td>
<td><b>Team</b>
</td>
<td><b>Challenge</b>
</td>
<td><b>Submitted Key</b>
</td>
<td class="text-center"><b>Date</b>
</td>
<td class="text-center"><b>Delete</b>
</td>
</tr>
</thead>
<tbody>
{% for wrong_key in wrong_keys %}
<tr>
<td class="text-center">{{ wrong_key.id }}</td>
<td class="team" id="{{ wrong_key.teamid }}">
<a href="{{ request.script_root }}/admin/team/{{ wrong_key.teamid }}">{{ wrong_key.team_name }}</a>
</td>
<td class="chal" id="{{ wrong_key.chalid }}">
{{ wrong_key.chal_name }}
</td>
<td class="flag" id="{{ wrong_key.id }}">
<pre class="mb-0">{{ wrong_key.flag }}</pre>
</td>
<td class="text-center solve-time">
<small>
<script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
</small>
</td>
<td class="text-center">
<i class="fa fa-times"></i>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/wrong_keys/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page + 1 }}">&gt;&gt;&gt;</a>{% endif %}
</div>
{% endif %}
</div>
</div>
<table id="teamsboard" class="table table-striped">
<thead>
<tr>
<td class="text-center"><b>Team</b>
</td>
<td class="text-center"><b>Challenge</b>
</td>
<td class="text-center"><b>Date</b>
</td>
<td class="text-center"><b>Submitted Key</b>
</td>
<td class="text-center"><b>Delete</b>
</td>
</tr>
</thead>
<tbody>
{% for wrong_key in wrong_keys %}
<tr>
<td class="text-center team" id="{{ wrong_key.teamid }}"><a href="{{ request.script_root }}/admin/team/{{ wrong_key.teamid }}">{{ wrong_key.team_name }}</a>
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal_name }}</td>
<td class="text-center solve-time"><script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
<td class="text-center flag" id="{{ wrong_key.id }}">{{ wrong_key.flag }}</td>
<td class="text-center"><i class="fa fa-times"></i></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if pages > 1 %}
<div class="text-center">Page
<br>
{% if curr_page != 1 %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page-1 }}">&lt;&lt;&lt;</a>{% endif %}
{% for page in range(1, pages + 1) %}
{% if curr_page != page %}
<a href="{{ request.script_root }}/admin/wrong_keys/{{ page }}">{{ page }}</a>
{% else %}
<b>{{ page }}</b>
{% endif %}
{% endfor %}
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page+1 }}">&gt;&gt;&gt;</a>{% endif %}
<a href="{{ request.script_root }}">
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
<script>
$('#delete-solve').click(function (e) {
e.preventDefault();
var solve = $('#confirm input[name="solve"]').val();
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function (data) {
<script>
var nonce = "{{ nonce }}";
$('.fa-times').click(function () {
var elem = $(this).parent().parent();
var chal = elem.find('.chal').attr('id');
var chal_name = elem.find('.chal').text().trim();
var team = elem.find('.team').attr('id');
var team_name = elem.find('.team').text().trim();
var key_id = elem.find('.flag').attr('id');
ezq({
title: 'Delete Submission',
body: "Are you sure you want to delete incorrect submission from {0} for challenge {1}".format(
"<strong>" + team_name + "</strong>",
"<strong>" + chal_name + "</strong>"
),
success: function () {
var route = script_root + '/admin/wrong_keys/' + key_id + '/delete';
$.post(route, {
nonce: nonce,
}, function (data) {
var data = $.parseJSON(JSON.stringify(data));
if (data == "1") {
location.reload()
location.reload();
}
})
});
function load_confirm_modal(key_id, team_name, chal_name) {
var modal = $('#confirm')
modal.find('#confirm-team-name').text(team_name);
modal.find('#confirm-chal-name').text(chal_name);
$('#confirm form').attr('action', '{{ request.script_root }}/admin/wrong_keys/' + key_id + '/delete');
$('#confirm').modal('show');
});
}
$('.fa-times').click(function () {
var elem = $(this).parent().parent();
var chal = elem.find('.chal').attr('id');
var chal_name = elem.find('.chal').text().trim();
var team = elem.find('.team').attr('id');
var team_name = elem.find('.team').text().trim();
var key_id = elem.find('.flag').attr('id');
load_confirm_modal(key_id, team_name, chal_name);
});
</script>
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,110 @@
html, body, .container {
font-family: 'Lato', 'LatoOffline', sans-serif;
}
h1, h2 {
font-family: 'Raleway', 'RalewayOffline', sans-serif;
font-weight: 500;
letter-spacing: 2px;
}
a {
color: #337ab7;
text-decoration: none;
}
table > thead > tr > td {
/* Remove border line from thead of all tables */
/* It can overlap with other element styles */
border-top: none !important;
}
#score-graph {
height: 450px;
display: block;
clear: both;
}
.fa.spinner {
margin-top: 225px;
text-align: center;
opacity: 0.5;
}
.spinner-error {
padding-top: 20vh;
text-align: center;
opacity: 0.5;
}
.jumbotron {
background-color: #343a40;
color: #FFF;
border-radius: 0;
text-align: center;
}
.form-control {
position: relative;
display: block;
padding: 0.8em;
border-radius: 0;
background: #f0f0f0;
color: #aaa;
font-weight: 400;
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-appearance: none;
}
.form-control:focus {
background-color: transparent;
border-color: #a3d39c;
box-shadow: 0 0 0 0.2rem #a3d39c;
transition: background-color 0.3s, border-color 0.3s;
}
.input-filled-valid {
background-color: transparent;
border-color: #a3d39c;
box-shadow: 0 0 0 0.2rem #a3d39c;
transition: background-color 0.3s, border-color 0.3s;
}
.btn-outlined.btn-theme {
background: none;
color: #545454;
border-color: #545454;
border: 3px solid;
}
.btn-outlined {
border-radius: 0;
-webkit-transition: all 0.3s;
-moz-transition: all 0.3s;
transition: all 0.3s;
}
.btn {
letter-spacing: 1px;
text-decoration: none;
-moz-user-select: none;
border-radius: 0;
cursor: pointer;
display: inline-block;
margin-bottom: 0;
vertical-align: middle;
white-space: nowrap;
font-size: 14px;
line-height: 20px;
font-weight: 700;
padding: 8px 20px;
}
.btn-info {
background-color: #5B7290 !important;
}
.alert {
border-radius: 0 !important;
padding: 0.8em;
}

View File

@ -0,0 +1,53 @@
.chal-desc {
padding-left: 30px;
padding-right: 30px;
font-size: 14px;
}
.modal-content {
border-radius: 0px;
max-width: 1000px;
padding: 1em;
margin: 0 auto;
}
.btn-info {
background-color: #5B7290 !important;
}
.badge-info {
background-color: #5B7290 !important;
}
.btn-file:nth-child(1):before {
content: "\f0ed";
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.solved-challenge {
background-color: #37d63e !important;
opacity: 0.4;
border: none;
}
.solved-challenge p:after {
content: "\f00c";
display: inline-block;
font: normal normal normal 18px/1 FontAwesome;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin-top: -10px;
margin-right: 25px;
position: absolute;
right: 0;
}
.key-submit .btn {
height: 51px;
}

View File

@ -0,0 +1,4 @@
/* Move down content because we have a fixed navbar that is 3.5rem tall */
body {
padding-top: 3.5rem;
}

View File

@ -0,0 +1,19 @@
/* Sticky footer styles
-------------------------------------------------- */
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px; /* Margin bottom by footer height */
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
line-height: 60px; /* Vertically center the text there */
/*background-color: #f5f5f5;*/
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More