mirror of https://github.com/JohnHammond/CTFd.git
Format all the things (#991)
* Format Javascript and CSS files with `prettier`: `prettier --write 'CTFd/themes/**/*'` * Format Python with `black`: `black CTFd` & `black tests` * Travis now uses xenial instead of trusty.selenium-screenshot-testing
parent
3d23ece370
commit
6833378c36
|
@ -69,4 +69,7 @@ CTFd/uploads
|
||||||
.vagrant
|
.vagrant
|
||||||
|
|
||||||
# CTFd Exports
|
# CTFd Exports
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
|
# JS
|
||||||
|
node_modules/
|
|
@ -0,0 +1,10 @@
|
||||||
|
CTFd/themes/**/vendor/
|
||||||
|
*.html
|
||||||
|
*.njk
|
||||||
|
*.png
|
||||||
|
*.svg
|
||||||
|
*.ico
|
||||||
|
*.ai
|
||||||
|
*.svg
|
||||||
|
*.mp3
|
||||||
|
*.webm
|
18
.travis.yml
18
.travis.yml
|
@ -1,5 +1,8 @@
|
||||||
language: python
|
language: python
|
||||||
cache: pip
|
dist: xenial
|
||||||
|
cache:
|
||||||
|
- pip
|
||||||
|
- yarn
|
||||||
services:
|
services:
|
||||||
- mysql
|
- mysql
|
||||||
- postgresql
|
- postgresql
|
||||||
|
@ -7,26 +10,25 @@ services:
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- mysql-5.7-trusty
|
- deadsnakes
|
||||||
packages:
|
packages:
|
||||||
- mysql-server
|
- python3.6
|
||||||
- mysql-client
|
- python3-pip
|
||||||
env:
|
env:
|
||||||
- TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd'
|
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
|
||||||
- TESTING_DATABASE_URL='sqlite://'
|
- TESTING_DATABASE_URL='sqlite://'
|
||||||
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
||||||
python:
|
python:
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.6
|
- 3.6
|
||||||
before_install:
|
before_install:
|
||||||
- sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
|
|
||||||
- sudo mysql_upgrade -u root -ppassword
|
|
||||||
- sudo service mysql restart
|
|
||||||
- sudo rm -f /etc/boto.cfg
|
- sudo rm -f /etc/boto.cfg
|
||||||
- export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
- export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||||
- export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
- export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||||
|
- python3.6 -m pip install black>=19.3b0
|
||||||
install:
|
install:
|
||||||
- pip install -r development.txt
|
- pip install -r development.txt
|
||||||
|
- yarn global add prettier@1.17.0
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c 'create database ctfd;' -U postgres
|
- psql -c 'create database ctfd;' -U postgres
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -28,7 +28,7 @@ if sys.version_info[0] < 3:
|
||||||
reload(sys) # noqa: F821
|
reload(sys) # noqa: F821
|
||||||
sys.setdefaultencoding("utf-8")
|
sys.setdefaultencoding("utf-8")
|
||||||
|
|
||||||
__version__ = '2.1.1'
|
__version__ = "2.1.1"
|
||||||
|
|
||||||
|
|
||||||
class CTFdRequest(Request):
|
class CTFdRequest(Request):
|
||||||
|
@ -48,7 +48,7 @@ class CTFdFlask(Flask):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Overriden Jinja constructor setting a custom jinja_environment"""
|
"""Overriden Jinja constructor setting a custom jinja_environment"""
|
||||||
self.jinja_environment = SandboxedBaseEnvironment
|
self.jinja_environment = SandboxedBaseEnvironment
|
||||||
self.session_interface = CachingSessionInterface(key_prefix='session')
|
self.session_interface = CachingSessionInterface(key_prefix="session")
|
||||||
self.request_class = CTFdRequest
|
self.request_class = CTFdRequest
|
||||||
Flask.__init__(self, *args, **kwargs)
|
Flask.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -59,9 +59,10 @@ class CTFdFlask(Flask):
|
||||||
|
|
||||||
class SandboxedBaseEnvironment(SandboxedEnvironment):
|
class SandboxedBaseEnvironment(SandboxedEnvironment):
|
||||||
"""SandboxEnvironment that mimics the Flask BaseEnvironment"""
|
"""SandboxEnvironment that mimics the Flask BaseEnvironment"""
|
||||||
|
|
||||||
def __init__(self, app, **options):
|
def __init__(self, app, **options):
|
||||||
if 'loader' not in options:
|
if "loader" not in options:
|
||||||
options['loader'] = app.create_global_jinja_loader()
|
options["loader"] = app.create_global_jinja_loader()
|
||||||
# Disable cache entirely so that themes can be switched (#662)
|
# Disable cache entirely so that themes can be switched (#662)
|
||||||
# If the cache is enabled, switching themes will cause odd rendering errors
|
# If the cache is enabled, switching themes will cause odd rendering errors
|
||||||
SandboxedEnvironment.__init__(self, cache_size=0, **options)
|
SandboxedEnvironment.__init__(self, cache_size=0, **options)
|
||||||
|
@ -70,7 +71,8 @@ class SandboxedBaseEnvironment(SandboxedEnvironment):
|
||||||
|
|
||||||
class ThemeLoader(FileSystemLoader):
|
class ThemeLoader(FileSystemLoader):
|
||||||
"""Custom FileSystemLoader that switches themes based on the configuration value"""
|
"""Custom FileSystemLoader that switches themes based on the configuration value"""
|
||||||
def __init__(self, searchpath, encoding='utf-8', followlinks=False):
|
|
||||||
|
def __init__(self, searchpath, encoding="utf-8", followlinks=False):
|
||||||
super(ThemeLoader, self).__init__(searchpath, encoding, followlinks)
|
super(ThemeLoader, self).__init__(searchpath, encoding, followlinks)
|
||||||
self.overriden_templates = {}
|
self.overriden_templates = {}
|
||||||
|
|
||||||
|
@ -80,14 +82,14 @@ class ThemeLoader(FileSystemLoader):
|
||||||
return self.overriden_templates[template], template, True
|
return self.overriden_templates[template], template, True
|
||||||
|
|
||||||
# Check if the template requested is for the admin panel
|
# Check if the template requested is for the admin panel
|
||||||
if template.startswith('admin/'):
|
if template.startswith("admin/"):
|
||||||
template = template[6:] # Strip out admin/
|
template = template[6:] # Strip out admin/
|
||||||
template = "/".join(['admin', 'templates', template])
|
template = "/".join(["admin", "templates", template])
|
||||||
return super(ThemeLoader, self).get_source(environment, template)
|
return super(ThemeLoader, self).get_source(environment, template)
|
||||||
|
|
||||||
# Load regular theme data
|
# Load regular theme data
|
||||||
theme = utils.get_config('ctf_theme')
|
theme = utils.get_config("ctf_theme")
|
||||||
template = "/".join([theme, 'templates', template])
|
template = "/".join([theme, "templates", template])
|
||||||
return super(ThemeLoader, self).get_source(environment, template)
|
return super(ThemeLoader, self).get_source(environment, template)
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,10 +98,10 @@ def confirm_upgrade():
|
||||||
print("/*\\ CTFd has updated and must update the database! /*\\")
|
print("/*\\ CTFd has updated and must update the database! /*\\")
|
||||||
print("/*\\ Please backup your database before proceeding! /*\\")
|
print("/*\\ Please backup your database before proceeding! /*\\")
|
||||||
print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\")
|
print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\")
|
||||||
if input('Run database migrations (Y/N)').lower().strip() == 'y':
|
if input("Run database migrations (Y/N)").lower().strip() == "y":
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print('/*\\ Ignored database migrations... /*\\')
|
print("/*\\ Ignored database migrations... /*\\")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
@ -107,24 +109,36 @@ def confirm_upgrade():
|
||||||
|
|
||||||
def run_upgrade():
|
def run_upgrade():
|
||||||
upgrade()
|
upgrade()
|
||||||
utils.set_config('ctf_version', __version__)
|
utils.set_config("ctf_version", __version__)
|
||||||
|
|
||||||
|
|
||||||
def create_app(config='CTFd.config.Config'):
|
def create_app(config="CTFd.config.Config"):
|
||||||
app = CTFdFlask(__name__)
|
app = CTFdFlask(__name__)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
|
|
||||||
theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True)
|
theme_loader = ThemeLoader(
|
||||||
|
os.path.join(app.root_path, "themes"), followlinks=True
|
||||||
|
)
|
||||||
app.jinja_loader = theme_loader
|
app.jinja_loader = theme_loader
|
||||||
|
|
||||||
from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking # noqa: F401
|
from CTFd.models import ( # noqa: F401
|
||||||
|
db,
|
||||||
|
Teams,
|
||||||
|
Solves,
|
||||||
|
Challenges,
|
||||||
|
Fails,
|
||||||
|
Flags,
|
||||||
|
Tags,
|
||||||
|
Files,
|
||||||
|
Tracking,
|
||||||
|
)
|
||||||
|
|
||||||
url = create_database()
|
url = create_database()
|
||||||
|
|
||||||
# This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in
|
# This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in
|
||||||
# This is mostly so we can force MySQL's charset
|
# This is mostly so we can force MySQL's charset
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = str(url)
|
app.config["SQLALCHEMY_DATABASE_URI"] = str(url)
|
||||||
|
|
||||||
# Register database
|
# Register database
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
@ -133,7 +147,7 @@ def create_app(config='CTFd.config.Config'):
|
||||||
migrations.init_app(app, db)
|
migrations.init_app(app, db)
|
||||||
|
|
||||||
# Alembic sqlite support is lacking so we should just create_all anyway
|
# Alembic sqlite support is lacking so we should just create_all anyway
|
||||||
if url.drivername.startswith('sqlite'):
|
if url.drivername.startswith("sqlite"):
|
||||||
db.create_all()
|
db.create_all()
|
||||||
stamp()
|
stamp()
|
||||||
else:
|
else:
|
||||||
|
@ -153,15 +167,11 @@ def create_app(config='CTFd.config.Config'):
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
app.cache = cache
|
app.cache = cache
|
||||||
|
|
||||||
reverse_proxy = app.config.get('REVERSE_PROXY')
|
reverse_proxy = app.config.get("REVERSE_PROXY")
|
||||||
if reverse_proxy:
|
if reverse_proxy:
|
||||||
if ',' in reverse_proxy:
|
if "," in reverse_proxy:
|
||||||
proxyfix_args = [int(i) for i in reverse_proxy.split(',')]
|
proxyfix_args = [int(i) for i in reverse_proxy.split(",")]
|
||||||
app.wsgi_app = ProxyFix(
|
app.wsgi_app = ProxyFix(app.wsgi_app, None, *proxyfix_args)
|
||||||
app.wsgi_app,
|
|
||||||
None,
|
|
||||||
*proxyfix_args
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
app.wsgi_app = ProxyFix(
|
app.wsgi_app = ProxyFix(
|
||||||
app.wsgi_app,
|
app.wsgi_app,
|
||||||
|
@ -170,10 +180,10 @@ def create_app(config='CTFd.config.Config'):
|
||||||
x_proto=1,
|
x_proto=1,
|
||||||
x_host=1,
|
x_host=1,
|
||||||
x_port=1,
|
x_port=1,
|
||||||
x_prefix=1
|
x_prefix=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
version = utils.get_config('ctf_version')
|
version = utils.get_config("ctf_version")
|
||||||
|
|
||||||
# Upgrading from an older version of CTFd
|
# Upgrading from an older version of CTFd
|
||||||
if version and (StrictVersion(version) < StrictVersion(__version__)):
|
if version and (StrictVersion(version) < StrictVersion(__version__)):
|
||||||
|
@ -183,10 +193,10 @@ def create_app(config='CTFd.config.Config'):
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
if not version:
|
if not version:
|
||||||
utils.set_config('ctf_version', __version__)
|
utils.set_config("ctf_version", __version__)
|
||||||
|
|
||||||
if not utils.get_config('ctf_theme'):
|
if not utils.get_config("ctf_theme"):
|
||||||
utils.set_config('ctf_theme', 'core')
|
utils.set_config("ctf_theme", "core")
|
||||||
|
|
||||||
update_check(force=True)
|
update_check(force=True)
|
||||||
|
|
||||||
|
|
|
@ -7,22 +7,18 @@ from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
abort,
|
abort,
|
||||||
render_template_string,
|
render_template_string,
|
||||||
send_file
|
send_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
from CTFd.utils.decorators import admins_only
|
from CTFd.utils.decorators import admins_only
|
||||||
from CTFd.utils.user import is_admin
|
from CTFd.utils.user import is_admin
|
||||||
from CTFd.utils.security.auth import logout_user
|
from CTFd.utils.security.auth import logout_user
|
||||||
from CTFd.utils import (
|
from CTFd.utils import config as ctf_config, get_config, set_config
|
||||||
config as ctf_config,
|
|
||||||
get_config,
|
|
||||||
set_config,
|
|
||||||
)
|
|
||||||
from CTFd.cache import cache, clear_config
|
from CTFd.cache import cache, clear_config
|
||||||
from CTFd.utils.helpers import get_errors
|
from CTFd.utils.helpers import get_errors
|
||||||
from CTFd.utils.exports import (
|
from CTFd.utils.exports import (
|
||||||
export_ctf as export_ctf_util,
|
export_ctf as export_ctf_util,
|
||||||
import_ctf as import_ctf_util
|
import_ctf as import_ctf_util,
|
||||||
)
|
)
|
||||||
from CTFd.models import (
|
from CTFd.models import (
|
||||||
db,
|
db,
|
||||||
|
@ -34,7 +30,7 @@ from CTFd.models import (
|
||||||
Solves,
|
Solves,
|
||||||
Awards,
|
Awards,
|
||||||
Unlocks,
|
Unlocks,
|
||||||
Tracking
|
Tracking,
|
||||||
)
|
)
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
@ -42,7 +38,7 @@ import six
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
|
|
||||||
admin = Blueprint('admin', __name__)
|
admin = Blueprint("admin", __name__)
|
||||||
|
|
||||||
|
|
||||||
from CTFd.admin import challenges # noqa: F401
|
from CTFd.admin import challenges # noqa: F401
|
||||||
|
@ -55,40 +51,45 @@ from CTFd.admin import submissions # noqa: F401
|
||||||
from CTFd.admin import notifications # noqa: F401
|
from CTFd.admin import notifications # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin', methods=['GET'])
|
@admin.route("/admin", methods=["GET"])
|
||||||
def view():
|
def view():
|
||||||
if is_admin():
|
if is_admin():
|
||||||
return redirect(url_for('admin.statistics'))
|
return redirect(url_for("admin.statistics"))
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/plugins/<plugin>', methods=['GET', 'POST'])
|
@admin.route("/admin/plugins/<plugin>", methods=["GET", "POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def plugin(plugin):
|
def plugin(plugin):
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
plugins_path = os.path.join(app.root_path, 'plugins')
|
plugins_path = os.path.join(app.root_path, "plugins")
|
||||||
|
|
||||||
config_html_plugins = [name for name in os.listdir(plugins_path)
|
config_html_plugins = [
|
||||||
if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))]
|
name
|
||||||
|
for name in os.listdir(plugins_path)
|
||||||
|
if os.path.isfile(os.path.join(plugins_path, name, "config.html"))
|
||||||
|
]
|
||||||
|
|
||||||
if plugin in config_html_plugins:
|
if plugin in config_html_plugins:
|
||||||
config_html = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
|
config_html = open(
|
||||||
|
os.path.join(app.root_path, "plugins", plugin, "config.html")
|
||||||
|
).read()
|
||||||
return render_template_string(config_html)
|
return render_template_string(config_html)
|
||||||
abort(404)
|
abort(404)
|
||||||
elif request.method == 'POST':
|
elif request.method == "POST":
|
||||||
for k, v in request.form.items():
|
for k, v in request.form.items():
|
||||||
if k == "nonce":
|
if k == "nonce":
|
||||||
continue
|
continue
|
||||||
set_config(k, v)
|
set_config(k, v)
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
clear_config()
|
clear_config()
|
||||||
return '1'
|
return "1"
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/import', methods=['POST'])
|
@admin.route("/admin/import", methods=["POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def import_ctf():
|
def import_ctf():
|
||||||
backup = request.files['backup']
|
backup = request.files["backup"]
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
try:
|
try:
|
||||||
import_ctf_util(backup)
|
import_ctf_util(backup)
|
||||||
|
@ -99,10 +100,10 @@ def import_ctf():
|
||||||
if errors:
|
if errors:
|
||||||
return errors[0], 500
|
return errors[0], 500
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('admin.config'))
|
return redirect(url_for("admin.config"))
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/export', methods=['GET', 'POST'])
|
@admin.route("/admin/export", methods=["GET", "POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def export_ctf():
|
def export_ctf():
|
||||||
backup = export_ctf_util()
|
backup = export_ctf_util()
|
||||||
|
@ -112,10 +113,10 @@ def export_ctf():
|
||||||
return send_file(backup, as_attachment=True, attachment_filename=full_name)
|
return send_file(backup, as_attachment=True, attachment_filename=full_name)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/export/csv')
|
@admin.route("/admin/export/csv")
|
||||||
@admins_only
|
@admins_only
|
||||||
def export_csv():
|
def export_csv():
|
||||||
table = request.args.get('table')
|
table = request.args.get("table")
|
||||||
|
|
||||||
# TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
|
# TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
|
||||||
model = get_class_by_tablename(table)
|
model = get_class_by_tablename(table)
|
||||||
|
@ -131,18 +132,22 @@ def export_csv():
|
||||||
responses = model.query.all()
|
responses = model.query.all()
|
||||||
|
|
||||||
for curr in responses:
|
for curr in responses:
|
||||||
writer.writerow([getattr(curr, column.name) for column in model.__mapper__.columns])
|
writer.writerow(
|
||||||
|
[getattr(curr, column.name) for column in model.__mapper__.columns]
|
||||||
|
)
|
||||||
|
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
return send_file(
|
return send_file(
|
||||||
output,
|
output,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
cache_timeout=-1,
|
cache_timeout=-1,
|
||||||
attachment_filename="{name}-{table}.csv".format(name=ctf_config.ctf_name(), table=table)
|
attachment_filename="{name}-{table}.csv".format(
|
||||||
|
name=ctf_config.ctf_name(), table=table
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/config', methods=['GET', 'POST'])
|
@admin.route("/admin/config", methods=["GET", "POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def config():
|
def config():
|
||||||
# Clear the config cache so that we don't get stale values
|
# Clear the config cache so that we don't get stale values
|
||||||
|
@ -154,20 +159,17 @@ def config():
|
||||||
configs = dict([(c.key, get_config(c.key)) for c in configs])
|
configs = dict([(c.key, get_config(c.key)) for c in configs])
|
||||||
|
|
||||||
themes = ctf_config.get_themes()
|
themes = ctf_config.get_themes()
|
||||||
themes.remove(get_config('ctf_theme'))
|
themes.remove(get_config("ctf_theme"))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/config.html',
|
"admin/config.html", database_tables=database_tables, themes=themes, **configs
|
||||||
database_tables=database_tables,
|
|
||||||
themes=themes,
|
|
||||||
**configs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/reset', methods=['GET', 'POST'])
|
@admin.route("/admin/reset", methods=["GET", "POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def reset():
|
def reset():
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
|
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
|
||||||
Tracking.query.delete()
|
Tracking.query.delete()
|
||||||
Solves.query.delete()
|
Solves.query.delete()
|
||||||
|
@ -176,11 +178,11 @@ def reset():
|
||||||
Unlocks.query.delete()
|
Unlocks.query.delete()
|
||||||
Users.query.delete()
|
Users.query.delete()
|
||||||
Teams.query.delete()
|
Teams.query.delete()
|
||||||
set_config('setup', False)
|
set_config("setup", False)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
cache.clear()
|
cache.clear()
|
||||||
logout_user()
|
logout_user()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return redirect(url_for('views.setup'))
|
return redirect(url_for("views.setup"))
|
||||||
|
|
||||||
return render_template('admin/reset.html')
|
return render_template("admin/reset.html")
|
||||||
|
|
|
@ -8,44 +8,48 @@ import os
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/challenges')
|
@admin.route("/admin/challenges")
|
||||||
@admins_only
|
@admins_only
|
||||||
def challenges_listing():
|
def challenges_listing():
|
||||||
challenges = Challenges.query.all()
|
challenges = Challenges.query.all()
|
||||||
return render_template('admin/challenges/challenges.html', challenges=challenges)
|
return render_template("admin/challenges/challenges.html", challenges=challenges)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/challenges/<int:challenge_id>')
|
@admin.route("/admin/challenges/<int:challenge_id>")
|
||||||
@admins_only
|
@admins_only
|
||||||
def challenges_detail(challenge_id):
|
def challenges_detail(challenge_id):
|
||||||
challenges = dict(Challenges.query.with_entities(Challenges.id, Challenges.name).all())
|
challenges = dict(
|
||||||
|
Challenges.query.with_entities(Challenges.id, Challenges.name).all()
|
||||||
|
)
|
||||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||||
solves = Solves.query.filter_by(challenge_id=challenge.id).all()
|
solves = Solves.query.filter_by(challenge_id=challenge.id).all()
|
||||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||||
challenge_class = get_chal_class(challenge.type)
|
challenge_class = get_chal_class(challenge.type)
|
||||||
|
|
||||||
with open(os.path.join(app.root_path, challenge_class.templates['update'].lstrip('/')), 'rb') as update:
|
with open(
|
||||||
|
os.path.join(app.root_path, challenge_class.templates["update"].lstrip("/")),
|
||||||
|
"rb",
|
||||||
|
) as update:
|
||||||
tpl = update.read()
|
tpl = update.read()
|
||||||
if six.PY3 and isinstance(tpl, binary_type):
|
if six.PY3 and isinstance(tpl, binary_type):
|
||||||
tpl = tpl.decode('utf-8')
|
tpl = tpl.decode("utf-8")
|
||||||
update_j2 = render_template_string(
|
update_j2 = render_template_string(tpl, challenge=challenge)
|
||||||
tpl,
|
|
||||||
challenge=challenge
|
|
||||||
)
|
|
||||||
|
|
||||||
update_script = url_for('views.static_html', route=challenge_class.scripts['update'].lstrip('/'))
|
update_script = url_for(
|
||||||
|
"views.static_html", route=challenge_class.scripts["update"].lstrip("/")
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/challenges/challenge.html',
|
"admin/challenges/challenge.html",
|
||||||
update_template=update_j2,
|
update_template=update_j2,
|
||||||
update_script=update_script,
|
update_script=update_script,
|
||||||
challenge=challenge,
|
challenge=challenge,
|
||||||
challenges=challenges,
|
challenges=challenges,
|
||||||
solves=solves,
|
solves=solves,
|
||||||
flags=flags
|
flags=flags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/challenges/new')
|
@admin.route("/admin/challenges/new")
|
||||||
@admins_only
|
@admins_only
|
||||||
def challenges_new():
|
def challenges_new():
|
||||||
return render_template('admin/challenges/new.html')
|
return render_template("admin/challenges/new.html")
|
||||||
|
|
|
@ -4,8 +4,8 @@ from CTFd.models import Notifications
|
||||||
from CTFd.admin import admin
|
from CTFd.admin import admin
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/notifications')
|
@admin.route("/admin/notifications")
|
||||||
@admins_only
|
@admins_only
|
||||||
def notifications():
|
def notifications():
|
||||||
notifs = Notifications.query.order_by(Notifications.id.desc()).all()
|
notifs = Notifications.query.order_by(Notifications.id.desc()).all()
|
||||||
return render_template('admin/notifications.html', notifications=notifs)
|
return render_template("admin/notifications.html", notifications=notifs)
|
||||||
|
|
|
@ -6,38 +6,38 @@ from CTFd.utils import markdown
|
||||||
from CTFd.admin import admin
|
from CTFd.admin import admin
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/pages')
|
@admin.route("/admin/pages")
|
||||||
@admins_only
|
@admins_only
|
||||||
def pages_listing():
|
def pages_listing():
|
||||||
pages = Pages.query.all()
|
pages = Pages.query.all()
|
||||||
return render_template('admin/pages.html', pages=pages)
|
return render_template("admin/pages.html", pages=pages)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/pages/new')
|
@admin.route("/admin/pages/new")
|
||||||
@admins_only
|
@admins_only
|
||||||
def pages_new():
|
def pages_new():
|
||||||
return render_template('admin/editor.html')
|
return render_template("admin/editor.html")
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/pages/preview', methods=['POST'])
|
@admin.route("/admin/pages/preview", methods=["POST"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def pages_preview():
|
def pages_preview():
|
||||||
data = request.form.to_dict()
|
data = request.form.to_dict()
|
||||||
schema = PageSchema()
|
schema = PageSchema()
|
||||||
page = schema.load(data)
|
page = schema.load(data)
|
||||||
return render_template('page.html', content=markdown(page.data.content))
|
return render_template("page.html", content=markdown(page.data.content))
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/pages/<int:page_id>')
|
@admin.route("/admin/pages/<int:page_id>")
|
||||||
@admins_only
|
@admins_only
|
||||||
def pages_detail(page_id):
|
def pages_detail(page_id):
|
||||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||||
page_op = request.args.get('operation')
|
page_op = request.args.get("operation")
|
||||||
|
|
||||||
if request.method == 'GET' and page_op == 'preview':
|
if request.method == "GET" and page_op == "preview":
|
||||||
return render_template('page.html', content=markdown(page.content))
|
return render_template("page.html", content=markdown(page.content))
|
||||||
|
|
||||||
if request.method == 'GET' and page_op == 'create':
|
if request.method == "GET" and page_op == "create":
|
||||||
return render_template('admin/editor.html')
|
return render_template("admin/editor.html")
|
||||||
|
|
||||||
return render_template('admin/editor.html', page=page)
|
return render_template("admin/editor.html", page=page)
|
||||||
|
|
|
@ -4,8 +4,8 @@ from CTFd.admin import admin
|
||||||
from CTFd.scoreboard import get_standings
|
from CTFd.scoreboard import get_standings
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/scoreboard')
|
@admin.route("/admin/scoreboard")
|
||||||
@admins_only
|
@admins_only
|
||||||
def scoreboard_listing():
|
def scoreboard_listing():
|
||||||
standings = get_standings(admin=True)
|
standings = get_standings(admin=True)
|
||||||
return render_template('admin/scoreboard.html', standings=standings)
|
return render_template("admin/scoreboard.html", standings=standings)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from CTFd.models import db, Solves, Challenges, Fails, Tracking
|
||||||
from CTFd.admin import admin
|
from CTFd.admin import admin
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/statistics', methods=['GET'])
|
@admin.route("/admin/statistics", methods=["GET"])
|
||||||
@admins_only
|
@admins_only
|
||||||
def statistics():
|
def statistics():
|
||||||
update_check()
|
update_check()
|
||||||
|
@ -15,47 +15,41 @@ def statistics():
|
||||||
|
|
||||||
teams_registered = Model.query.count()
|
teams_registered = Model.query.count()
|
||||||
|
|
||||||
wrong_count = Fails.query.join(
|
wrong_count = (
|
||||||
Model,
|
Fails.query.join(Model, Fails.account_id == Model.id)
|
||||||
Fails.account_id == Model.id
|
.filter(Model.banned == False, Model.hidden == False)
|
||||||
).filter(
|
.count()
|
||||||
Model.banned == False,
|
)
|
||||||
Model.hidden == False
|
|
||||||
).count()
|
|
||||||
|
|
||||||
solve_count = Solves.query.join(
|
solve_count = (
|
||||||
Model,
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
Solves.account_id == Model.id
|
.filter(Model.banned == False, Model.hidden == False)
|
||||||
).filter(
|
.count()
|
||||||
Model.banned == False,
|
)
|
||||||
Model.hidden == False
|
|
||||||
).count()
|
|
||||||
|
|
||||||
challenge_count = Challenges.query.count()
|
challenge_count = Challenges.query.count()
|
||||||
|
|
||||||
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
|
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
|
||||||
|
|
||||||
solves_sub = db.session.query(
|
solves_sub = (
|
||||||
Solves.challenge_id,
|
db.session.query(
|
||||||
db.func.count(Solves.challenge_id).label('solves_cnt')
|
Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves_cnt")
|
||||||
).join(
|
)
|
||||||
Model,
|
.join(Model, Solves.account_id == Model.id)
|
||||||
Solves.account_id == Model.id
|
.filter(Model.banned == False, Model.hidden == False)
|
||||||
).filter(
|
.group_by(Solves.challenge_id)
|
||||||
Model.banned == False,
|
.subquery()
|
||||||
Model.hidden == False
|
)
|
||||||
).group_by(
|
|
||||||
Solves.challenge_id
|
|
||||||
).subquery()
|
|
||||||
|
|
||||||
solves = db.session.query(
|
solves = (
|
||||||
solves_sub.columns.challenge_id,
|
db.session.query(
|
||||||
solves_sub.columns.solves_cnt,
|
solves_sub.columns.challenge_id,
|
||||||
Challenges.name
|
solves_sub.columns.solves_cnt,
|
||||||
).join(
|
Challenges.name,
|
||||||
Challenges,
|
)
|
||||||
solves_sub.columns.challenge_id == Challenges.id
|
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id)
|
||||||
).all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
solve_data = {}
|
solve_data = {}
|
||||||
for chal, count, name in solves:
|
for chal, count, name in solves:
|
||||||
|
@ -70,7 +64,7 @@ def statistics():
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/statistics.html',
|
"admin/statistics.html",
|
||||||
team_count=teams_registered,
|
team_count=teams_registered,
|
||||||
ip_count=ip_count,
|
ip_count=ip_count,
|
||||||
wrong_count=wrong_count,
|
wrong_count=wrong_count,
|
||||||
|
@ -78,5 +72,5 @@ def statistics():
|
||||||
challenge_count=challenge_count,
|
challenge_count=challenge_count,
|
||||||
solve_data=solve_data,
|
solve_data=solve_data,
|
||||||
most_solved=most_solved,
|
most_solved=most_solved,
|
||||||
least_solved=least_solved
|
least_solved=least_solved,
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,15 +5,15 @@ from CTFd.utils.modes import get_model
|
||||||
from CTFd.admin import admin
|
from CTFd.admin import admin
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/submissions', defaults={'submission_type': None})
|
@admin.route("/admin/submissions", defaults={"submission_type": None})
|
||||||
@admin.route('/admin/submissions/<submission_type>')
|
@admin.route("/admin/submissions/<submission_type>")
|
||||||
@admins_only
|
@admins_only
|
||||||
def submissions_listing(submission_type):
|
def submissions_listing(submission_type):
|
||||||
filters = {}
|
filters = {}
|
||||||
if submission_type:
|
if submission_type:
|
||||||
filters['type'] = submission_type
|
filters["type"] = submission_type
|
||||||
|
|
||||||
curr_page = abs(int(request.args.get('page', 1, type=int)))
|
curr_page = abs(int(request.args.get("page", 1, type=int)))
|
||||||
results_per_page = 50
|
results_per_page = 50
|
||||||
page_start = results_per_page * (curr_page - 1)
|
page_start = results_per_page * (curr_page - 1)
|
||||||
page_end = results_per_page * (curr_page - 1) + results_per_page
|
page_end = results_per_page * (curr_page - 1) + results_per_page
|
||||||
|
@ -22,27 +22,29 @@ def submissions_listing(submission_type):
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
submissions = Submissions.query.add_columns(
|
submissions = (
|
||||||
Submissions.id,
|
Submissions.query.add_columns(
|
||||||
Submissions.type,
|
Submissions.id,
|
||||||
Submissions.challenge_id,
|
Submissions.type,
|
||||||
Submissions.provided,
|
Submissions.challenge_id,
|
||||||
Submissions.account_id,
|
Submissions.provided,
|
||||||
Submissions.date,
|
Submissions.account_id,
|
||||||
Challenges.name.label('challenge_name'),
|
Submissions.date,
|
||||||
Model.name.label('team_name')
|
Challenges.name.label("challenge_name"),
|
||||||
)\
|
Model.name.label("team_name"),
|
||||||
.filter_by(**filters) \
|
)
|
||||||
.join(Challenges)\
|
.filter_by(**filters)
|
||||||
.join(Model)\
|
.join(Challenges)
|
||||||
.order_by(Submissions.date.desc())\
|
.join(Model)
|
||||||
.slice(page_start, page_end)\
|
.order_by(Submissions.date.desc())
|
||||||
|
.slice(page_start, page_end)
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/submissions.html',
|
"admin/submissions.html",
|
||||||
submissions=submissions,
|
submissions=submissions,
|
||||||
page_count=page_count,
|
page_count=page_count,
|
||||||
curr_page=curr_page,
|
curr_page=curr_page,
|
||||||
type=submission_type
|
type=submission_type,
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,28 +7,47 @@ from CTFd.utils.helpers import get_errors
|
||||||
from sqlalchemy.sql import not_
|
from sqlalchemy.sql import not_
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/teams')
|
@admin.route("/admin/teams")
|
||||||
@admins_only
|
@admins_only
|
||||||
def teams_listing():
|
def teams_listing():
|
||||||
page = abs(request.args.get('page', 1, type=int))
|
page = abs(request.args.get("page", 1, type=int))
|
||||||
q = request.args.get('q')
|
q = request.args.get("q")
|
||||||
if q:
|
if q:
|
||||||
field = request.args.get('field')
|
field = request.args.get("field")
|
||||||
teams = []
|
teams = []
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
if field == 'id':
|
if field == "id":
|
||||||
if q.isnumeric():
|
if q.isnumeric():
|
||||||
teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all()
|
teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all()
|
||||||
else:
|
else:
|
||||||
teams = []
|
teams = []
|
||||||
errors.append('Your ID search term is not numeric')
|
errors.append("Your ID search term is not numeric")
|
||||||
elif field == 'name':
|
elif field == "name":
|
||||||
teams = Teams.query.filter(Teams.name.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
teams = (
|
||||||
elif field == 'email':
|
Teams.query.filter(Teams.name.like("%{}%".format(q)))
|
||||||
teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
.order_by(Teams.id.asc())
|
||||||
elif field == 'affiliation':
|
.all()
|
||||||
teams = Teams.query.filter(Teams.affiliation.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
)
|
||||||
return render_template('admin/teams/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field)
|
elif field == "email":
|
||||||
|
teams = (
|
||||||
|
Teams.query.filter(Teams.email.like("%{}%".format(q)))
|
||||||
|
.order_by(Teams.id.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
elif field == "affiliation":
|
||||||
|
teams = (
|
||||||
|
Teams.query.filter(Teams.affiliation.like("%{}%".format(q)))
|
||||||
|
.order_by(Teams.id.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"admin/teams/teams.html",
|
||||||
|
teams=teams,
|
||||||
|
pages=None,
|
||||||
|
curr_page=None,
|
||||||
|
q=q,
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
|
||||||
page = abs(int(page))
|
page = abs(int(page))
|
||||||
results_per_page = 50
|
results_per_page = 50
|
||||||
|
@ -38,16 +57,18 @@ def teams_listing():
|
||||||
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
|
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
|
||||||
count = db.session.query(db.func.count(Teams.id)).first()[0]
|
count = db.session.query(db.func.count(Teams.id)).first()[0]
|
||||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||||
return render_template('admin/teams/teams.html', teams=teams, pages=pages, curr_page=page)
|
return render_template(
|
||||||
|
"admin/teams/teams.html", teams=teams, pages=pages, curr_page=page
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/teams/new')
|
@admin.route("/admin/teams/new")
|
||||||
@admins_only
|
@admins_only
|
||||||
def teams_new():
|
def teams_new():
|
||||||
return render_template('admin/teams/new.html')
|
return render_template("admin/teams/new.html")
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/teams/<int:team_id>')
|
@admin.route("/admin/teams/<int:team_id>")
|
||||||
@admins_only
|
@admins_only
|
||||||
def teams_detail(team_id):
|
def teams_detail(team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
|
@ -69,14 +90,17 @@ def teams_detail(team_id):
|
||||||
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
||||||
|
|
||||||
# Get addresses for all members
|
# Get addresses for all members
|
||||||
last_seen = db.func.max(Tracking.date).label('last_seen')
|
last_seen = db.func.max(Tracking.date).label("last_seen")
|
||||||
addrs = db.session.query(Tracking.ip, last_seen) \
|
addrs = (
|
||||||
.filter(Tracking.user_id.in_(member_ids)) \
|
db.session.query(Tracking.ip, last_seen)
|
||||||
.group_by(Tracking.ip) \
|
.filter(Tracking.user_id.in_(member_ids))
|
||||||
.order_by(last_seen.desc()).all()
|
.group_by(Tracking.ip)
|
||||||
|
.order_by(last_seen.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/teams/team.html',
|
"admin/teams/team.html",
|
||||||
team=team,
|
team=team,
|
||||||
members=members,
|
members=members,
|
||||||
score=score,
|
score=score,
|
||||||
|
|
|
@ -9,28 +9,47 @@ from CTFd.utils.helpers import get_errors
|
||||||
from sqlalchemy.sql import not_
|
from sqlalchemy.sql import not_
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/users')
|
@admin.route("/admin/users")
|
||||||
@admins_only
|
@admins_only
|
||||||
def users_listing():
|
def users_listing():
|
||||||
page = abs(request.args.get('page', 1, type=int))
|
page = abs(request.args.get("page", 1, type=int))
|
||||||
q = request.args.get('q')
|
q = request.args.get("q")
|
||||||
if q:
|
if q:
|
||||||
field = request.args.get('field')
|
field = request.args.get("field")
|
||||||
users = []
|
users = []
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
if field == 'id':
|
if field == "id":
|
||||||
if q.isnumeric():
|
if q.isnumeric():
|
||||||
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
|
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
|
||||||
else:
|
else:
|
||||||
users = []
|
users = []
|
||||||
errors.append('Your ID search term is not numeric')
|
errors.append("Your ID search term is not numeric")
|
||||||
elif field == 'name':
|
elif field == "name":
|
||||||
users = Users.query.filter(Users.name.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
users = (
|
||||||
elif field == 'email':
|
Users.query.filter(Users.name.like("%{}%".format(q)))
|
||||||
users = Users.query.filter(Users.email.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
.order_by(Users.id.asc())
|
||||||
elif field == 'affiliation':
|
.all()
|
||||||
users = Users.query.filter(Users.affiliation.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
)
|
||||||
return render_template('admin/users/users.html', users=users, pages=None, curr_page=None, q=q, field=field)
|
elif field == "email":
|
||||||
|
users = (
|
||||||
|
Users.query.filter(Users.email.like("%{}%".format(q)))
|
||||||
|
.order_by(Users.id.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
elif field == "affiliation":
|
||||||
|
users = (
|
||||||
|
Users.query.filter(Users.affiliation.like("%{}%".format(q)))
|
||||||
|
.order_by(Users.id.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"admin/users/users.html",
|
||||||
|
users=users,
|
||||||
|
pages=None,
|
||||||
|
curr_page=None,
|
||||||
|
q=q,
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
|
||||||
page = abs(int(page))
|
page = abs(int(page))
|
||||||
results_per_page = 50
|
results_per_page = 50
|
||||||
|
@ -41,16 +60,18 @@ def users_listing():
|
||||||
count = db.session.query(db.func.count(Users.id)).first()[0]
|
count = db.session.query(db.func.count(Users.id)).first()[0]
|
||||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||||
|
|
||||||
return render_template('admin/users/users.html', users=users, pages=pages, curr_page=page)
|
return render_template(
|
||||||
|
"admin/users/users.html", users=users, pages=pages, curr_page=page
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/users/new')
|
@admin.route("/admin/users/new")
|
||||||
@admins_only
|
@admins_only
|
||||||
def users_new():
|
def users_new():
|
||||||
return render_template('admin/users/new.html')
|
return render_template("admin/users/new.html")
|
||||||
|
|
||||||
|
|
||||||
@admin.route('/admin/users/<int:user_id>')
|
@admin.route("/admin/users/<int:user_id>")
|
||||||
@admins_only
|
@admins_only
|
||||||
def users_detail(user_id):
|
def users_detail(user_id):
|
||||||
# Get user object
|
# Get user object
|
||||||
|
@ -60,7 +81,7 @@ def users_detail(user_id):
|
||||||
solves = user.get_solves(admin=True)
|
solves = user.get_solves(admin=True)
|
||||||
|
|
||||||
# Get challenges that the user is missing
|
# Get challenges that the user is missing
|
||||||
if get_config('user_mode') == TEAMS_MODE:
|
if get_config("user_mode") == TEAMS_MODE:
|
||||||
if user.team:
|
if user.team:
|
||||||
all_solves = user.team.get_solves(admin=True)
|
all_solves = user.team.get_solves(admin=True)
|
||||||
else:
|
else:
|
||||||
|
@ -72,11 +93,14 @@ def users_detail(user_id):
|
||||||
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
||||||
|
|
||||||
# Get IP addresses that the User has used
|
# Get IP addresses that the User has used
|
||||||
last_seen = db.func.max(Tracking.date).label('last_seen')
|
last_seen = db.func.max(Tracking.date).label("last_seen")
|
||||||
addrs = db.session.query(Tracking.ip, last_seen) \
|
addrs = (
|
||||||
.filter_by(user_id=user_id) \
|
db.session.query(Tracking.ip, last_seen)
|
||||||
.group_by(Tracking.ip) \
|
.filter_by(user_id=user_id)
|
||||||
.order_by(last_seen.desc()).all()
|
.group_by(Tracking.ip)
|
||||||
|
.order_by(last_seen.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
# Get Fails
|
# Get Fails
|
||||||
fails = user.get_fails(admin=True)
|
fails = user.get_fails(admin=True)
|
||||||
|
@ -89,7 +113,7 @@ def users_detail(user_id):
|
||||||
place = user.get_place(admin=True)
|
place = user.get_place(admin=True)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/users/user.html',
|
"admin/users/user.html",
|
||||||
solves=solves,
|
solves=solves,
|
||||||
user=user,
|
user=user,
|
||||||
addrs=addrs,
|
addrs=addrs,
|
||||||
|
@ -97,5 +121,5 @@ def users_detail(user_id):
|
||||||
missing=missing,
|
missing=missing,
|
||||||
place=place,
|
place=place,
|
||||||
fails=fails,
|
fails=fails,
|
||||||
awards=awards
|
awards=awards,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,21 +17,21 @@ from CTFd.api.v1.notifications import notifications_namespace
|
||||||
from CTFd.api.v1.pages import pages_namespace
|
from CTFd.api.v1.pages import pages_namespace
|
||||||
from CTFd.api.v1.unlocks import unlocks_namespace
|
from CTFd.api.v1.unlocks import unlocks_namespace
|
||||||
|
|
||||||
api = Blueprint('api', __name__, url_prefix='/api/v1')
|
api = Blueprint("api", __name__, url_prefix="/api/v1")
|
||||||
CTFd_API_v1 = Api(api, version='v1', doc=current_app.config.get('SWAGGER_UI'))
|
CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI"))
|
||||||
|
|
||||||
CTFd_API_v1.add_namespace(challenges_namespace, '/challenges')
|
CTFd_API_v1.add_namespace(challenges_namespace, "/challenges")
|
||||||
CTFd_API_v1.add_namespace(tags_namespace, '/tags')
|
CTFd_API_v1.add_namespace(tags_namespace, "/tags")
|
||||||
CTFd_API_v1.add_namespace(awards_namespace, '/awards')
|
CTFd_API_v1.add_namespace(awards_namespace, "/awards")
|
||||||
CTFd_API_v1.add_namespace(hints_namespace, '/hints')
|
CTFd_API_v1.add_namespace(hints_namespace, "/hints")
|
||||||
CTFd_API_v1.add_namespace(flags_namespace, '/flags')
|
CTFd_API_v1.add_namespace(flags_namespace, "/flags")
|
||||||
CTFd_API_v1.add_namespace(submissions_namespace, '/submissions')
|
CTFd_API_v1.add_namespace(submissions_namespace, "/submissions")
|
||||||
CTFd_API_v1.add_namespace(scoreboard_namespace, '/scoreboard')
|
CTFd_API_v1.add_namespace(scoreboard_namespace, "/scoreboard")
|
||||||
CTFd_API_v1.add_namespace(teams_namespace, '/teams')
|
CTFd_API_v1.add_namespace(teams_namespace, "/teams")
|
||||||
CTFd_API_v1.add_namespace(users_namespace, '/users')
|
CTFd_API_v1.add_namespace(users_namespace, "/users")
|
||||||
CTFd_API_v1.add_namespace(statistics_namespace, '/statistics')
|
CTFd_API_v1.add_namespace(statistics_namespace, "/statistics")
|
||||||
CTFd_API_v1.add_namespace(files_namespace, '/files')
|
CTFd_API_v1.add_namespace(files_namespace, "/files")
|
||||||
CTFd_API_v1.add_namespace(notifications_namespace, '/notifications')
|
CTFd_API_v1.add_namespace(notifications_namespace, "/notifications")
|
||||||
CTFd_API_v1.add_namespace(configs_namespace, '/configs')
|
CTFd_API_v1.add_namespace(configs_namespace, "/configs")
|
||||||
CTFd_API_v1.add_namespace(pages_namespace, '/pages')
|
CTFd_API_v1.add_namespace(pages_namespace, "/pages")
|
||||||
CTFd_API_v1.add_namespace(unlocks_namespace, '/unlocks')
|
CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks")
|
||||||
|
|
|
@ -2,16 +2,13 @@ from flask import request
|
||||||
from flask_restplus import Namespace, Resource
|
from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Awards
|
from CTFd.models import db, Awards
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
|
|
||||||
awards_namespace = Namespace('awards', description="Endpoint to retrieve Awards")
|
awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards")
|
||||||
|
|
||||||
|
|
||||||
@awards_namespace.route('')
|
@awards_namespace.route("")
|
||||||
class AwardList(Resource):
|
class AwardList(Resource):
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
|
@ -19,10 +16,7 @@ class AwardList(Resource):
|
||||||
|
|
||||||
response = schema.load(req, session=db.session)
|
response = schema.load(req, session=db.session)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -30,29 +24,20 @@ class AwardList(Resource):
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@awards_namespace.route('/<award_id>')
|
@awards_namespace.route("/<award_id>")
|
||||||
@awards_namespace.param('award_id', 'An Award ID')
|
@awards_namespace.param("award_id", "An Award ID")
|
||||||
class Award(Resource):
|
class Award(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, award_id):
|
def get(self, award_id):
|
||||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||||
response = AwardSchema().dump(award)
|
response = AwardSchema().dump(award)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, award_id):
|
def delete(self, award_id):
|
||||||
|
@ -61,6 +46,4 @@ class Award(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,14 +16,18 @@ from CTFd.utils.dates import isoformat
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import (
|
||||||
during_ctf_time_only,
|
during_ctf_time_only,
|
||||||
require_verified_emails,
|
require_verified_emails,
|
||||||
admins_only
|
admins_only,
|
||||||
)
|
)
|
||||||
from CTFd.utils.decorators.visibility import (
|
from CTFd.utils.decorators.visibility import (
|
||||||
check_challenge_visibility,
|
check_challenge_visibility,
|
||||||
check_score_visibility
|
check_score_visibility,
|
||||||
)
|
)
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.utils.config.visibility import scores_visible, accounts_visible, challenges_visible
|
from CTFd.utils.config.visibility import (
|
||||||
|
scores_visible,
|
||||||
|
accounts_visible,
|
||||||
|
challenges_visible,
|
||||||
|
)
|
||||||
from CTFd.utils.user import is_admin, authed
|
from CTFd.utils.user import is_admin, authed
|
||||||
from CTFd.utils.modes import get_model, USERS_MODE, TEAMS_MODE
|
from CTFd.utils.modes import get_model, USERS_MODE, TEAMS_MODE
|
||||||
from CTFd.schemas.tags import TagSchema
|
from CTFd.schemas.tags import TagSchema
|
||||||
|
@ -40,11 +44,12 @@ from CTFd.utils.security.signing import serialize
|
||||||
from sqlalchemy.sql import and_
|
from sqlalchemy.sql import and_
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
challenges_namespace = Namespace('challenges',
|
challenges_namespace = Namespace(
|
||||||
description="Endpoint to retrieve Challenges")
|
"challenges", description="Endpoint to retrieve Challenges"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('')
|
@challenges_namespace.route("")
|
||||||
class ChallengeList(Resource):
|
class ChallengeList(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
|
@ -53,16 +58,21 @@ class ChallengeList(Resource):
|
||||||
# This can return None (unauth) if visibility is set to public
|
# This can return None (unauth) if visibility is set to public
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
|
||||||
challenges = Challenges.query.filter(
|
challenges = (
|
||||||
and_(Challenges.state != 'hidden', Challenges.state != 'locked')
|
Challenges.query.filter(
|
||||||
).order_by(Challenges.value).all()
|
and_(Challenges.state != "hidden", Challenges.state != "locked")
|
||||||
|
)
|
||||||
|
.order_by(Challenges.value)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
solve_ids = Solves.query\
|
solve_ids = (
|
||||||
.with_entities(Solves.challenge_id)\
|
Solves.query.with_entities(Solves.challenge_id)
|
||||||
.filter_by(account_id=user.account_id)\
|
.filter_by(account_id=user.account_id)
|
||||||
.order_by(Solves.challenge_id.asc())\
|
.order_by(Solves.challenge_id.asc())
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
solve_ids = set([value for value, in solve_ids])
|
solve_ids = set([value for value, in solve_ids])
|
||||||
|
|
||||||
# TODO: Convert this into a re-useable decorator
|
# TODO: Convert this into a re-useable decorator
|
||||||
|
@ -75,61 +85,59 @@ class ChallengeList(Resource):
|
||||||
solve_ids = set()
|
solve_ids = set()
|
||||||
|
|
||||||
response = []
|
response = []
|
||||||
tag_schema = TagSchema(view='user', many=True)
|
tag_schema = TagSchema(view="user", many=True)
|
||||||
for challenge in challenges:
|
for challenge in challenges:
|
||||||
if challenge.requirements:
|
if challenge.requirements:
|
||||||
requirements = challenge.requirements.get('prerequisites', [])
|
requirements = challenge.requirements.get("prerequisites", [])
|
||||||
anonymize = challenge.requirements.get('anonymize')
|
anonymize = challenge.requirements.get("anonymize")
|
||||||
prereqs = set(requirements)
|
prereqs = set(requirements)
|
||||||
if solve_ids >= prereqs:
|
if solve_ids >= prereqs:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if anonymize:
|
if anonymize:
|
||||||
response.append({
|
response.append(
|
||||||
'id': challenge.id,
|
{
|
||||||
'type': 'hidden',
|
"id": challenge.id,
|
||||||
'name': '???',
|
"type": "hidden",
|
||||||
'value': 0,
|
"name": "???",
|
||||||
'category': '???',
|
"value": 0,
|
||||||
'tags': [],
|
"category": "???",
|
||||||
'template': '',
|
"tags": [],
|
||||||
'script': ''
|
"template": "",
|
||||||
})
|
"script": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
# Fallthrough to continue
|
# Fallthrough to continue
|
||||||
continue
|
continue
|
||||||
|
|
||||||
challenge_type = get_chal_class(challenge.type)
|
challenge_type = get_chal_class(challenge.type)
|
||||||
response.append({
|
response.append(
|
||||||
'id': challenge.id,
|
{
|
||||||
'type': challenge_type.name,
|
"id": challenge.id,
|
||||||
'name': challenge.name,
|
"type": challenge_type.name,
|
||||||
'value': challenge.value,
|
"name": challenge.name,
|
||||||
'category': challenge.category,
|
"value": challenge.value,
|
||||||
'tags': tag_schema.dump(challenge.tags).data,
|
"category": challenge.category,
|
||||||
'template': challenge_type.templates['view'],
|
"tags": tag_schema.dump(challenge.tags).data,
|
||||||
'script': challenge_type.scripts['view'],
|
"template": challenge_type.templates["view"],
|
||||||
})
|
"script": challenge_type.scripts["view"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
challenge_type = data['type']
|
challenge_type = data["type"]
|
||||||
challenge_class = get_chal_class(challenge_type)
|
challenge_class = get_chal_class(challenge_type)
|
||||||
challenge = challenge_class.create(request)
|
challenge = challenge_class.create(request)
|
||||||
response = challenge_class.read(challenge)
|
response = challenge_class.read(challenge)
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/types')
|
@challenges_namespace.route("/types")
|
||||||
class ChallengeTypes(Resource):
|
class ChallengeTypes(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -138,19 +146,16 @@ class ChallengeTypes(Resource):
|
||||||
for class_id in CHALLENGE_CLASSES:
|
for class_id in CHALLENGE_CLASSES:
|
||||||
challenge_class = CHALLENGE_CLASSES.get(class_id)
|
challenge_class = CHALLENGE_CLASSES.get(class_id)
|
||||||
response[challenge_class.id] = {
|
response[challenge_class.id] = {
|
||||||
'id': challenge_class.id,
|
"id": challenge_class.id,
|
||||||
'name': challenge_class.name,
|
"name": challenge_class.name,
|
||||||
'templates': challenge_class.templates,
|
"templates": challenge_class.templates,
|
||||||
'scripts': challenge_class.scripts,
|
"scripts": challenge_class.scripts,
|
||||||
}
|
}
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>')
|
@challenges_namespace.route("/<challenge_id>")
|
||||||
@challenges_namespace.param('challenge_id', 'A Challenge ID')
|
@challenges_namespace.param("challenge_id", "A Challenge ID")
|
||||||
class Challenge(Resource):
|
class Challenge(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
|
@ -160,22 +165,24 @@ class Challenge(Resource):
|
||||||
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
|
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
|
||||||
else:
|
else:
|
||||||
chal = Challenges.query.filter(
|
chal = Challenges.query.filter(
|
||||||
Challenges.id == challenge_id, and_(Challenges.state != 'hidden', Challenges.state != 'locked')
|
Challenges.id == challenge_id,
|
||||||
|
and_(Challenges.state != "hidden", Challenges.state != "locked"),
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
|
|
||||||
chal_class = get_chal_class(chal.type)
|
chal_class = get_chal_class(chal.type)
|
||||||
|
|
||||||
if chal.requirements:
|
if chal.requirements:
|
||||||
requirements = chal.requirements.get('prerequisites', [])
|
requirements = chal.requirements.get("prerequisites", [])
|
||||||
anonymize = chal.requirements.get('anonymize')
|
anonymize = chal.requirements.get("anonymize")
|
||||||
if challenges_visible():
|
if challenges_visible():
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if user:
|
if user:
|
||||||
solve_ids = Solves.query \
|
solve_ids = (
|
||||||
.with_entities(Solves.challenge_id) \
|
Solves.query.with_entities(Solves.challenge_id)
|
||||||
.filter_by(account_id=user.account_id) \
|
.filter_by(account_id=user.account_id)
|
||||||
.order_by(Solves.challenge_id.asc()) \
|
.order_by(Solves.challenge_id.asc())
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# We need to handle the case where a user is viewing challenges anonymously
|
# We need to handle the case where a user is viewing challenges anonymously
|
||||||
solve_ids = []
|
solve_ids = []
|
||||||
|
@ -186,26 +193,24 @@ class Challenge(Resource):
|
||||||
else:
|
else:
|
||||||
if anonymize:
|
if anonymize:
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {
|
||||||
'id': chal.id,
|
"id": chal.id,
|
||||||
'type': 'hidden',
|
"type": "hidden",
|
||||||
'name': '???',
|
"name": "???",
|
||||||
'value': 0,
|
"value": 0,
|
||||||
'category': '???',
|
"category": "???",
|
||||||
'tags': [],
|
"tags": [],
|
||||||
'template': '',
|
"template": "",
|
||||||
'script': ''
|
"script": "",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
abort(403)
|
abort(403)
|
||||||
else:
|
else:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
tags = [
|
tags = [
|
||||||
tag['value'] for tag in TagSchema(
|
tag["value"] for tag in TagSchema("user", many=True).dump(chal.tags).data
|
||||||
"user", many=True).dump(
|
|
||||||
chal.tags).data
|
|
||||||
]
|
]
|
||||||
|
|
||||||
unlocked_hints = set()
|
unlocked_hints = set()
|
||||||
|
@ -221,56 +226,59 @@ class Challenge(Resource):
|
||||||
if config.is_teams_mode() and team is None:
|
if config.is_teams_mode() and team is None:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
unlocked_hints = set([
|
unlocked_hints = set(
|
||||||
u.target for u in HintUnlocks.query.filter_by(type='hints', account_id=user.account_id)
|
[
|
||||||
])
|
u.target
|
||||||
|
for u in HintUnlocks.query.filter_by(
|
||||||
|
type="hints", account_id=user.account_id
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
files = []
|
files = []
|
||||||
for f in chal.files:
|
for f in chal.files:
|
||||||
token = {
|
token = {
|
||||||
'user_id': user.id,
|
"user_id": user.id,
|
||||||
'team_id': team.id if team else None,
|
"team_id": team.id if team else None,
|
||||||
'file_id': f.id,
|
"file_id": f.id,
|
||||||
}
|
}
|
||||||
files.append(
|
files.append(
|
||||||
url_for('views.files', path=f.location, token=serialize(token))
|
url_for("views.files", path=f.location, token=serialize(token))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
files = [
|
files = [url_for("views.files", path=f.location) for f in chal.files]
|
||||||
url_for('views.files', path=f.location) for f in chal.files
|
|
||||||
]
|
|
||||||
|
|
||||||
for hint in Hints.query.filter_by(challenge_id=chal.id).all():
|
for hint in Hints.query.filter_by(challenge_id=chal.id).all():
|
||||||
if hint.id in unlocked_hints or ctf_ended():
|
if hint.id in unlocked_hints or ctf_ended():
|
||||||
hints.append({
|
hints.append(
|
||||||
'id': hint.id,
|
{"id": hint.id, "cost": hint.cost, "content": hint.content}
|
||||||
'cost': hint.cost,
|
)
|
||||||
'content': hint.content
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
hints.append({'id': hint.id, 'cost': hint.cost})
|
hints.append({"id": hint.id, "cost": hint.cost})
|
||||||
|
|
||||||
response = chal_class.read(challenge=chal)
|
response = chal_class.read(challenge=chal)
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
if scores_visible() is True and accounts_visible() is True:
|
if scores_visible() is True and accounts_visible() is True:
|
||||||
solves = Solves.query\
|
solves = (
|
||||||
.join(Model, Solves.account_id == Model.id)\
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
.filter(Solves.challenge_id == chal.id, Model.banned == False, Model.hidden == False)\
|
.filter(
|
||||||
|
Solves.challenge_id == chal.id,
|
||||||
|
Model.banned == False,
|
||||||
|
Model.hidden == False,
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
response['solves'] = solves
|
)
|
||||||
|
response["solves"] = solves
|
||||||
else:
|
else:
|
||||||
response['solves'] = None
|
response["solves"] = None
|
||||||
|
|
||||||
response['files'] = files
|
response["files"] = files
|
||||||
response['tags'] = tags
|
response["tags"] = tags
|
||||||
response['hints'] = hints
|
response["hints"] = hints
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, challenge_id):
|
def patch(self, challenge_id):
|
||||||
|
@ -278,10 +286,7 @@ class Challenge(Resource):
|
||||||
challenge_class = get_chal_class(challenge.type)
|
challenge_class = get_chal_class(challenge.type)
|
||||||
challenge = challenge_class.update(challenge, request)
|
challenge = challenge_class.update(challenge, request)
|
||||||
response = challenge_class.read(challenge)
|
response = challenge_class.read(challenge)
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, challenge_id):
|
def delete(self, challenge_id):
|
||||||
|
@ -289,55 +294,51 @@ class Challenge(Resource):
|
||||||
chal_class = get_chal_class(challenge.type)
|
chal_class = get_chal_class(challenge.type)
|
||||||
chal_class.delete(challenge)
|
chal_class.delete(challenge)
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/attempt')
|
@challenges_namespace.route("/attempt")
|
||||||
class ChallengeAttempt(Resource):
|
class ChallengeAttempt(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
def post(self):
|
def post(self):
|
||||||
if authed() is False:
|
if authed() is False:
|
||||||
return {
|
return {"success": True, "data": {"status": "authentication_required"}}, 403
|
||||||
'success': True,
|
|
||||||
'data': {
|
|
||||||
'status': "authentication_required",
|
|
||||||
}
|
|
||||||
}, 403
|
|
||||||
|
|
||||||
if request.content_type != 'application/json':
|
if request.content_type != "application/json":
|
||||||
request_data = request.form
|
request_data = request.form
|
||||||
else:
|
else:
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
|
|
||||||
challenge_id = request_data.get('challenge_id')
|
challenge_id = request_data.get("challenge_id")
|
||||||
|
|
||||||
if current_user.is_admin():
|
if current_user.is_admin():
|
||||||
preview = request.args.get('preview', False)
|
preview = request.args.get("preview", False)
|
||||||
if preview:
|
if preview:
|
||||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||||
chal_class = get_chal_class(challenge.type)
|
chal_class = get_chal_class(challenge.type)
|
||||||
status, message = chal_class.attempt(challenge, request)
|
status, message = chal_class.attempt(challenge, request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {
|
||||||
'status': "correct" if status else "incorrect",
|
"status": "correct" if status else "incorrect",
|
||||||
'message': message
|
"message": message,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctf_paused():
|
if ctf_paused():
|
||||||
return {
|
return (
|
||||||
'success': True,
|
{
|
||||||
'data': {
|
"success": True,
|
||||||
'status': "paused",
|
"data": {
|
||||||
'message': '{} is paused'.format(config.ctf_name())
|
"status": "paused",
|
||||||
}
|
"message": "{} is paused".format(config.ctf_name()),
|
||||||
}, 403
|
},
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
|
@ -347,26 +348,25 @@ class ChallengeAttempt(Resource):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
fails = Fails.query.filter_by(
|
fails = Fails.query.filter_by(
|
||||||
account_id=user.account_id,
|
account_id=user.account_id, challenge_id=challenge_id
|
||||||
challenge_id=challenge_id
|
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
challenge = Challenges.query.filter_by(
|
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||||
id=challenge_id).first_or_404()
|
|
||||||
|
|
||||||
if challenge.state == 'hidden':
|
if challenge.state == "hidden":
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if challenge.state == 'locked':
|
if challenge.state == "locked":
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
if challenge.requirements:
|
if challenge.requirements:
|
||||||
requirements = challenge.requirements.get('prerequisites', [])
|
requirements = challenge.requirements.get("prerequisites", [])
|
||||||
solve_ids = Solves.query \
|
solve_ids = (
|
||||||
.with_entities(Solves.challenge_id) \
|
Solves.query.with_entities(Solves.challenge_id)
|
||||||
.filter_by(account_id=user.account_id) \
|
.filter_by(account_id=user.account_id)
|
||||||
.order_by(Solves.challenge_id.asc()) \
|
.order_by(Solves.challenge_id.asc())
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
solve_ids = set([solve_id for solve_id, in solve_ids])
|
solve_ids = set([solve_id for solve_id, in solve_ids])
|
||||||
prereqs = set(requirements)
|
prereqs = set(requirements)
|
||||||
if solve_ids >= prereqs:
|
if solve_ids >= prereqs:
|
||||||
|
@ -381,29 +381,28 @@ class ChallengeAttempt(Resource):
|
||||||
if kpm > 10:
|
if kpm > 10:
|
||||||
if ctftime():
|
if ctftime():
|
||||||
chal_class.fail(
|
chal_class.fail(
|
||||||
user=user,
|
user=user, team=team, challenge=challenge, request=request
|
||||||
team=team,
|
|
||||||
challenge=challenge,
|
|
||||||
request=request
|
|
||||||
)
|
)
|
||||||
log(
|
log(
|
||||||
'submissions',
|
"submissions",
|
||||||
"[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]",
|
"[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]",
|
||||||
submission=request_data['submission'].encode('utf-8'),
|
submission=request_data["submission"].encode("utf-8"),
|
||||||
kpm=kpm
|
kpm=kpm,
|
||||||
)
|
)
|
||||||
# Submitting too fast
|
# Submitting too fast
|
||||||
return {
|
return (
|
||||||
'success': True,
|
{
|
||||||
'data': {
|
"success": True,
|
||||||
'status': "ratelimited",
|
"data": {
|
||||||
'message': "You're submitting flags too fast. Slow down."
|
"status": "ratelimited",
|
||||||
}
|
"message": "You're submitting flags too fast. Slow down.",
|
||||||
}, 429
|
},
|
||||||
|
},
|
||||||
|
429,
|
||||||
|
)
|
||||||
|
|
||||||
solves = Solves.query.filter_by(
|
solves = Solves.query.filter_by(
|
||||||
account_id=user.account_id,
|
account_id=user.account_id, challenge_id=challenge_id
|
||||||
challenge_id=challenge_id
|
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Challenge not solved yet
|
# Challenge not solved yet
|
||||||
|
@ -411,99 +410,92 @@ class ChallengeAttempt(Resource):
|
||||||
# Hit max attempts
|
# Hit max attempts
|
||||||
max_tries = challenge.max_attempts
|
max_tries = challenge.max_attempts
|
||||||
if max_tries and fails >= max_tries > 0:
|
if max_tries and fails >= max_tries > 0:
|
||||||
return {
|
return (
|
||||||
'success': True,
|
{
|
||||||
'data': {
|
"success": True,
|
||||||
'status': "incorrect",
|
"data": {
|
||||||
'message': "You have 0 tries remaining"
|
"status": "incorrect",
|
||||||
}
|
"message": "You have 0 tries remaining",
|
||||||
}, 403
|
},
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
status, message = chal_class.attempt(challenge, request)
|
status, message = chal_class.attempt(challenge, request)
|
||||||
if status: # The challenge plugin says the input is right
|
if status: # The challenge plugin says the input is right
|
||||||
if ctftime() or current_user.is_admin():
|
if ctftime() or current_user.is_admin():
|
||||||
chal_class.solve(
|
chal_class.solve(
|
||||||
user=user,
|
user=user, team=team, challenge=challenge, request=request
|
||||||
team=team,
|
|
||||||
challenge=challenge,
|
|
||||||
request=request
|
|
||||||
)
|
)
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
log(
|
log(
|
||||||
'submissions',
|
"submissions",
|
||||||
"[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]",
|
"[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]",
|
||||||
submission=request_data['submission'].encode('utf-8'),
|
submission=request_data["submission"].encode("utf-8"),
|
||||||
kpm=kpm
|
kpm=kpm,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {"status": "correct", "message": message},
|
||||||
'status': "correct",
|
|
||||||
'message': message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else: # The challenge plugin says the input is wrong
|
else: # The challenge plugin says the input is wrong
|
||||||
if ctftime() or current_user.is_admin():
|
if ctftime() or current_user.is_admin():
|
||||||
chal_class.fail(
|
chal_class.fail(
|
||||||
user=user,
|
user=user, team=team, challenge=challenge, request=request
|
||||||
team=team,
|
|
||||||
challenge=challenge,
|
|
||||||
request=request
|
|
||||||
)
|
)
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
log(
|
log(
|
||||||
'submissions',
|
"submissions",
|
||||||
"[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]",
|
"[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]",
|
||||||
submission=request_data['submission'].encode('utf-8'),
|
submission=request_data["submission"].encode("utf-8"),
|
||||||
kpm=kpm
|
kpm=kpm,
|
||||||
)
|
)
|
||||||
|
|
||||||
if max_tries:
|
if max_tries:
|
||||||
# Off by one since fails has changed since it was gotten
|
# Off by one since fails has changed since it was gotten
|
||||||
attempts_left = max_tries - fails - 1
|
attempts_left = max_tries - fails - 1
|
||||||
tries_str = 'tries'
|
tries_str = "tries"
|
||||||
if attempts_left == 1:
|
if attempts_left == 1:
|
||||||
tries_str = 'try'
|
tries_str = "try"
|
||||||
# Add a punctuation mark if there isn't one
|
# Add a punctuation mark if there isn't one
|
||||||
if message[-1] not in '!().;?[]{}':
|
if message[-1] not in "!().;?[]{}":
|
||||||
message = message + '.'
|
message = message + "."
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {
|
||||||
'status': "incorrect",
|
"status": "incorrect",
|
||||||
'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str)
|
"message": "{} You have {} {} remaining.".format(
|
||||||
}
|
message, attempts_left, tries_str
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {"status": "incorrect", "message": message},
|
||||||
'status': "incorrect",
|
|
||||||
'message': message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Challenge already solved
|
# Challenge already solved
|
||||||
else:
|
else:
|
||||||
log(
|
log(
|
||||||
'submissions',
|
"submissions",
|
||||||
"[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]",
|
"[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]",
|
||||||
submission=request_data['submission'].encode('utf-8'),
|
submission=request_data["submission"].encode("utf-8"),
|
||||||
kpm=kpm
|
kpm=kpm,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
'success': True,
|
"success": True,
|
||||||
'data': {
|
"data": {
|
||||||
'status': "already_solved",
|
"status": "already_solved",
|
||||||
'message': 'You already solved this'
|
"message": "You already solved this",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>/solves')
|
@challenges_namespace.route("/<challenge_id>/solves")
|
||||||
@challenges_namespace.param('id', 'A Challenge ID')
|
@challenges_namespace.param("id", "A Challenge ID")
|
||||||
class ChallengeSolves(Resource):
|
class ChallengeSolves(Resource):
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
|
@ -515,68 +507,67 @@ class ChallengeSolves(Resource):
|
||||||
|
|
||||||
# TODO: Need a generic challenge visibility call.
|
# TODO: Need a generic challenge visibility call.
|
||||||
# However, it should be stated that a solve on a gated challenge is not considered private.
|
# However, it should be stated that a solve on a gated challenge is not considered private.
|
||||||
if challenge.state == 'hidden' and is_admin() is False:
|
if challenge.state == "hidden" and is_admin() is False:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
solves = Solves.query.join(Model, Solves.account_id == Model.id)\
|
solves = (
|
||||||
.filter(Solves.challenge_id == challenge_id, Model.banned == False, Model.hidden == False)\
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
|
.filter(
|
||||||
|
Solves.challenge_id == challenge_id,
|
||||||
|
Model.banned == False,
|
||||||
|
Model.hidden == False,
|
||||||
|
)
|
||||||
.order_by(Solves.date.asc())
|
.order_by(Solves.date.asc())
|
||||||
|
)
|
||||||
|
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze:
|
if freeze:
|
||||||
preview = request.args.get('preview')
|
preview = request.args.get("preview")
|
||||||
if (is_admin() is False) or (is_admin() is True and preview):
|
if (is_admin() is False) or (is_admin() is True and preview):
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
solves = solves.filter(Solves.date < dt)
|
solves = solves.filter(Solves.date < dt)
|
||||||
|
|
||||||
endpoint = None
|
endpoint = None
|
||||||
if get_config('user_mode') == TEAMS_MODE:
|
if get_config("user_mode") == TEAMS_MODE:
|
||||||
endpoint = 'teams.public'
|
endpoint = "teams.public"
|
||||||
arg = 'team_id'
|
arg = "team_id"
|
||||||
elif get_config('user_mode') == USERS_MODE:
|
elif get_config("user_mode") == USERS_MODE:
|
||||||
endpoint = 'users.public'
|
endpoint = "users.public"
|
||||||
arg = 'user_id'
|
arg = "user_id"
|
||||||
|
|
||||||
for solve in solves:
|
for solve in solves:
|
||||||
response.append({
|
response.append(
|
||||||
'account_id': solve.account_id,
|
{
|
||||||
'name': solve.account.name,
|
"account_id": solve.account_id,
|
||||||
'date': isoformat(solve.date),
|
"name": solve.account.name,
|
||||||
'account_url': url_for(endpoint, **{arg: solve.account_id})
|
"date": isoformat(solve.date),
|
||||||
})
|
"account_url": url_for(endpoint, **{arg: solve.account_id}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>/files')
|
@challenges_namespace.route("/<challenge_id>/files")
|
||||||
@challenges_namespace.param('id', 'A Challenge ID')
|
@challenges_namespace.param("id", "A Challenge ID")
|
||||||
class ChallengeFiles(Resource):
|
class ChallengeFiles(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
response = []
|
response = []
|
||||||
|
|
||||||
challenge_files = ChallengeFilesModel.query.filter_by(
|
challenge_files = ChallengeFilesModel.query.filter_by(
|
||||||
challenge_id=challenge_id).all()
|
challenge_id=challenge_id
|
||||||
|
).all()
|
||||||
|
|
||||||
for f in challenge_files:
|
for f in challenge_files:
|
||||||
response.append({
|
response.append({"id": f.id, "type": f.type, "location": f.location})
|
||||||
'id': f.id,
|
return {"success": True, "data": response}
|
||||||
'type': f.type,
|
|
||||||
'location': f.location
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>/tags')
|
@challenges_namespace.route("/<challenge_id>/tags")
|
||||||
@challenges_namespace.param('id', 'A Challenge ID')
|
@challenges_namespace.param("id", "A Challenge ID")
|
||||||
class ChallengeTags(Resource):
|
class ChallengeTags(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
|
@ -585,19 +576,14 @@ class ChallengeTags(Resource):
|
||||||
tags = Tags.query.filter_by(challenge_id=challenge_id).all()
|
tags = Tags.query.filter_by(challenge_id=challenge_id).all()
|
||||||
|
|
||||||
for t in tags:
|
for t in tags:
|
||||||
response.append({
|
response.append(
|
||||||
'id': t.id,
|
{"id": t.id, "challenge_id": t.challenge_id, "value": t.value}
|
||||||
'challenge_id': t.challenge_id,
|
)
|
||||||
'value': t.value
|
return {"success": True, "data": response}
|
||||||
})
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>/hints')
|
@challenges_namespace.route("/<challenge_id>/hints")
|
||||||
@challenges_namespace.param('id', 'A Challenge ID')
|
@challenges_namespace.param("id", "A Challenge ID")
|
||||||
class ChallengeHints(Resource):
|
class ChallengeHints(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
|
@ -606,19 +592,13 @@ class ChallengeHints(Resource):
|
||||||
response = schema.dump(hints)
|
response = schema.dump(hints)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@challenges_namespace.route('/<challenge_id>/flags')
|
@challenges_namespace.route("/<challenge_id>/flags")
|
||||||
@challenges_namespace.param('id', 'A Challenge ID')
|
@challenges_namespace.param("id", "A Challenge ID")
|
||||||
class ChallengeFlags(Resource):
|
class ChallengeFlags(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, challenge_id):
|
def get(self, challenge_id):
|
||||||
|
@ -627,12 +607,6 @@ class ChallengeFlags(Resource):
|
||||||
response = schema.dump(flags)
|
response = schema.dump(flags)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,16 +2,14 @@ from flask import request
|
||||||
from flask_restplus import Namespace, Resource
|
from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Configs
|
from CTFd.models import db, Configs
|
||||||
from CTFd.schemas.config import ConfigSchema
|
from CTFd.schemas.config import ConfigSchema
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
from CTFd.utils import get_config, set_config
|
from CTFd.utils import get_config, set_config
|
||||||
from CTFd.cache import clear_config, clear_standings
|
from CTFd.cache import clear_config, clear_standings
|
||||||
|
|
||||||
configs_namespace = Namespace('configs', description="Endpoint to retrieve Configs")
|
configs_namespace = Namespace("configs", description="Endpoint to retrieve Configs")
|
||||||
|
|
||||||
|
|
||||||
@configs_namespace.route('')
|
@configs_namespace.route("")
|
||||||
class ConfigList(Resource):
|
class ConfigList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -19,15 +17,9 @@ class ConfigList(Resource):
|
||||||
schema = ConfigSchema(many=True)
|
schema = ConfigSchema(many=True)
|
||||||
response = schema.dump(configs)
|
response = schema.dump(configs)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors,
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
|
@ -36,10 +28,7 @@ class ConfigList(Resource):
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -50,10 +39,7 @@ class ConfigList(Resource):
|
||||||
clear_config()
|
clear_config()
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self):
|
def patch(self):
|
||||||
|
@ -65,20 +51,15 @@ class ConfigList(Resource):
|
||||||
clear_config()
|
clear_config()
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@configs_namespace.route('/<config_key>')
|
@configs_namespace.route("/<config_key>")
|
||||||
class Config(Resource):
|
class Config(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, config_key):
|
def get(self, config_key):
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": get_config(config_key)}
|
||||||
'success': True,
|
|
||||||
'data': get_config(config_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, config_key):
|
def patch(self, config_key):
|
||||||
|
@ -89,7 +70,7 @@ class Config(Resource):
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
else:
|
else:
|
||||||
schema = ConfigSchema()
|
schema = ConfigSchema()
|
||||||
data['key'] = config_key
|
data["key"] = config_key
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
|
|
||||||
|
@ -104,10 +85,7 @@ class Config(Resource):
|
||||||
clear_config()
|
clear_config()
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, config_key):
|
def delete(self, config_key):
|
||||||
|
@ -120,6 +98,4 @@ class Config(Resource):
|
||||||
clear_config()
|
clear_config()
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,36 +3,28 @@ from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Files
|
from CTFd.models import db, Files
|
||||||
from CTFd.schemas.files import FileSchema
|
from CTFd.schemas.files import FileSchema
|
||||||
from CTFd.utils import uploads
|
from CTFd.utils import uploads
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
|
|
||||||
files_namespace = Namespace('files', description="Endpoint to retrieve Files")
|
files_namespace = Namespace("files", description="Endpoint to retrieve Files")
|
||||||
|
|
||||||
|
|
||||||
@files_namespace.route('')
|
@files_namespace.route("")
|
||||||
class FilesList(Resource):
|
class FilesList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
file_type = request.args.get('type')
|
file_type = request.args.get("type")
|
||||||
files = Files.query.filter_by(type=file_type).all()
|
files = Files.query.filter_by(type=file_type).all()
|
||||||
schema = FileSchema(many=True)
|
schema = FileSchema(many=True)
|
||||||
response = schema.dump(files)
|
response = schema.dump(files)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
files = request.files.getlist('file')
|
files = request.files.getlist("file")
|
||||||
# challenge_id
|
# challenge_id
|
||||||
# page_id
|
# page_id
|
||||||
|
|
||||||
|
@ -46,18 +38,12 @@ class FilesList(Resource):
|
||||||
response = schema.dump(objs)
|
response = schema.dump(objs)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errorss}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errorss
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@files_namespace.route('/<file_id>')
|
@files_namespace.route("/<file_id>")
|
||||||
class FilesDetail(Resource):
|
class FilesDetail(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, file_id):
|
def get(self, file_id):
|
||||||
|
@ -66,15 +52,9 @@ class FilesDetail(Resource):
|
||||||
response = schema.dump(f)
|
response = schema.dump(f)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, file_id):
|
def delete(self, file_id):
|
||||||
|
@ -84,6 +64,4 @@ class FilesDetail(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,14 +3,12 @@ from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Flags
|
from CTFd.models import db, Flags
|
||||||
from CTFd.schemas.flags import FlagSchema
|
from CTFd.schemas.flags import FlagSchema
|
||||||
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
|
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
|
|
||||||
flags_namespace = Namespace('flags', description="Endpoint to retrieve Flags")
|
flags_namespace = Namespace("flags", description="Endpoint to retrieve Flags")
|
||||||
|
|
||||||
|
|
||||||
@flags_namespace.route('')
|
@flags_namespace.route("")
|
||||||
class FlagList(Resource):
|
class FlagList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -18,15 +16,9 @@ class FlagList(Resource):
|
||||||
schema = FlagSchema(many=True)
|
schema = FlagSchema(many=True)
|
||||||
response = schema.dump(flags)
|
response = schema.dump(flags)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
|
@ -35,10 +27,7 @@ class FlagList(Resource):
|
||||||
response = schema.load(req, session=db.session)
|
response = schema.load(req, session=db.session)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -46,42 +35,30 @@ class FlagList(Resource):
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@flags_namespace.route('/types', defaults={'type_name': None})
|
@flags_namespace.route("/types", defaults={"type_name": None})
|
||||||
@flags_namespace.route('/types/<type_name>')
|
@flags_namespace.route("/types/<type_name>")
|
||||||
class FlagTypes(Resource):
|
class FlagTypes(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, type_name):
|
def get(self, type_name):
|
||||||
if type_name:
|
if type_name:
|
||||||
flag_class = get_flag_class(type_name)
|
flag_class = get_flag_class(type_name)
|
||||||
response = {
|
response = {"name": flag_class.name, "templates": flag_class.templates}
|
||||||
'name': flag_class.name,
|
return {"success": True, "data": response}
|
||||||
'templates': flag_class.templates
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
response = {}
|
response = {}
|
||||||
for class_id in FLAG_CLASSES:
|
for class_id in FLAG_CLASSES:
|
||||||
flag_class = FLAG_CLASSES.get(class_id)
|
flag_class = FLAG_CLASSES.get(class_id)
|
||||||
response[class_id] = {
|
response[class_id] = {
|
||||||
'name': flag_class.name,
|
"name": flag_class.name,
|
||||||
'templates': flag_class.templates,
|
"templates": flag_class.templates,
|
||||||
}
|
}
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@flags_namespace.route('/<flag_id>')
|
@flags_namespace.route("/<flag_id>")
|
||||||
class Flag(Resource):
|
class Flag(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, flag_id):
|
def get(self, flag_id):
|
||||||
|
@ -90,17 +67,11 @@ class Flag(Resource):
|
||||||
response = schema.dump(flag)
|
response = schema.dump(flag)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
response.data['templates'] = get_flag_class(flag.type).templates
|
response.data["templates"] = get_flag_class(flag.type).templates
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, flag_id):
|
def delete(self, flag_id):
|
||||||
|
@ -110,9 +81,7 @@ class Flag(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, flag_id):
|
def patch(self, flag_id):
|
||||||
|
@ -123,17 +92,11 @@ class Flag(Resource):
|
||||||
response = schema.load(req, session=db.session, instance=flag, partial=True)
|
response = schema.load(req, session=db.session, instance=flag, partial=True)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,16 +3,12 @@ from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Hints, HintUnlocks
|
from CTFd.models import db, Hints, HintUnlocks
|
||||||
from CTFd.utils.user import get_current_user, is_admin
|
from CTFd.utils.user import get_current_user, is_admin
|
||||||
from CTFd.schemas.hints import HintSchema
|
from CTFd.schemas.hints import HintSchema
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import during_ctf_time_only, admins_only, authed_only
|
||||||
during_ctf_time_only,
|
|
||||||
admins_only,
|
|
||||||
authed_only
|
|
||||||
)
|
|
||||||
|
|
||||||
hints_namespace = Namespace('hints', description="Endpoint to retrieve Hints")
|
hints_namespace = Namespace("hints", description="Endpoint to retrieve Hints")
|
||||||
|
|
||||||
|
|
||||||
@hints_namespace.route('')
|
@hints_namespace.route("")
|
||||||
class HintList(Resource):
|
class HintList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -20,40 +16,28 @@ class HintList(Resource):
|
||||||
response = HintSchema(many=True).dump(hints)
|
response = HintSchema(many=True).dump(hints)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = HintSchema('admin')
|
schema = HintSchema("admin")
|
||||||
response = schema.load(req, session=db.session)
|
response = schema.load(req, session=db.session)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@hints_namespace.route('/<hint_id>')
|
@hints_namespace.route("/<hint_id>")
|
||||||
class Hint(Resource):
|
class Hint(Resource):
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@authed_only
|
@authed_only
|
||||||
|
@ -61,32 +45,25 @@ class Hint(Resource):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||||
|
|
||||||
view = 'unlocked'
|
view = "unlocked"
|
||||||
if hint.cost:
|
if hint.cost:
|
||||||
view = 'locked'
|
view = "locked"
|
||||||
unlocked = HintUnlocks.query.filter_by(
|
unlocked = HintUnlocks.query.filter_by(
|
||||||
account_id=user.account_id,
|
account_id=user.account_id, target=hint.id
|
||||||
target=hint.id
|
|
||||||
).first()
|
).first()
|
||||||
if unlocked:
|
if unlocked:
|
||||||
view = 'unlocked'
|
view = "unlocked"
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
if request.args.get('preview', False):
|
if request.args.get("preview", False):
|
||||||
view = 'admin'
|
view = "admin"
|
||||||
|
|
||||||
response = HintSchema(view=view).dump(hint)
|
response = HintSchema(view=view).dump(hint)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, hint_id):
|
def patch(self, hint_id):
|
||||||
|
@ -97,20 +74,14 @@ class Hint(Resource):
|
||||||
response = schema.load(req, instance=hint, partial=True, session=db.session)
|
response = schema.load(req, instance=hint, partial=True, session=db.session)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, hint_id):
|
def delete(self, hint_id):
|
||||||
|
@ -119,6 +90,4 @@ class Hint(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,28 +3,22 @@ from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Notifications
|
from CTFd.models import db, Notifications
|
||||||
from CTFd.schemas.notifications import NotificationSchema
|
from CTFd.schemas.notifications import NotificationSchema
|
||||||
|
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
|
notifications_namespace = Namespace(
|
||||||
|
"notifications", description="Endpoint to retrieve Notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
notifications_namespace = Namespace('notifications', description="Endpoint to retrieve Notifications")
|
|
||||||
|
|
||||||
|
@notifications_namespace.route("")
|
||||||
@notifications_namespace.route('')
|
|
||||||
class NotificantionList(Resource):
|
class NotificantionList(Resource):
|
||||||
def get(self):
|
def get(self):
|
||||||
notifications = Notifications.query.all()
|
notifications = Notifications.query.all()
|
||||||
schema = NotificationSchema(many=True)
|
schema = NotificationSchema(many=True)
|
||||||
result = schema.dump(notifications)
|
result = schema.dump(notifications)
|
||||||
if result.errors:
|
if result.errors:
|
||||||
return {
|
return {"success": False, "errors": result.errors}, 400
|
||||||
'success': False,
|
return {"success": True, "data": result.data}
|
||||||
'errors': result.errors
|
|
||||||
}, 400
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': result.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
|
@ -34,42 +28,28 @@ class NotificantionList(Resource):
|
||||||
result = schema.load(req)
|
result = schema.load(req)
|
||||||
|
|
||||||
if result.errors:
|
if result.errors:
|
||||||
return {
|
return {"success": False, "errors": result.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': result.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(result.data)
|
db.session.add(result.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = schema.dump(result.data)
|
response = schema.dump(result.data)
|
||||||
current_app.events_manager.publish(
|
current_app.events_manager.publish(data=response.data, type="notification")
|
||||||
data=response.data, type='notification'
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@notifications_namespace.route('/<notification_id>')
|
@notifications_namespace.route("/<notification_id>")
|
||||||
@notifications_namespace.param('notification_id', 'A Notification ID')
|
@notifications_namespace.param("notification_id", "A Notification ID")
|
||||||
class Notification(Resource):
|
class Notification(Resource):
|
||||||
def get(self, notification_id):
|
def get(self, notification_id):
|
||||||
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
notif = Notifications.query.filter_by(id=notification_id).first_or_404()
|
||||||
schema = NotificationSchema()
|
schema = NotificationSchema()
|
||||||
response = schema.dump(notif)
|
response = schema.dump(notif)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, notification_id):
|
def delete(self, notification_id):
|
||||||
|
@ -78,6 +58,4 @@ class Notification(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,30 +4,22 @@ from CTFd.models import db, Pages
|
||||||
from CTFd.schemas.pages import PageSchema
|
from CTFd.schemas.pages import PageSchema
|
||||||
from CTFd.cache import clear_pages
|
from CTFd.cache import clear_pages
|
||||||
|
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
|
|
||||||
pages_namespace = Namespace('pages', description="Endpoint to retrieve Pages")
|
pages_namespace = Namespace("pages", description="Endpoint to retrieve Pages")
|
||||||
|
|
||||||
|
|
||||||
@pages_namespace.route('')
|
@pages_namespace.route("")
|
||||||
class PageList(Resource):
|
class PageList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
pages = Pages.query.all()
|
pages = Pages.query.all()
|
||||||
schema = PageSchema(exclude=['content'], many=True)
|
schema = PageSchema(exclude=["content"], many=True)
|
||||||
response = schema.dump(pages)
|
response = schema.dump(pages)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
|
@ -36,10 +28,7 @@ class PageList(Resource):
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -49,13 +38,10 @@ class PageList(Resource):
|
||||||
|
|
||||||
clear_pages()
|
clear_pages()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pages_namespace.route('/<page_id>')
|
@pages_namespace.route("/<page_id>")
|
||||||
class PageDetail(Resource):
|
class PageDetail(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, page_id):
|
def get(self, page_id):
|
||||||
|
@ -64,15 +50,9 @@ class PageDetail(Resource):
|
||||||
response = schema.dump(page)
|
response = schema.dump(page)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, page_id):
|
def patch(self, page_id):
|
||||||
|
@ -83,10 +63,7 @@ class PageDetail(Resource):
|
||||||
response = schema.load(req, instance=page, partial=True)
|
response = schema.load(req, instance=page, partial=True)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -95,10 +72,7 @@ class PageDetail(Resource):
|
||||||
|
|
||||||
clear_pages()
|
clear_pages()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, page_id):
|
def delete(self, page_id):
|
||||||
|
@ -109,6 +83,4 @@ class PageDetail(Resource):
|
||||||
|
|
||||||
clear_pages()
|
clear_pages()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,12 +6,17 @@ from CTFd.utils.scores import get_standings
|
||||||
from CTFd.utils import get_config
|
from CTFd.utils import get_config
|
||||||
from CTFd.utils.modes import TEAMS_MODE
|
from CTFd.utils.modes import TEAMS_MODE
|
||||||
from CTFd.utils.dates import unix_time_to_utc, isoformat
|
from CTFd.utils.dates import unix_time_to_utc, isoformat
|
||||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
from CTFd.utils.decorators.visibility import (
|
||||||
|
check_account_visibility,
|
||||||
|
check_score_visibility,
|
||||||
|
)
|
||||||
|
|
||||||
scoreboard_namespace = Namespace('scoreboard', description="Endpoint to retrieve scores")
|
scoreboard_namespace = Namespace(
|
||||||
|
"scoreboard", description="Endpoint to retrieve scores"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@scoreboard_namespace.route('')
|
@scoreboard_namespace.route("")
|
||||||
class ScoreboardList(Resource):
|
class ScoreboardList(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
|
@ -19,7 +24,7 @@ class ScoreboardList(Resource):
|
||||||
def get(self):
|
def get(self):
|
||||||
standings = get_standings()
|
standings = get_standings()
|
||||||
response = []
|
response = []
|
||||||
mode = get_config('user_mode')
|
mode = get_config("user_mode")
|
||||||
|
|
||||||
if mode == TEAMS_MODE:
|
if mode == TEAMS_MODE:
|
||||||
team_ids = []
|
team_ids = []
|
||||||
|
@ -30,36 +35,33 @@ class ScoreboardList(Resource):
|
||||||
|
|
||||||
for i, x in enumerate(standings):
|
for i, x in enumerate(standings):
|
||||||
entry = {
|
entry = {
|
||||||
'pos': i + 1,
|
"pos": i + 1,
|
||||||
'account_id': x.account_id,
|
"account_id": x.account_id,
|
||||||
'oauth_id': x.oauth_id,
|
"oauth_id": x.oauth_id,
|
||||||
'name': x.name,
|
"name": x.name,
|
||||||
'score': int(x.score)
|
"score": int(x.score),
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode == TEAMS_MODE:
|
if mode == TEAMS_MODE:
|
||||||
members = []
|
members = []
|
||||||
for member in teams[i].members:
|
for member in teams[i].members:
|
||||||
members.append({
|
members.append(
|
||||||
'id': member.id,
|
{
|
||||||
'oauth_id': member.oauth_id,
|
"id": member.id,
|
||||||
'name': member.name,
|
"oauth_id": member.oauth_id,
|
||||||
'score': int(member.score)
|
"name": member.name,
|
||||||
})
|
"score": int(member.score),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
entry['members'] = members
|
entry["members"] = members
|
||||||
|
|
||||||
response.append(
|
response.append(entry)
|
||||||
entry
|
return {"success": True, "data": response}
|
||||||
)
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@scoreboard_namespace.route('/top/<count>')
|
@scoreboard_namespace.route("/top/<count>")
|
||||||
@scoreboard_namespace.param('count', 'How many top teams to return')
|
@scoreboard_namespace.param("count", "How many top teams to return")
|
||||||
class ScoreboardDetail(Resource):
|
class ScoreboardDetail(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
|
@ -74,7 +76,7 @@ class ScoreboardDetail(Resource):
|
||||||
solves = Solves.query.filter(Solves.account_id.in_(team_ids))
|
solves = Solves.query.filter(Solves.account_id.in_(team_ids))
|
||||||
awards = Awards.query.filter(Awards.account_id.in_(team_ids))
|
awards = Awards.query.filter(Awards.account_id.in_(team_ids))
|
||||||
|
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
|
|
||||||
if freeze:
|
if freeze:
|
||||||
solves = solves.filter(Solves.date < unix_time_to_utc(freeze))
|
solves = solves.filter(Solves.date < unix_time_to_utc(freeze))
|
||||||
|
@ -85,33 +87,36 @@ class ScoreboardDetail(Resource):
|
||||||
|
|
||||||
for i, team in enumerate(team_ids):
|
for i, team in enumerate(team_ids):
|
||||||
response[i + 1] = {
|
response[i + 1] = {
|
||||||
'id': standings[i].account_id,
|
"id": standings[i].account_id,
|
||||||
'name': standings[i].name,
|
"name": standings[i].name,
|
||||||
'solves': []
|
"solves": [],
|
||||||
}
|
}
|
||||||
for solve in solves:
|
for solve in solves:
|
||||||
if solve.account_id == team:
|
if solve.account_id == team:
|
||||||
response[i + 1]['solves'].append({
|
response[i + 1]["solves"].append(
|
||||||
'challenge_id': solve.challenge_id,
|
{
|
||||||
'account_id': solve.account_id,
|
"challenge_id": solve.challenge_id,
|
||||||
'team_id': solve.team_id,
|
"account_id": solve.account_id,
|
||||||
'user_id': solve.user_id,
|
"team_id": solve.team_id,
|
||||||
'value': solve.challenge.value,
|
"user_id": solve.user_id,
|
||||||
'date': isoformat(solve.date)
|
"value": solve.challenge.value,
|
||||||
})
|
"date": isoformat(solve.date),
|
||||||
|
}
|
||||||
|
)
|
||||||
for award in awards:
|
for award in awards:
|
||||||
if award.account_id == team:
|
if award.account_id == team:
|
||||||
response[i + 1]['solves'].append({
|
response[i + 1]["solves"].append(
|
||||||
'challenge_id': None,
|
{
|
||||||
'account_id': award.account_id,
|
"challenge_id": None,
|
||||||
'team_id': award.team_id,
|
"account_id": award.account_id,
|
||||||
'user_id': award.user_id,
|
"team_id": award.team_id,
|
||||||
'value': award.value,
|
"user_id": award.user_id,
|
||||||
'date': isoformat(award.date)
|
"value": award.value,
|
||||||
})
|
"date": isoformat(award.date),
|
||||||
response[i + 1]['solves'] = sorted(response[i + 1]['solves'], key=lambda k: k['date'])
|
}
|
||||||
|
)
|
||||||
|
response[i + 1]["solves"] = sorted(
|
||||||
|
response[i + 1]["solves"], key=lambda k: k["date"]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from flask_restplus import Namespace
|
from flask_restplus import Namespace
|
||||||
|
|
||||||
statistics_namespace = Namespace('statistics', description="Endpoint to retrieve Statistics")
|
statistics_namespace = Namespace(
|
||||||
|
"statistics", description="Endpoint to retrieve Statistics"
|
||||||
|
)
|
||||||
|
|
||||||
from CTFd.api.v1.statistics import challenges # noqa: F401
|
from CTFd.api.v1.statistics import challenges # noqa: F401
|
||||||
from CTFd.api.v1.statistics import teams # noqa: F401
|
from CTFd.api.v1.statistics import teams # noqa: F401
|
||||||
|
|
|
@ -1,123 +1,119 @@
|
||||||
from flask_restplus import Resource
|
from flask_restplus import Resource
|
||||||
from CTFd.models import db, Challenges, Solves
|
from CTFd.models import db, Challenges, Solves
|
||||||
from CTFd.utils.modes import get_model
|
from CTFd.utils.modes import get_model
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only,
|
|
||||||
)
|
|
||||||
from CTFd.api.v1.statistics import statistics_namespace
|
from CTFd.api.v1.statistics import statistics_namespace
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql import or_
|
from sqlalchemy.sql import or_
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/challenges/<column>')
|
@statistics_namespace.route("/challenges/<column>")
|
||||||
class ChallengePropertyCounts(Resource):
|
class ChallengePropertyCounts(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, column):
|
def get(self, column):
|
||||||
if column in Challenges.__table__.columns.keys():
|
if column in Challenges.__table__.columns.keys():
|
||||||
prop = getattr(Challenges, column)
|
prop = getattr(Challenges, column)
|
||||||
data = Challenges.query\
|
data = (
|
||||||
.with_entities(prop, func.count(prop))\
|
Challenges.query.with_entities(prop, func.count(prop))
|
||||||
.group_by(prop)\
|
.group_by(prop)
|
||||||
.all()
|
.all()
|
||||||
return {
|
)
|
||||||
'success': True,
|
return {"success": True, "data": dict(data)}
|
||||||
'data': dict(data)
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
response = {
|
response = {"message": "That could not be found"}, 404
|
||||||
'message': 'That could not be found'
|
|
||||||
}, 404
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/challenges/solves')
|
@statistics_namespace.route("/challenges/solves")
|
||||||
class ChallengeSolveStatistics(Resource):
|
class ChallengeSolveStatistics(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
chals = Challenges.query \
|
chals = (
|
||||||
.filter(or_(Challenges.state != 'hidden', Challenges.state != 'locked')) \
|
Challenges.query.filter(
|
||||||
.order_by(Challenges.value) \
|
or_(Challenges.state != "hidden", Challenges.state != "locked")
|
||||||
|
)
|
||||||
|
.order_by(Challenges.value)
|
||||||
.all()
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
solves_sub = db.session.query(
|
solves_sub = (
|
||||||
Solves.challenge_id,
|
db.session.query(
|
||||||
db.func.count(Solves.challenge_id).label('solves')
|
Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves")
|
||||||
) \
|
)
|
||||||
.join(Model, Solves.account_id == Model.id) \
|
.join(Model, Solves.account_id == Model.id)
|
||||||
.filter(Model.banned == False, Model.hidden == False) \
|
.filter(Model.banned == False, Model.hidden == False)
|
||||||
.group_by(Solves.challenge_id).subquery()
|
.group_by(Solves.challenge_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
solves = db.session.query(
|
solves = (
|
||||||
solves_sub.columns.challenge_id,
|
db.session.query(
|
||||||
solves_sub.columns.solves,
|
solves_sub.columns.challenge_id,
|
||||||
Challenges.name
|
solves_sub.columns.solves,
|
||||||
) \
|
Challenges.name,
|
||||||
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id).all()
|
)
|
||||||
|
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
response = []
|
response = []
|
||||||
has_solves = []
|
has_solves = []
|
||||||
|
|
||||||
for challenge_id, count, name in solves:
|
for challenge_id, count, name in solves:
|
||||||
challenge = {
|
challenge = {"id": challenge_id, "name": name, "solves": count}
|
||||||
'id': challenge_id,
|
|
||||||
'name': name,
|
|
||||||
'solves': count,
|
|
||||||
}
|
|
||||||
response.append(challenge)
|
response.append(challenge)
|
||||||
has_solves.append(challenge_id)
|
has_solves.append(challenge_id)
|
||||||
for c in chals:
|
for c in chals:
|
||||||
if c.id not in has_solves:
|
if c.id not in has_solves:
|
||||||
challenge = {
|
challenge = {"id": c.id, "name": c.name, "solves": 0}
|
||||||
'id': c.id,
|
|
||||||
'name': c.name,
|
|
||||||
'solves': 0,
|
|
||||||
}
|
|
||||||
response.append(challenge)
|
response.append(challenge)
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/challenges/solves/percentages')
|
@statistics_namespace.route("/challenges/solves/percentages")
|
||||||
class ChallengeSolvePercentages(Resource):
|
class ChallengeSolvePercentages(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
challenges = Challenges.query\
|
challenges = (
|
||||||
.add_columns('id', 'name', 'state', 'max_attempts')\
|
Challenges.query.add_columns("id", "name", "state", "max_attempts")
|
||||||
.order_by(Challenges.value).all()
|
.order_by(Challenges.value)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
teams_with_points = db.session.query(Solves.account_id) \
|
teams_with_points = (
|
||||||
.join(Model) \
|
db.session.query(Solves.account_id)
|
||||||
.filter(Model.banned == False, Model.hidden == False) \
|
.join(Model)
|
||||||
.group_by(Solves.account_id) \
|
.filter(Model.banned == False, Model.hidden == False)
|
||||||
|
.group_by(Solves.account_id)
|
||||||
.count()
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
percentage_data = []
|
percentage_data = []
|
||||||
for challenge in challenges:
|
for challenge in challenges:
|
||||||
solve_count = Solves.query.join(Model, Solves.account_id == Model.id) \
|
solve_count = (
|
||||||
.filter(Solves.challenge_id == challenge.id, Model.banned == False, Model.hidden == False) \
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
|
.filter(
|
||||||
|
Solves.challenge_id == challenge.id,
|
||||||
|
Model.banned == False,
|
||||||
|
Model.hidden == False,
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
if teams_with_points > 0:
|
if teams_with_points > 0:
|
||||||
percentage = (float(solve_count) / float(teams_with_points))
|
percentage = float(solve_count) / float(teams_with_points)
|
||||||
else:
|
else:
|
||||||
percentage = 0.0
|
percentage = 0.0
|
||||||
|
|
||||||
percentage_data.append({
|
percentage_data.append(
|
||||||
'id': challenge.id,
|
{"id": challenge.id, "name": challenge.name, "percentage": percentage}
|
||||||
'name': challenge.name,
|
)
|
||||||
'percentage': percentage,
|
|
||||||
})
|
|
||||||
|
|
||||||
response = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
|
response = sorted(percentage_data, key=lambda x: x["percentage"], reverse=True)
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,29 +1,22 @@
|
||||||
from flask_restplus import Resource
|
from flask_restplus import Resource
|
||||||
from CTFd.models import Submissions
|
from CTFd.models import Submissions
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only,
|
|
||||||
)
|
|
||||||
from CTFd.api.v1.statistics import statistics_namespace
|
from CTFd.api.v1.statistics import statistics_namespace
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/submissions/<column>')
|
@statistics_namespace.route("/submissions/<column>")
|
||||||
class SubmissionPropertyCounts(Resource):
|
class SubmissionPropertyCounts(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, column):
|
def get(self, column):
|
||||||
if column in Submissions.__table__.columns.keys():
|
if column in Submissions.__table__.columns.keys():
|
||||||
prop = getattr(Submissions, column)
|
prop = getattr(Submissions, column)
|
||||||
data = Submissions.query \
|
data = (
|
||||||
.with_entities(prop, func.count(prop)) \
|
Submissions.query.with_entities(prop, func.count(prop))
|
||||||
.group_by(prop) \
|
.group_by(prop)
|
||||||
.all()
|
.all()
|
||||||
return {
|
)
|
||||||
'success': True,
|
return {"success": True, "data": dict(data)}
|
||||||
'data': dict(data)
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
response = {
|
response = {"success": False, "errors": "That could not be found"}, 404
|
||||||
'success': False,
|
|
||||||
'errors': 'That could not be found'
|
|
||||||
}, 404
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
from flask_restplus import Resource
|
from flask_restplus import Resource
|
||||||
from CTFd.models import Teams
|
from CTFd.models import Teams
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only,
|
|
||||||
)
|
|
||||||
from CTFd.api.v1.statistics import statistics_namespace
|
from CTFd.api.v1.statistics import statistics_namespace
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/teams')
|
@statistics_namespace.route("/teams")
|
||||||
class TeamStatistics(Resource):
|
class TeamStatistics(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
registered = Teams.query.count()
|
registered = Teams.query.count()
|
||||||
data = {
|
data = {"registered": registered}
|
||||||
'registered': registered,
|
return {"success": True, "data": data}
|
||||||
}
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': data
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,37 +5,24 @@ from CTFd.utils.decorators import admins_only
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/users')
|
@statistics_namespace.route("/users")
|
||||||
class UserStatistics(Resource):
|
class UserStatistics(Resource):
|
||||||
def get(self):
|
def get(self):
|
||||||
registered = Users.query.count()
|
registered = Users.query.count()
|
||||||
confirmed = Users.query.filter_by(verified=True).count()
|
confirmed = Users.query.filter_by(verified=True).count()
|
||||||
data = {
|
data = {"registered": registered, "confirmed": confirmed}
|
||||||
'registered': registered,
|
return {"success": True, "data": data}
|
||||||
'confirmed': confirmed
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@statistics_namespace.route('/users/<column>')
|
@statistics_namespace.route("/users/<column>")
|
||||||
class UserPropertyCounts(Resource):
|
class UserPropertyCounts(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, column):
|
def get(self, column):
|
||||||
if column in Users.__table__.columns.keys():
|
if column in Users.__table__.columns.keys():
|
||||||
prop = getattr(Users, column)
|
prop = getattr(Users, column)
|
||||||
data = Users.query \
|
data = (
|
||||||
.with_entities(prop, func.count(prop)) \
|
Users.query.with_entities(prop, func.count(prop)).group_by(prop).all()
|
||||||
.group_by(prop) \
|
)
|
||||||
.all()
|
return {"success": True, "data": dict(data)}
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': dict(data)
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
return {
|
return {"success": False, "message": "That could not be found"}, 404
|
||||||
'success': False,
|
|
||||||
'message': 'That could not be found'
|
|
||||||
}, 404
|
|
||||||
|
|
|
@ -4,16 +4,15 @@ from flask_restplus import Namespace, Resource
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.models import db, Submissions
|
from CTFd.models import db, Submissions
|
||||||
from CTFd.schemas.submissions import SubmissionSchema
|
from CTFd.schemas.submissions import SubmissionSchema
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only,
|
|
||||||
|
submissions_namespace = Namespace(
|
||||||
|
"submissions", description="Endpoint to retrieve Submission"
|
||||||
)
|
)
|
||||||
|
|
||||||
submissions_namespace = Namespace('submissions', description="Endpoint to retrieve Submission")
|
|
||||||
|
|
||||||
|
@submissions_namespace.route("")
|
||||||
@submissions_namespace.route('')
|
|
||||||
class SubmissionsList(Resource):
|
class SubmissionsList(Resource):
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
args = request.args.to_dict()
|
args = request.args.to_dict()
|
||||||
|
@ -26,27 +25,18 @@ class SubmissionsList(Resource):
|
||||||
response = schema.dump(submissions)
|
response = schema.dump(submissions)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
Model = Submissions.get_child(type=req.get('type'))
|
Model = Submissions.get_child(type=req.get("type"))
|
||||||
schema = SubmissionSchema(instance=Model())
|
schema = SubmissionSchema(instance=Model())
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -57,14 +47,11 @@ class SubmissionsList(Resource):
|
||||||
# Delete standings cache
|
# Delete standings cache
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@submissions_namespace.route('/<submission_id>')
|
@submissions_namespace.route("/<submission_id>")
|
||||||
@submissions_namespace.param('submission_id', 'A Submission ID')
|
@submissions_namespace.param("submission_id", "A Submission ID")
|
||||||
class Submission(Resource):
|
class Submission(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, submission_id):
|
def get(self, submission_id):
|
||||||
|
@ -73,15 +60,9 @@ class Submission(Resource):
|
||||||
response = schema.dump(submission)
|
response = schema.dump(submission)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, submission_id):
|
def delete(self, submission_id):
|
||||||
|
@ -93,6 +74,4 @@ class Submission(Resource):
|
||||||
# Delete standings cache
|
# Delete standings cache
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,14 +2,12 @@ from flask import request
|
||||||
from flask_restplus import Namespace, Resource
|
from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Tags
|
from CTFd.models import db, Tags
|
||||||
from CTFd.schemas.tags import TagSchema
|
from CTFd.schemas.tags import TagSchema
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import admins_only
|
||||||
admins_only
|
|
||||||
)
|
|
||||||
|
|
||||||
tags_namespace = Namespace('tags', description="Endpoint to retrieve Tags")
|
tags_namespace = Namespace("tags", description="Endpoint to retrieve Tags")
|
||||||
|
|
||||||
|
|
||||||
@tags_namespace.route('')
|
@tags_namespace.route("")
|
||||||
class TagList(Resource):
|
class TagList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -19,15 +17,9 @@ class TagList(Resource):
|
||||||
response = schema.dump(tags)
|
response = schema.dump(tags)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
|
@ -36,10 +28,7 @@ class TagList(Resource):
|
||||||
response = schema.load(req, session=db.session)
|
response = schema.load(req, session=db.session)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -47,14 +36,11 @@ class TagList(Resource):
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@tags_namespace.route('/<tag_id>')
|
@tags_namespace.route("/<tag_id>")
|
||||||
@tags_namespace.param('tag_id', 'A Tag ID')
|
@tags_namespace.param("tag_id", "A Tag ID")
|
||||||
class Tag(Resource):
|
class Tag(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, tag_id):
|
def get(self, tag_id):
|
||||||
|
@ -63,15 +49,9 @@ class Tag(Resource):
|
||||||
response = TagSchema().dump(tag)
|
response = TagSchema().dump(tag)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, tag_id):
|
def patch(self, tag_id):
|
||||||
|
@ -81,20 +61,14 @@ class Tag(Resource):
|
||||||
|
|
||||||
response = schema.load(req, session=db.session, instance=tag)
|
response = schema.load(req, session=db.session, instance=tag)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, tag_id):
|
def delete(self, tag_id):
|
||||||
|
@ -103,6 +77,4 @@ class Tag(Resource):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,58 +6,37 @@ from CTFd.schemas.submissions import SubmissionSchema
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.utils.decorators.visibility import check_account_visibility
|
from CTFd.utils.decorators.visibility import check_account_visibility
|
||||||
from CTFd.utils.config.visibility import (
|
from CTFd.utils.config.visibility import accounts_visible, scores_visible
|
||||||
accounts_visible,
|
from CTFd.utils.user import get_current_team, is_admin, authed
|
||||||
scores_visible
|
from CTFd.utils.decorators import authed_only, admins_only
|
||||||
)
|
|
||||||
from CTFd.utils.user import (
|
|
||||||
get_current_team,
|
|
||||||
is_admin,
|
|
||||||
authed
|
|
||||||
)
|
|
||||||
from CTFd.utils.decorators import (
|
|
||||||
authed_only,
|
|
||||||
admins_only,
|
|
||||||
)
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
teams_namespace = Namespace('teams', description="Endpoint to retrieve Teams")
|
teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams")
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('')
|
@teams_namespace.route("")
|
||||||
class TeamList(Resource):
|
class TeamList(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
def get(self):
|
def get(self):
|
||||||
teams = Teams.query.filter_by(hidden=False, banned=False)
|
teams = Teams.query.filter_by(hidden=False, banned=False)
|
||||||
view = copy.deepcopy(TeamSchema.views.get(
|
view = copy.deepcopy(TeamSchema.views.get(session.get("type", "user")))
|
||||||
session.get('type', 'user')
|
view.remove("members")
|
||||||
))
|
|
||||||
view.remove('members')
|
|
||||||
response = TeamSchema(view=view, many=True).dump(teams)
|
response = TeamSchema(view=view, many=True).dump(teams)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
view = TeamSchema.views.get(session.get('type', 'self'))
|
view = TeamSchema.views.get(session.get("type", "self"))
|
||||||
schema = TeamSchema(view=view)
|
schema = TeamSchema(view=view)
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -67,14 +46,11 @@ class TeamList(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/<int:team_id>')
|
@teams_namespace.route("/<int:team_id>")
|
||||||
@teams_namespace.param('team_id', "Team ID")
|
@teams_namespace.param("team_id", "Team ID")
|
||||||
class TeamPublic(Resource):
|
class TeamPublic(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
|
@ -83,35 +59,26 @@ class TeamPublic(Resource):
|
||||||
if (team.banned or team.hidden) and is_admin() is False:
|
if (team.banned or team.hidden) and is_admin() is False:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
view = TeamSchema.views.get(session.get('type', 'user'))
|
view = TeamSchema.views.get(session.get("type", "user"))
|
||||||
schema = TeamSchema(view=view)
|
schema = TeamSchema(view=view)
|
||||||
response = schema.dump(team)
|
response = schema.dump(team)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, team_id):
|
def patch(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
data['id'] = team_id
|
data["id"] = team_id
|
||||||
|
|
||||||
schema = TeamSchema(view='admin', instance=team, partial=True)
|
schema = TeamSchema(view="admin", instance=team, partial=True)
|
||||||
|
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -119,10 +86,7 @@ class TeamPublic(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, team_id):
|
def delete(self, team_id):
|
||||||
|
@ -137,170 +101,131 @@ class TeamPublic(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/me')
|
@teams_namespace.route("/me")
|
||||||
@teams_namespace.param('team_id', "Current Team")
|
@teams_namespace.param("team_id", "Current Team")
|
||||||
class TeamPrivate(Resource):
|
class TeamPrivate(Resource):
|
||||||
@authed_only
|
@authed_only
|
||||||
def get(self):
|
def get(self):
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
response = TeamSchema(view='self').dump(team)
|
response = TeamSchema(view="self").dump(team)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@authed_only
|
@authed_only
|
||||||
def patch(self):
|
def patch(self):
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
if team.captain_id != session['id']:
|
if team.captain_id != session["id"]:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{
|
||||||
'errors': {
|
"success": False,
|
||||||
'': [
|
"errors": {"": ["Only team captains can edit team information"]},
|
||||||
'Only team captains can edit team information'
|
},
|
||||||
]
|
400,
|
||||||
}
|
)
|
||||||
}, 400
|
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
response = TeamSchema(view='self', instance=team, partial=True).load(data)
|
response = TeamSchema(view="self", instance=team, partial=True).load(data)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = TeamSchema('self').dump(response.data)
|
response = TeamSchema("self").dump(response.data)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/<team_id>/members')
|
@teams_namespace.route("/<team_id>/members")
|
||||||
@teams_namespace.param('team_id', "Team ID")
|
@teams_namespace.param("team_id", "Team ID")
|
||||||
class TeamMembers(Resource):
|
class TeamMembers(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
|
|
||||||
view = 'admin' if is_admin() else 'user'
|
view = "admin" if is_admin() else "user"
|
||||||
schema = TeamSchema(view=view)
|
schema = TeamSchema(view=view)
|
||||||
response = schema.dump(team)
|
response = schema.dump(team)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
members = response.data.get('members')
|
members = response.data.get("members")
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": members}
|
||||||
'success': True,
|
|
||||||
'data': members
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self, team_id):
|
def post(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
user_id = data['id']
|
user_id = data["id"]
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
if user.team_id is None:
|
if user.team_id is None:
|
||||||
team.members.append(user)
|
team.members.append(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{
|
||||||
'errors': {
|
"success": False,
|
||||||
'id': [
|
"errors": {"id": ["User has already joined a team"]},
|
||||||
'User has already joined a team'
|
},
|
||||||
]
|
400,
|
||||||
}
|
)
|
||||||
}, 400
|
|
||||||
|
|
||||||
view = 'admin' if is_admin() else 'user'
|
view = "admin" if is_admin() else "user"
|
||||||
schema = TeamSchema(view=view)
|
schema = TeamSchema(view=view)
|
||||||
response = schema.dump(team)
|
response = schema.dump(team)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
members = response.data.get('members')
|
members = response.data.get("members")
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": members}
|
||||||
'success': True,
|
|
||||||
'data': members
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, team_id):
|
def delete(self, team_id):
|
||||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
user_id = data['id']
|
user_id = data["id"]
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
|
|
||||||
if user.team_id == team.id:
|
if user.team_id == team.id:
|
||||||
team.members.remove(user)
|
team.members.remove(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{"success": False, "errors": {"id": ["User is not part of this team"]}},
|
||||||
'errors': {
|
400,
|
||||||
'id': [
|
)
|
||||||
'User is not part of this team'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
view = 'admin' if is_admin() else 'user'
|
view = "admin" if is_admin() else "user"
|
||||||
schema = TeamSchema(view=view)
|
schema = TeamSchema(view=view)
|
||||||
response = schema.dump(team)
|
response = schema.dump(team)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
members = response.data.get('members')
|
members = response.data.get("members")
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": members}
|
||||||
'success': True,
|
|
||||||
'data': members
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/<team_id>/solves')
|
@teams_namespace.route("/<team_id>/solves")
|
||||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
@teams_namespace.param("team_id", "Team ID or 'me'")
|
||||||
class TeamSolves(Resource):
|
class TeamSolves(Resource):
|
||||||
|
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
if team_id == 'me':
|
if team_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
|
@ -314,28 +239,21 @@ class TeamSolves(Resource):
|
||||||
abort(404)
|
abort(404)
|
||||||
solves = team.get_solves(admin=is_admin())
|
solves = team.get_solves(admin=is_admin())
|
||||||
|
|
||||||
view = 'admin' if is_admin() else 'user'
|
view = "admin" if is_admin() else "user"
|
||||||
schema = SubmissionSchema(view=view, many=True)
|
schema = SubmissionSchema(view=view, many=True)
|
||||||
response = schema.dump(solves)
|
response = schema.dump(solves)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/<team_id>/fails')
|
@teams_namespace.route("/<team_id>/fails")
|
||||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
@teams_namespace.param("team_id", "Team ID or 'me'")
|
||||||
class TeamFails(Resource):
|
class TeamFails(Resource):
|
||||||
|
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
if team_id == 'me':
|
if team_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
|
@ -349,16 +267,13 @@ class TeamFails(Resource):
|
||||||
abort(404)
|
abort(404)
|
||||||
fails = team.get_fails(admin=is_admin())
|
fails = team.get_fails(admin=is_admin())
|
||||||
|
|
||||||
view = 'admin' if is_admin() else 'user'
|
view = "admin" if is_admin() else "user"
|
||||||
|
|
||||||
schema = SubmissionSchema(view=view, many=True)
|
schema = SubmissionSchema(view=view, many=True)
|
||||||
response = schema.dump(fails)
|
response = schema.dump(fails)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
data = response.data
|
data = response.data
|
||||||
|
@ -366,21 +281,14 @@ class TeamFails(Resource):
|
||||||
data = []
|
data = []
|
||||||
count = len(response.data)
|
count = len(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": data, "meta": {"count": count}}
|
||||||
'success': True,
|
|
||||||
'data': data,
|
|
||||||
'meta': {
|
|
||||||
'count': count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@teams_namespace.route('/<team_id>/awards')
|
@teams_namespace.route("/<team_id>/awards")
|
||||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
@teams_namespace.param("team_id", "Team ID or 'me'")
|
||||||
class TeamAwards(Resource):
|
class TeamAwards(Resource):
|
||||||
|
|
||||||
def get(self, team_id):
|
def get(self, team_id):
|
||||||
if team_id == 'me':
|
if team_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
team = get_current_team()
|
team = get_current_team()
|
||||||
|
@ -398,12 +306,6 @@ class TeamAwards(Resource):
|
||||||
response = schema.dump(awards)
|
response = schema.dump(awards)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,13 +8,13 @@ from CTFd.utils.decorators import (
|
||||||
during_ctf_time_only,
|
during_ctf_time_only,
|
||||||
require_verified_emails,
|
require_verified_emails,
|
||||||
admins_only,
|
admins_only,
|
||||||
authed_only
|
authed_only,
|
||||||
)
|
)
|
||||||
|
|
||||||
unlocks_namespace = Namespace('unlocks', description="Endpoint to retrieve Unlocks")
|
unlocks_namespace = Namespace("unlocks", description="Endpoint to retrieve Unlocks")
|
||||||
|
|
||||||
|
|
||||||
@unlocks_namespace.route('')
|
@unlocks_namespace.route("")
|
||||||
class UnlockList(Resource):
|
class UnlockList(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
def get(self):
|
def get(self):
|
||||||
|
@ -23,15 +23,9 @@ class UnlockList(Resource):
|
||||||
response = schema.dump(hints)
|
response = schema.dump(hints)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
|
@ -40,39 +34,39 @@ class UnlockList(Resource):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
|
||||||
req['user_id'] = user.id
|
req["user_id"] = user.id
|
||||||
req['team_id'] = user.team_id
|
req["team_id"] = user.team_id
|
||||||
|
|
||||||
Model = get_class_by_tablename(req['type'])
|
Model = get_class_by_tablename(req["type"])
|
||||||
target = Model.query.filter_by(id=req['target']).first_or_404()
|
target = Model.query.filter_by(id=req["target"]).first_or_404()
|
||||||
|
|
||||||
if target.cost > user.score:
|
if target.cost > user.score:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{
|
||||||
'errors': {
|
"success": False,
|
||||||
'score': 'You do not have enough points to unlock this hint'
|
"errors": {
|
||||||
}
|
"score": "You do not have enough points to unlock this hint"
|
||||||
}, 400
|
},
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
schema = UnlockSchema()
|
schema = UnlockSchema()
|
||||||
response = schema.load(req, session=db.session)
|
response = schema.load(req, session=db.session)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
|
|
||||||
award_schema = AwardSchema()
|
award_schema = AwardSchema()
|
||||||
award = {
|
award = {
|
||||||
'user_id': user.id,
|
"user_id": user.id,
|
||||||
'team_id': user.team_id,
|
"team_id": user.team_id,
|
||||||
'name': target.name,
|
"name": target.name,
|
||||||
'description': target.description,
|
"description": target.description,
|
||||||
'value': (-target.cost),
|
"value": (-target.cost),
|
||||||
'category': target.category
|
"category": target.category,
|
||||||
}
|
}
|
||||||
|
|
||||||
award = award_schema.load(award)
|
award = award_schema.load(award)
|
||||||
|
@ -81,7 +75,4 @@ class UnlockList(Resource):
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,88 +1,77 @@
|
||||||
from flask import session, request, abort
|
from flask import session, request, abort
|
||||||
from flask_restplus import Namespace, Resource
|
from flask_restplus import Namespace, Resource
|
||||||
from CTFd.models import db, Users, Solves, Awards, Tracking, Unlocks, Submissions, Notifications
|
from CTFd.models import (
|
||||||
from CTFd.utils.decorators import (
|
db,
|
||||||
authed_only,
|
Users,
|
||||||
admins_only,
|
Solves,
|
||||||
authed,
|
Awards,
|
||||||
ratelimit
|
Tracking,
|
||||||
|
Unlocks,
|
||||||
|
Submissions,
|
||||||
|
Notifications,
|
||||||
)
|
)
|
||||||
|
from CTFd.utils.decorators import authed_only, admins_only, authed, ratelimit
|
||||||
from CTFd.cache import clear_standings
|
from CTFd.cache import clear_standings
|
||||||
from CTFd.utils.config import get_mail_provider
|
from CTFd.utils.config import get_mail_provider
|
||||||
from CTFd.utils.email import sendmail, user_created_notification
|
from CTFd.utils.email import sendmail, user_created_notification
|
||||||
from CTFd.utils.user import get_current_user, is_admin
|
from CTFd.utils.user import get_current_user, is_admin
|
||||||
from CTFd.utils.decorators.visibility import check_account_visibility
|
from CTFd.utils.decorators.visibility import check_account_visibility
|
||||||
|
|
||||||
from CTFd.utils.config.visibility import (
|
from CTFd.utils.config.visibility import accounts_visible, scores_visible
|
||||||
accounts_visible,
|
|
||||||
scores_visible
|
|
||||||
)
|
|
||||||
|
|
||||||
from CTFd.schemas.submissions import SubmissionSchema
|
from CTFd.schemas.submissions import SubmissionSchema
|
||||||
from CTFd.schemas.awards import AwardSchema
|
from CTFd.schemas.awards import AwardSchema
|
||||||
from CTFd.schemas.users import UserSchema
|
from CTFd.schemas.users import UserSchema
|
||||||
|
|
||||||
|
|
||||||
users_namespace = Namespace('users', description="Endpoint to retrieve Users")
|
users_namespace = Namespace("users", description="Endpoint to retrieve Users")
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('')
|
@users_namespace.route("")
|
||||||
class UserList(Resource):
|
class UserList(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
def get(self):
|
def get(self):
|
||||||
users = Users.query.filter_by(banned=False, hidden=False)
|
users = Users.query.filter_by(banned=False, hidden=False)
|
||||||
response = UserSchema(view='user', many=True).dump(users)
|
response = UserSchema(view="user", many=True).dump(users)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
@users_namespace.doc(
|
||||||
|
params={
|
||||||
|
"notify": "Whether to send the created user an email with their credentials"
|
||||||
}
|
}
|
||||||
|
)
|
||||||
@users_namespace.doc(params={'notify': 'Whether to send the created user an email with their credentials'})
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def post(self):
|
def post(self):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
schema = UserSchema('admin')
|
schema = UserSchema("admin")
|
||||||
response = schema.load(req)
|
response = schema.load(req)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.add(response.data)
|
db.session.add(response.data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if request.args.get('notify'):
|
if request.args.get("notify"):
|
||||||
name = response.data.name
|
name = response.data.name
|
||||||
email = response.data.email
|
email = response.data.email
|
||||||
password = req.get('password')
|
password = req.get("password")
|
||||||
|
|
||||||
user_created_notification(
|
user_created_notification(addr=email, name=name, password=password)
|
||||||
addr=email,
|
|
||||||
name=name,
|
|
||||||
password=password
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
response = schema.dump(response.data)
|
response = schema.dump(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/<int:user_id>')
|
@users_namespace.route("/<int:user_id>")
|
||||||
@users_namespace.param('user_id', "User ID")
|
@users_namespace.param("user_id", "User ID")
|
||||||
class UserPublic(Resource):
|
class UserPublic(Resource):
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
def get(self, user_id):
|
def get(self, user_id):
|
||||||
|
@ -91,36 +80,25 @@ class UserPublic(Resource):
|
||||||
if (user.banned or user.hidden) and is_admin() is False:
|
if (user.banned or user.hidden) and is_admin() is False:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
response = UserSchema(
|
response = UserSchema(view=session.get("type", "user")).dump(user)
|
||||||
view=session.get('type', 'user')
|
|
||||||
).dump(user)
|
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
response.data['place'] = user.place
|
response.data["place"] = user.place
|
||||||
response.data['score'] = user.score
|
response.data["score"] = user.score
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def patch(self, user_id):
|
def patch(self, user_id):
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
data['id'] = user_id
|
data["id"] = user_id
|
||||||
schema = UserSchema(view='admin', instance=user, partial=True)
|
schema = UserSchema(view="admin", instance=user, partial=True)
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -130,10 +108,7 @@ class UserPublic(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete(self, user_id):
|
def delete(self, user_id):
|
||||||
|
@ -149,35 +124,27 @@ class UserPublic(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True}
|
||||||
'success': True
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/me')
|
@users_namespace.route("/me")
|
||||||
class UserPrivate(Resource):
|
class UserPrivate(Resource):
|
||||||
@authed_only
|
@authed_only
|
||||||
def get(self):
|
def get(self):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
response = UserSchema('self').dump(user).data
|
response = UserSchema("self").dump(user).data
|
||||||
response['place'] = user.place
|
response["place"] = user.place
|
||||||
response['score'] = user.score
|
response["score"] = user.score
|
||||||
return {
|
return {"success": True, "data": response}
|
||||||
'success': True,
|
|
||||||
'data': response
|
|
||||||
}
|
|
||||||
|
|
||||||
@authed_only
|
@authed_only
|
||||||
def patch(self):
|
def patch(self):
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
schema = UserSchema(view='self', instance=user, partial=True)
|
schema = UserSchema(view="self", instance=user, partial=True)
|
||||||
response = schema.load(data)
|
response = schema.load(data)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -186,17 +153,14 @@ class UserPrivate(Resource):
|
||||||
|
|
||||||
clear_standings()
|
clear_standings()
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/<user_id>/solves')
|
@users_namespace.route("/<user_id>/solves")
|
||||||
@users_namespace.param('user_id', "User ID or 'me'")
|
@users_namespace.param("user_id", "User ID or 'me'")
|
||||||
class UserSolves(Resource):
|
class UserSolves(Resource):
|
||||||
def get(self, user_id):
|
def get(self, user_id):
|
||||||
if user_id == 'me':
|
if user_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
@ -210,26 +174,20 @@ class UserSolves(Resource):
|
||||||
abort(404)
|
abort(404)
|
||||||
solves = user.get_solves(admin=is_admin())
|
solves = user.get_solves(admin=is_admin())
|
||||||
|
|
||||||
view = 'user' if not is_admin() else 'admin'
|
view = "user" if not is_admin() else "admin"
|
||||||
response = SubmissionSchema(view=view, many=True).dump(solves)
|
response = SubmissionSchema(view=view, many=True).dump(solves)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/<user_id>/fails')
|
@users_namespace.route("/<user_id>/fails")
|
||||||
@users_namespace.param('user_id', "User ID or 'me'")
|
@users_namespace.param("user_id", "User ID or 'me'")
|
||||||
class UserFails(Resource):
|
class UserFails(Resource):
|
||||||
def get(self, user_id):
|
def get(self, user_id):
|
||||||
if user_id == 'me':
|
if user_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
@ -243,13 +201,10 @@ class UserFails(Resource):
|
||||||
abort(404)
|
abort(404)
|
||||||
fails = user.get_fails(admin=is_admin())
|
fails = user.get_fails(admin=is_admin())
|
||||||
|
|
||||||
view = 'user' if not is_admin() else 'admin'
|
view = "user" if not is_admin() else "admin"
|
||||||
response = SubmissionSchema(view=view, many=True).dump(fails)
|
response = SubmissionSchema(view=view, many=True).dump(fails)
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
data = response.data
|
data = response.data
|
||||||
|
@ -257,20 +212,14 @@ class UserFails(Resource):
|
||||||
data = []
|
data = []
|
||||||
count = len(response.data)
|
count = len(response.data)
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": data, "meta": {"count": count}}
|
||||||
'success': True,
|
|
||||||
'data': data,
|
|
||||||
'meta': {
|
|
||||||
'count': count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/<user_id>/awards')
|
@users_namespace.route("/<user_id>/awards")
|
||||||
@users_namespace.param('user_id', "User ID or 'me'")
|
@users_namespace.param("user_id", "User ID or 'me'")
|
||||||
class UserAwards(Resource):
|
class UserAwards(Resource):
|
||||||
def get(self, user_id):
|
def get(self, user_id):
|
||||||
if user_id == 'me':
|
if user_id == "me":
|
||||||
if not authed():
|
if not authed():
|
||||||
abort(403)
|
abort(403)
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
@ -284,57 +233,37 @@ class UserAwards(Resource):
|
||||||
abort(404)
|
abort(404)
|
||||||
awards = user.get_awards(admin=is_admin())
|
awards = user.get_awards(admin=is_admin())
|
||||||
|
|
||||||
view = 'user' if not is_admin() else 'admin'
|
view = "user" if not is_admin() else "admin"
|
||||||
response = AwardSchema(view=view, many=True).dump(awards)
|
response = AwardSchema(view=view, many=True).dump(awards)
|
||||||
|
|
||||||
if response.errors:
|
if response.errors:
|
||||||
return {
|
return {"success": False, "errors": response.errors}, 400
|
||||||
'success': False,
|
|
||||||
'errors': response.errors
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
return {
|
return {"success": True, "data": response.data}
|
||||||
'success': True,
|
|
||||||
'data': response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@users_namespace.route('/<int:user_id>/email')
|
@users_namespace.route("/<int:user_id>/email")
|
||||||
@users_namespace.param('user_id', "User ID")
|
@users_namespace.param("user_id", "User ID")
|
||||||
class UserEmails(Resource):
|
class UserEmails(Resource):
|
||||||
@admins_only
|
@admins_only
|
||||||
@ratelimit(method="POST", limit=10, interval=60)
|
@ratelimit(method="POST", limit=10, interval=60)
|
||||||
def post(self, user_id):
|
def post(self, user_id):
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
text = req.get('text', '').strip()
|
text = req.get("text", "").strip()
|
||||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||||
|
|
||||||
if get_mail_provider() is None:
|
if get_mail_provider() is None:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{"success": False, "errors": {"": ["Email settings not configured"]}},
|
||||||
'errors': {
|
400,
|
||||||
"": [
|
)
|
||||||
"Email settings not configured"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
return {
|
return (
|
||||||
'success': False,
|
{"success": False, "errors": {"text": ["Email text cannot be empty"]}},
|
||||||
'errors': {
|
400,
|
||||||
"text": [
|
)
|
||||||
"Email text cannot be empty"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
result, response = sendmail(
|
result, response = sendmail(addr=user.email, text=text)
|
||||||
addr=user.email,
|
|
||||||
text=text
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {"success": result, "data": {}}
|
||||||
'success': result,
|
|
||||||
'data': {}
|
|
||||||
}
|
|
||||||
|
|
327
CTFd/auth.py
327
CTFd/auth.py
|
@ -28,119 +28,150 @@ from CTFd.utils.helpers import error_for, get_errors
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
auth = Blueprint('auth', __name__)
|
auth = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/confirm', methods=['POST', 'GET'])
|
@auth.route("/confirm", methods=["POST", "GET"])
|
||||||
@auth.route('/confirm/<data>', methods=['GET'])
|
@auth.route("/confirm/<data>", methods=["GET"])
|
||||||
@ratelimit(method="POST", limit=10, interval=60)
|
@ratelimit(method="POST", limit=10, interval=60)
|
||||||
def confirm(data=None):
|
def confirm(data=None):
|
||||||
if not get_config('verify_emails'):
|
if not get_config("verify_emails"):
|
||||||
# If the CTF doesn't care about confirming email addresses then redierct to challenges
|
# If the CTF doesn't care about confirming email addresses then redierct to challenges
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
|
|
||||||
# User is confirming email account
|
# User is confirming email account
|
||||||
if data and request.method == "GET":
|
if data and request.method == "GET":
|
||||||
try:
|
try:
|
||||||
user_email = unserialize(data, max_age=1800)
|
user_email = unserialize(data, max_age=1800)
|
||||||
except (BadTimeSignature, SignatureExpired):
|
except (BadTimeSignature, SignatureExpired):
|
||||||
return render_template('confirm.html', errors=['Your confirmation link has expired'])
|
return render_template(
|
||||||
|
"confirm.html", errors=["Your confirmation link has expired"]
|
||||||
|
)
|
||||||
except (BadSignature, TypeError, base64.binascii.Error):
|
except (BadSignature, TypeError, base64.binascii.Error):
|
||||||
return render_template('confirm.html', errors=['Your confirmation token is invalid'])
|
return render_template(
|
||||||
|
"confirm.html", errors=["Your confirmation token is invalid"]
|
||||||
|
)
|
||||||
|
|
||||||
user = Users.query.filter_by(email=user_email).first_or_404()
|
user = Users.query.filter_by(email=user_email).first_or_404()
|
||||||
user.verified = True
|
user.verified = True
|
||||||
log('registrations', format="[{date}] {ip} - successful confirmation for {name}", name=user.name)
|
log(
|
||||||
|
"registrations",
|
||||||
|
format="[{date}] {ip} - successful confirmation for {name}",
|
||||||
|
name=user.name,
|
||||||
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.close()
|
db.session.close()
|
||||||
if current_user.authed():
|
if current_user.authed():
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
# User is trying to start or restart the confirmation flow
|
# User is trying to start or restart the confirmation flow
|
||||||
if not current_user.authed():
|
if not current_user.authed():
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
user = Users.query.filter_by(id=session['id']).first_or_404()
|
user = Users.query.filter_by(id=session["id"]).first_or_404()
|
||||||
if user.verified:
|
if user.verified:
|
||||||
return redirect(url_for('views.settings'))
|
return redirect(url_for("views.settings"))
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# User wants to resend their confirmation email
|
# User wants to resend their confirmation email
|
||||||
email.verify_email_address(user.email)
|
email.verify_email_address(user.email)
|
||||||
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
|
log(
|
||||||
return render_template('confirm.html', user=user, infos=['Your confirmation email has been resent!'])
|
"registrations",
|
||||||
|
format="[{date}] {ip} - {name} initiated a confirmation email resend",
|
||||||
|
)
|
||||||
|
return render_template(
|
||||||
|
"confirm.html",
|
||||||
|
user=user,
|
||||||
|
infos=["Your confirmation email has been resent!"],
|
||||||
|
)
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
# User has been directed to the confirm page
|
# User has been directed to the confirm page
|
||||||
return render_template('confirm.html', user=user)
|
return render_template("confirm.html", user=user)
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/reset_password', methods=['POST', 'GET'])
|
@auth.route("/reset_password", methods=["POST", "GET"])
|
||||||
@auth.route('/reset_password/<data>', methods=['POST', 'GET'])
|
@auth.route("/reset_password/<data>", methods=["POST", "GET"])
|
||||||
@ratelimit(method="POST", limit=10, interval=60)
|
@ratelimit(method="POST", limit=10, interval=60)
|
||||||
def reset_password(data=None):
|
def reset_password(data=None):
|
||||||
if data is not None:
|
if data is not None:
|
||||||
try:
|
try:
|
||||||
name = unserialize(data, max_age=1800)
|
name = unserialize(data, max_age=1800)
|
||||||
except (BadTimeSignature, SignatureExpired):
|
except (BadTimeSignature, SignatureExpired):
|
||||||
return render_template('reset_password.html', errors=['Your link has expired'])
|
return render_template(
|
||||||
|
"reset_password.html", errors=["Your link has expired"]
|
||||||
|
)
|
||||||
except (BadSignature, TypeError, base64.binascii.Error):
|
except (BadSignature, TypeError, base64.binascii.Error):
|
||||||
return render_template('reset_password.html', errors=['Your reset token is invalid'])
|
return render_template(
|
||||||
|
"reset_password.html", errors=["Your reset token is invalid"]
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template('reset_password.html', mode='set')
|
return render_template("reset_password.html", mode="set")
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
user = Users.query.filter_by(name=name).first_or_404()
|
user = Users.query.filter_by(name=name).first_or_404()
|
||||||
user.password = request.form['password'].strip()
|
user.password = request.form["password"].strip()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
log('logins', format="[{date}] {ip} - successful password reset for {name}", name=name)
|
log(
|
||||||
|
"logins",
|
||||||
|
format="[{date}] {ip} - successful password reset for {name}",
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
email_address = request.form['email'].strip()
|
email_address = request.form["email"].strip()
|
||||||
team = Users.query.filter_by(email=email_address).first()
|
team = Users.query.filter_by(email=email_address).first()
|
||||||
|
|
||||||
get_errors()
|
get_errors()
|
||||||
|
|
||||||
if config.can_send_mail() is False:
|
if config.can_send_mail() is False:
|
||||||
return render_template(
|
return render_template(
|
||||||
'reset_password.html',
|
"reset_password.html",
|
||||||
errors=['Email could not be sent due to server misconfiguration']
|
errors=["Email could not be sent due to server misconfiguration"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not team:
|
if not team:
|
||||||
return render_template(
|
return render_template(
|
||||||
'reset_password.html',
|
"reset_password.html",
|
||||||
errors=['If that account exists you will receive an email, please check your inbox']
|
errors=[
|
||||||
|
"If that account exists you will receive an email, please check your inbox"
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
email.forgot_password(email_address, team.name)
|
email.forgot_password(email_address, team.name)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'reset_password.html',
|
"reset_password.html",
|
||||||
errors=['If that account exists you will receive an email, please check your inbox']
|
errors=[
|
||||||
|
"If that account exists you will receive an email, please check your inbox"
|
||||||
|
],
|
||||||
)
|
)
|
||||||
return render_template('reset_password.html')
|
return render_template("reset_password.html")
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/register', methods=['POST', 'GET'])
|
@auth.route("/register", methods=["POST", "GET"])
|
||||||
@check_registration_visibility
|
@check_registration_visibility
|
||||||
@ratelimit(method="POST", limit=10, interval=5)
|
@ratelimit(method="POST", limit=10, interval=5)
|
||||||
def register():
|
def register():
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
name = request.form['name']
|
name = request.form["name"]
|
||||||
email_address = request.form['email']
|
email_address = request.form["email"]
|
||||||
password = request.form['password']
|
password = request.form["password"]
|
||||||
|
|
||||||
name_len = len(name) == 0
|
name_len = len(name) == 0
|
||||||
names = Users.query.add_columns('name', 'id').filter_by(name=name).first()
|
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
|
||||||
emails = Users.query.add_columns('email', 'id').filter_by(email=email_address).first()
|
emails = (
|
||||||
|
Users.query.add_columns("email", "id")
|
||||||
|
.filter_by(email=email_address)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
pass_short = len(password) == 0
|
pass_short = len(password) == 0
|
||||||
pass_long = len(password) > 128
|
pass_long = len(password) > 128
|
||||||
valid_email = validators.validate_email(request.form['email'])
|
valid_email = validators.validate_email(request.form["email"])
|
||||||
team_name_email_check = validators.validate_email(name)
|
team_name_email_check = validators.validate_email(name)
|
||||||
|
|
||||||
if not valid_email:
|
if not valid_email:
|
||||||
|
@ -148,36 +179,36 @@ def register():
|
||||||
if email.check_email_is_whitelisted(email_address) is False:
|
if email.check_email_is_whitelisted(email_address) is False:
|
||||||
errors.append(
|
errors.append(
|
||||||
"Only email addresses under {domains} may register".format(
|
"Only email addresses under {domains} may register".format(
|
||||||
domains=get_config('domain_whitelist')
|
domains=get_config("domain_whitelist")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if names:
|
if names:
|
||||||
errors.append('That user name is already taken')
|
errors.append("That user name is already taken")
|
||||||
if team_name_email_check is True:
|
if team_name_email_check is True:
|
||||||
errors.append('Your user name cannot be an email address')
|
errors.append("Your user name cannot be an email address")
|
||||||
if emails:
|
if emails:
|
||||||
errors.append('That email has already been used')
|
errors.append("That email has already been used")
|
||||||
if pass_short:
|
if pass_short:
|
||||||
errors.append('Pick a longer password')
|
errors.append("Pick a longer password")
|
||||||
if pass_long:
|
if pass_long:
|
||||||
errors.append('Pick a shorter password')
|
errors.append("Pick a shorter password")
|
||||||
if name_len:
|
if name_len:
|
||||||
errors.append('Pick a longer user name')
|
errors.append("Pick a longer user name")
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
return render_template(
|
return render_template(
|
||||||
'register.html',
|
"register.html",
|
||||||
errors=errors,
|
errors=errors,
|
||||||
name=request.form['name'],
|
name=request.form["name"],
|
||||||
email=request.form['email'],
|
email=request.form["email"],
|
||||||
password=request.form['password']
|
password=request.form["password"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
user = Users(
|
user = Users(
|
||||||
name=name.strip(),
|
name=name.strip(),
|
||||||
email=email_address.lower(),
|
email=email_address.lower(),
|
||||||
password=password.strip()
|
password=password.strip(),
|
||||||
)
|
)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -185,31 +216,40 @@ def register():
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email.
|
if config.can_send_mail() and get_config(
|
||||||
log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}")
|
"verify_emails"
|
||||||
|
): # Confirming users is enabled and we can send email.
|
||||||
|
log(
|
||||||
|
"registrations",
|
||||||
|
format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
|
||||||
|
)
|
||||||
email.verify_email_address(user.email)
|
email.verify_email_address(user.email)
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return redirect(url_for('auth.confirm'))
|
return redirect(url_for("auth.confirm"))
|
||||||
else: # Don't care about confirming users
|
else: # Don't care about confirming users
|
||||||
if config.can_send_mail(): # We want to notify the user that they have registered.
|
if (
|
||||||
|
config.can_send_mail()
|
||||||
|
): # We want to notify the user that they have registered.
|
||||||
email.sendmail(
|
email.sendmail(
|
||||||
request.form['email'],
|
request.form["email"],
|
||||||
"You've successfully registered for {}".format(get_config('ctf_name'))
|
"You've successfully registered for {}".format(
|
||||||
|
get_config("ctf_name")
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
log('registrations', "[{date}] {ip} - {name} registered with {email}")
|
log("registrations", "[{date}] {ip} - {name} registered with {email}")
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
else:
|
else:
|
||||||
return render_template('register.html', errors=errors)
|
return render_template("register.html", errors=errors)
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/login', methods=['POST', 'GET'])
|
@auth.route("/login", methods=["POST", "GET"])
|
||||||
@ratelimit(method="POST", limit=10, interval=5)
|
@ratelimit(method="POST", limit=10, interval=5)
|
||||||
def login():
|
def login():
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
name = request.form['name']
|
name = request.form["name"]
|
||||||
|
|
||||||
# Check if the user submitted an email address or a team name
|
# Check if the user submitted an email address or a team name
|
||||||
if validators.validate_email(name) is True:
|
if validators.validate_email(name) is True:
|
||||||
|
@ -218,107 +258,112 @@ def login():
|
||||||
user = Users.query.filter_by(name=name).first()
|
user = Users.query.filter_by(name=name).first()
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
if user and verify_password(request.form['password'], user.password):
|
if user and verify_password(request.form["password"], user.password):
|
||||||
session.regenerate()
|
session.regenerate()
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
log('logins', "[{date}] {ip} - {name} logged in")
|
log("logins", "[{date}] {ip} - {name} logged in")
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
if request.args.get('next') and validators.is_safe_url(request.args.get('next')):
|
if request.args.get("next") and validators.is_safe_url(
|
||||||
return redirect(request.args.get('next'))
|
request.args.get("next")
|
||||||
return redirect(url_for('challenges.listing'))
|
):
|
||||||
|
return redirect(request.args.get("next"))
|
||||||
|
return redirect(url_for("challenges.listing"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This user exists but the password is wrong
|
# This user exists but the password is wrong
|
||||||
log('logins', "[{date}] {ip} - submitted invalid password for {name}")
|
log("logins", "[{date}] {ip} - submitted invalid password for {name}")
|
||||||
errors.append("Your username or password is incorrect")
|
errors.append("Your username or password is incorrect")
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return render_template('login.html', errors=errors)
|
return render_template("login.html", errors=errors)
|
||||||
else:
|
else:
|
||||||
# This user just doesn't exist
|
# This user just doesn't exist
|
||||||
log('logins', "[{date}] {ip} - submitted invalid account information")
|
log("logins", "[{date}] {ip} - submitted invalid account information")
|
||||||
errors.append("Your username or password is incorrect")
|
errors.append("Your username or password is incorrect")
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return render_template('login.html', errors=errors)
|
return render_template("login.html", errors=errors)
|
||||||
else:
|
else:
|
||||||
db.session.close()
|
db.session.close()
|
||||||
return render_template('login.html', errors=errors)
|
return render_template("login.html", errors=errors)
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/oauth')
|
@auth.route("/oauth")
|
||||||
def oauth_login():
|
def oauth_login():
|
||||||
endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \
|
endpoint = (
|
||||||
or get_config('oauth_authorization_endpoint') \
|
get_app_config("OAUTH_AUTHORIZATION_ENDPOINT")
|
||||||
or 'https://auth.majorleaguecyber.org/oauth/authorize'
|
or get_config("oauth_authorization_endpoint")
|
||||||
|
or "https://auth.majorleaguecyber.org/oauth/authorize"
|
||||||
|
)
|
||||||
|
|
||||||
if get_config('user_mode') == 'teams':
|
if get_config("user_mode") == "teams":
|
||||||
scope = 'profile team'
|
scope = "profile team"
|
||||||
else:
|
else:
|
||||||
scope = 'profile'
|
scope = "profile"
|
||||||
|
|
||||||
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
|
client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id")
|
||||||
|
|
||||||
if client_id is None:
|
if client_id is None:
|
||||||
error_for(
|
error_for(
|
||||||
endpoint='auth.login',
|
endpoint="auth.login",
|
||||||
message='OAuth Settings not configured. '
|
message="OAuth Settings not configured. "
|
||||||
'Ask your CTF administrator to configure MajorLeagueCyber integration.'
|
"Ask your CTF administrator to configure MajorLeagueCyber integration.",
|
||||||
)
|
)
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
|
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
|
||||||
endpoint=endpoint,
|
endpoint=endpoint, client_id=client_id, scope=scope, state=session["nonce"]
|
||||||
client_id=client_id,
|
|
||||||
scope=scope,
|
|
||||||
state=session['nonce']
|
|
||||||
)
|
)
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/redirect', methods=['GET'])
|
@auth.route("/redirect", methods=["GET"])
|
||||||
@ratelimit(method="GET", limit=10, interval=60)
|
@ratelimit(method="GET", limit=10, interval=60)
|
||||||
def oauth_redirect():
|
def oauth_redirect():
|
||||||
oauth_code = request.args.get('code')
|
oauth_code = request.args.get("code")
|
||||||
state = request.args.get('state')
|
state = request.args.get("state")
|
||||||
if session['nonce'] != state:
|
if session["nonce"] != state:
|
||||||
log('logins', "[{date}] {ip} - OAuth State validation mismatch")
|
log("logins", "[{date}] {ip} - OAuth State validation mismatch")
|
||||||
error_for(endpoint='auth.login', message='OAuth State validation mismatch.')
|
error_for(endpoint="auth.login", message="OAuth State validation mismatch.")
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
if oauth_code:
|
if oauth_code:
|
||||||
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \
|
url = (
|
||||||
or get_config('oauth_token_endpoint') \
|
get_app_config("OAUTH_TOKEN_ENDPOINT")
|
||||||
or 'https://auth.majorleaguecyber.org/oauth/token'
|
or get_config("oauth_token_endpoint")
|
||||||
|
or "https://auth.majorleaguecyber.org/oauth/token"
|
||||||
|
)
|
||||||
|
|
||||||
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
|
client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id")
|
||||||
client_secret = get_app_config('OAUTH_CLIENT_SECRET') or get_config('oauth_client_secret')
|
client_secret = get_app_config("OAUTH_CLIENT_SECRET") or get_config(
|
||||||
headers = {
|
"oauth_client_secret"
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
)
|
||||||
}
|
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||||
data = {
|
data = {
|
||||||
'code': oauth_code,
|
"code": oauth_code,
|
||||||
'client_id': client_id,
|
"client_id": client_id,
|
||||||
'client_secret': client_secret,
|
"client_secret": client_secret,
|
||||||
'grant_type': 'authorization_code'
|
"grant_type": "authorization_code",
|
||||||
}
|
}
|
||||||
token_request = requests.post(url, data=data, headers=headers)
|
token_request = requests.post(url, data=data, headers=headers)
|
||||||
|
|
||||||
if token_request.status_code == requests.codes.ok:
|
if token_request.status_code == requests.codes.ok:
|
||||||
token = token_request.json()['access_token']
|
token = token_request.json()["access_token"]
|
||||||
user_url = get_app_config('OAUTH_API_ENDPOINT') \
|
user_url = (
|
||||||
or get_config('oauth_api_endpoint') \
|
get_app_config("OAUTH_API_ENDPOINT")
|
||||||
or 'https://api.majorleaguecyber.org/user'
|
or get_config("oauth_api_endpoint")
|
||||||
|
or "https://api.majorleaguecyber.org/user"
|
||||||
|
)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Authorization': 'Bearer ' + str(token),
|
"Authorization": "Bearer " + str(token),
|
||||||
'Content-type': 'application/json'
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
api_data = requests.get(url=user_url, headers=headers).json()
|
api_data = requests.get(url=user_url, headers=headers).json()
|
||||||
|
|
||||||
user_id = api_data['id']
|
user_id = api_data["id"]
|
||||||
user_name = api_data['name']
|
user_name = api_data["name"]
|
||||||
user_email = api_data['email']
|
user_email = api_data["email"]
|
||||||
|
|
||||||
user = Users.query.filter_by(email=user_email).first()
|
user = Users.query.filter_by(email=user_email).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
|
@ -328,29 +373,25 @@ def oauth_redirect():
|
||||||
name=user_name,
|
name=user_name,
|
||||||
email=user_email,
|
email=user_email,
|
||||||
oauth_id=user_id,
|
oauth_id=user_id,
|
||||||
verified=True
|
verified=True,
|
||||||
)
|
)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
else:
|
else:
|
||||||
log('logins', "[{date}] {ip} - Public registration via MLC blocked")
|
log("logins", "[{date}] {ip} - Public registration via MLC blocked")
|
||||||
error_for(
|
error_for(
|
||||||
endpoint='auth.login',
|
endpoint="auth.login",
|
||||||
message='Public registration is disabled. Please try again later.'
|
message="Public registration is disabled. Please try again later.",
|
||||||
)
|
)
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
if get_config('user_mode') == TEAMS_MODE:
|
if get_config("user_mode") == TEAMS_MODE:
|
||||||
team_id = api_data['team']['id']
|
team_id = api_data["team"]["id"]
|
||||||
team_name = api_data['team']['name']
|
team_name = api_data["team"]["name"]
|
||||||
|
|
||||||
team = Teams.query.filter_by(oauth_id=team_id).first()
|
team = Teams.query.filter_by(oauth_id=team_id).first()
|
||||||
if team is None:
|
if team is None:
|
||||||
team = Teams(
|
team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id)
|
||||||
name=team_name,
|
|
||||||
oauth_id=team_id,
|
|
||||||
captain_id=user.id
|
|
||||||
)
|
|
||||||
db.session.add(team)
|
db.session.add(team)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -364,25 +405,21 @@ def oauth_redirect():
|
||||||
|
|
||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
else:
|
else:
|
||||||
log('logins', "[{date}] {ip} - OAuth token retrieval failure")
|
log("logins", "[{date}] {ip} - OAuth token retrieval failure")
|
||||||
error_for(
|
error_for(endpoint="auth.login", message="OAuth token retrieval failure.")
|
||||||
endpoint='auth.login',
|
return redirect(url_for("auth.login"))
|
||||||
message='OAuth token retrieval failure.'
|
|
||||||
)
|
|
||||||
return redirect(url_for('auth.login'))
|
|
||||||
else:
|
else:
|
||||||
log('logins', "[{date}] {ip} - Received redirect without OAuth code")
|
log("logins", "[{date}] {ip} - Received redirect without OAuth code")
|
||||||
error_for(
|
error_for(
|
||||||
endpoint='auth.login',
|
endpoint="auth.login", message="Received redirect without OAuth code."
|
||||||
message='Received redirect without OAuth code.'
|
|
||||||
)
|
)
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/logout')
|
@auth.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
if current_user.authed():
|
if current_user.authed():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('views.static_html'))
|
return redirect(url_for("views.static_html"))
|
||||||
|
|
|
@ -4,7 +4,7 @@ from flask_caching import Cache
|
||||||
cache = Cache()
|
cache = Cache()
|
||||||
|
|
||||||
|
|
||||||
def make_cache_key(path=None, key_prefix='view/%s'):
|
def make_cache_key(path=None, key_prefix="view/%s"):
|
||||||
"""
|
"""
|
||||||
This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses.
|
This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses.
|
||||||
Over time this function may be replaced with a cleaner custom cache implementation.
|
Over time this function may be replaced with a cleaner custom cache implementation.
|
||||||
|
@ -20,6 +20,7 @@ def make_cache_key(path=None, key_prefix='view/%s'):
|
||||||
|
|
||||||
def clear_config():
|
def clear_config():
|
||||||
from CTFd.utils import _get_config, get_app_config
|
from CTFd.utils import _get_config, get_app_config
|
||||||
|
|
||||||
cache.delete_memoized(_get_config)
|
cache.delete_memoized(_get_config)
|
||||||
cache.delete_memoized(get_app_config)
|
cache.delete_memoized(get_app_config)
|
||||||
|
|
||||||
|
@ -28,17 +29,15 @@ def clear_standings():
|
||||||
from CTFd.utils.scores import get_standings
|
from CTFd.utils.scores import get_standings
|
||||||
from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList
|
from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList
|
||||||
from CTFd.api import api
|
from CTFd.api import api
|
||||||
|
|
||||||
cache.delete_memoized(get_standings)
|
cache.delete_memoized(get_standings)
|
||||||
cache.delete(
|
cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint))
|
||||||
make_cache_key(path=api.name + '.' + ScoreboardList.endpoint)
|
cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint))
|
||||||
)
|
|
||||||
cache.delete(
|
|
||||||
make_cache_key(path=api.name + '.' + ScoreboardDetail.endpoint)
|
|
||||||
)
|
|
||||||
cache.delete_memoized(ScoreboardList.get)
|
cache.delete_memoized(ScoreboardList.get)
|
||||||
|
|
||||||
|
|
||||||
def clear_pages():
|
def clear_pages():
|
||||||
from CTFd.utils.config.pages import get_page, get_pages
|
from CTFd.utils.config.pages import get_page, get_pages
|
||||||
|
|
||||||
cache.delete_memoized(get_pages)
|
cache.delete_memoized(get_pages)
|
||||||
cache.delete_memoized(get_page)
|
cache.delete_memoized(get_page)
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
from flask import (
|
from flask import render_template, Blueprint
|
||||||
render_template,
|
|
||||||
Blueprint,
|
|
||||||
)
|
|
||||||
from CTFd.utils.decorators import (
|
from CTFd.utils.decorators import (
|
||||||
during_ctf_time_only,
|
during_ctf_time_only,
|
||||||
require_verified_emails,
|
require_verified_emails,
|
||||||
require_team
|
require_team,
|
||||||
)
|
)
|
||||||
from CTFd.utils.decorators.visibility import check_challenge_visibility
|
from CTFd.utils.decorators.visibility import check_challenge_visibility
|
||||||
from CTFd.utils import config, get_config
|
from CTFd.utils import config, get_config
|
||||||
from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf
|
from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf
|
||||||
from CTFd.utils.helpers import get_errors, get_infos
|
from CTFd.utils.helpers import get_errors, get_infos
|
||||||
|
|
||||||
challenges = Blueprint('challenges', __name__)
|
challenges = Blueprint("challenges", __name__)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/challenges', methods=['GET'])
|
@challenges.route("/challenges", methods=["GET"])
|
||||||
@during_ctf_time_only
|
@during_ctf_time_only
|
||||||
@require_verified_emails
|
@require_verified_emails
|
||||||
@check_challenge_visibility
|
@check_challenge_visibility
|
||||||
|
@ -23,14 +20,16 @@ challenges = Blueprint('challenges', __name__)
|
||||||
def listing():
|
def listing():
|
||||||
infos = get_infos()
|
infos = get_infos()
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
start = get_config('start') or 0
|
start = get_config("start") or 0
|
||||||
end = get_config('end') or 0
|
end = get_config("end") or 0
|
||||||
|
|
||||||
if ctf_paused():
|
if ctf_paused():
|
||||||
infos.append('{} is paused'.format(config.ctf_name()))
|
infos.append("{} is paused".format(config.ctf_name()))
|
||||||
|
|
||||||
# CTF has ended but we want to allow view_after_ctf. Show error but let JS load challenges.
|
# CTF has ended but we want to allow view_after_ctf. Show error but let JS load challenges.
|
||||||
if ctf_ended() and view_after_ctf():
|
if ctf_ended() and view_after_ctf():
|
||||||
infos.append('{} has ended'.format(config.ctf_name()))
|
infos.append("{} has ended".format(config.ctf_name()))
|
||||||
|
|
||||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
return render_template(
|
||||||
|
"challenges.html", infos=infos, errors=errors, start=int(start), end=int(end)
|
||||||
|
)
|
||||||
|
|
118
CTFd/config.py
118
CTFd/config.py
|
@ -1,12 +1,12 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
''' GENERATE SECRET KEY '''
|
""" GENERATE SECRET KEY """
|
||||||
|
|
||||||
if not os.getenv('SECRET_KEY'):
|
if not os.getenv("SECRET_KEY"):
|
||||||
# Attempt to read the secret from the secret file
|
# Attempt to read the secret from the secret file
|
||||||
# This will fail if the secret has not been written
|
# This will fail if the secret has not been written
|
||||||
try:
|
try:
|
||||||
with open('.ctfd_secret_key', 'rb') as secret:
|
with open(".ctfd_secret_key", "rb") as secret:
|
||||||
key = secret.read()
|
key = secret.read()
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
key = None
|
key = None
|
||||||
|
@ -16,14 +16,14 @@ if not os.getenv('SECRET_KEY'):
|
||||||
# Attempt to write the secret file
|
# Attempt to write the secret file
|
||||||
# This will fail if the filesystem is read-only
|
# This will fail if the filesystem is read-only
|
||||||
try:
|
try:
|
||||||
with open('.ctfd_secret_key', 'wb') as secret:
|
with open(".ctfd_secret_key", "wb") as secret:
|
||||||
secret.write(key)
|
secret.write(key)
|
||||||
secret.flush()
|
secret.flush()
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
''' SERVER SETTINGS '''
|
""" SERVER SETTINGS """
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
|
@ -31,7 +31,7 @@ class Config(object):
|
||||||
CTFd Configuration Object
|
CTFd Configuration Object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== REQUIRED SETTINGS ===
|
=== REQUIRED SETTINGS ===
|
||||||
|
|
||||||
SECRET_KEY:
|
SECRET_KEY:
|
||||||
|
@ -61,21 +61,27 @@ class Config(object):
|
||||||
REDIS_URL is the URL to connect to a Redis server.
|
REDIS_URL is the URL to connect to a Redis server.
|
||||||
e.g. redis://user:password@localhost:6379
|
e.g. redis://user:password@localhost:6379
|
||||||
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
|
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
|
||||||
'''
|
"""
|
||||||
SECRET_KEY = os.getenv('SECRET_KEY') or key
|
SECRET_KEY = os.getenv("SECRET_KEY") or key
|
||||||
DATABASE_URL = os.getenv('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))
|
DATABASE_URL = os.getenv("DATABASE_URL") or "sqlite:///{}/ctfd.db".format(
|
||||||
REDIS_URL = os.getenv('REDIS_URL')
|
os.path.dirname(os.path.abspath(__file__))
|
||||||
|
)
|
||||||
|
REDIS_URL = os.getenv("REDIS_URL")
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||||
CACHE_REDIS_URL = REDIS_URL
|
CACHE_REDIS_URL = REDIS_URL
|
||||||
if CACHE_REDIS_URL:
|
if CACHE_REDIS_URL:
|
||||||
CACHE_TYPE = 'redis'
|
CACHE_TYPE = "redis"
|
||||||
else:
|
else:
|
||||||
CACHE_TYPE = 'filesystem'
|
CACHE_TYPE = "filesystem"
|
||||||
CACHE_DIR = os.path.join(os.path.dirname(__file__), os.pardir, '.data', 'filesystem_cache')
|
CACHE_DIR = os.path.join(
|
||||||
CACHE_THRESHOLD = 0 # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.
|
os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache"
|
||||||
|
)
|
||||||
|
CACHE_THRESHOLD = (
|
||||||
|
0
|
||||||
|
) # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== SECURITY ===
|
=== SECURITY ===
|
||||||
|
|
||||||
SESSION_COOKIE_HTTPONLY:
|
SESSION_COOKIE_HTTPONLY:
|
||||||
|
@ -91,23 +97,25 @@ class Config(object):
|
||||||
|
|
||||||
CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based
|
CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based
|
||||||
solely on IP addresses unless you know what you are doing.
|
solely on IP addresses unless you know what you are doing.
|
||||||
'''
|
"""
|
||||||
SESSION_COOKIE_HTTPONLY = (not os.getenv("SESSION_COOKIE_HTTPONLY")) # Defaults True
|
SESSION_COOKIE_HTTPONLY = not os.getenv("SESSION_COOKIE_HTTPONLY") # Defaults True
|
||||||
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or 'Lax'
|
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax"
|
||||||
PERMANENT_SESSION_LIFETIME = int(os.getenv("PERMANENT_SESSION_LIFETIME") or 604800) # 7 days in seconds
|
PERMANENT_SESSION_LIFETIME = int(
|
||||||
|
os.getenv("PERMANENT_SESSION_LIFETIME") or 604800
|
||||||
|
) # 7 days in seconds
|
||||||
TRUSTED_PROXIES = [
|
TRUSTED_PROXIES = [
|
||||||
r'^127\.0\.0\.1$',
|
r"^127\.0\.0\.1$",
|
||||||
# Remove the following proxies if you do not trust the local network
|
# Remove the following proxies if you do not trust the local network
|
||||||
# For example if you are running a CTF on your laptop and the teams are
|
# For example if you are running a CTF on your laptop and the teams are
|
||||||
# all on the same network
|
# all on the same network
|
||||||
r'^::1$',
|
r"^::1$",
|
||||||
r'^fc00:',
|
r"^fc00:",
|
||||||
r'^10\.',
|
r"^10\.",
|
||||||
r'^172\.(1[6-9]|2[0-9]|3[0-1])\.',
|
r"^172\.(1[6-9]|2[0-9]|3[0-1])\.",
|
||||||
r'^192\.168\.'
|
r"^192\.168\.",
|
||||||
]
|
]
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== EMAIL ===
|
=== EMAIL ===
|
||||||
|
|
||||||
MAILFROM_ADDR:
|
MAILFROM_ADDR:
|
||||||
|
@ -139,7 +147,7 @@ class Config(object):
|
||||||
|
|
||||||
MAILGUN_BASE_URL
|
MAILGUN_BASE_URL
|
||||||
Mailgun base url to send email over Mailgun
|
Mailgun base url to send email over Mailgun
|
||||||
'''
|
"""
|
||||||
MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io"
|
MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io"
|
||||||
MAIL_SERVER = os.getenv("MAIL_SERVER") or None
|
MAIL_SERVER = os.getenv("MAIL_SERVER") or None
|
||||||
MAIL_PORT = os.getenv("MAIL_PORT")
|
MAIL_PORT = os.getenv("MAIL_PORT")
|
||||||
|
@ -151,15 +159,17 @@ class Config(object):
|
||||||
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY")
|
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY")
|
||||||
MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL")
|
MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL")
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== LOGS ===
|
=== LOGS ===
|
||||||
LOG_FOLDER:
|
LOG_FOLDER:
|
||||||
The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins.
|
The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins.
|
||||||
The default location is the CTFd/logs folder.
|
The default location is the CTFd/logs folder.
|
||||||
'''
|
"""
|
||||||
LOG_FOLDER = os.getenv('LOG_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
LOG_FOLDER = os.getenv("LOG_FOLDER") or os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "logs"
|
||||||
|
)
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== UPLOADS ===
|
=== UPLOADS ===
|
||||||
|
|
||||||
UPLOAD_PROVIDER:
|
UPLOAD_PROVIDER:
|
||||||
|
@ -180,16 +190,18 @@ class Config(object):
|
||||||
AWS_S3_ENDPOINT_URL:
|
AWS_S3_ENDPOINT_URL:
|
||||||
A URL pointing to a custom S3 implementation.
|
A URL pointing to a custom S3 implementation.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
UPLOAD_PROVIDER = os.getenv('UPLOAD_PROVIDER') or 'filesystem'
|
UPLOAD_PROVIDER = os.getenv("UPLOAD_PROVIDER") or "filesystem"
|
||||||
UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER") or os.path.join(
|
||||||
if UPLOAD_PROVIDER == 's3':
|
os.path.dirname(os.path.abspath(__file__)), "uploads"
|
||||||
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
)
|
||||||
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
|
if UPLOAD_PROVIDER == "s3":
|
||||||
AWS_S3_BUCKET = os.getenv('AWS_S3_BUCKET')
|
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
|
||||||
AWS_S3_ENDPOINT_URL = os.getenv('AWS_S3_ENDPOINT_URL')
|
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||||
|
AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET")
|
||||||
|
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== OPTIONAL ===
|
=== OPTIONAL ===
|
||||||
|
|
||||||
REVERSE_PROXY:
|
REVERSE_PROXY:
|
||||||
|
@ -216,33 +228,35 @@ class Config(object):
|
||||||
APPLICATION_ROOT:
|
APPLICATION_ROOT:
|
||||||
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
|
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
|
||||||
Example: /ctfd
|
Example: /ctfd
|
||||||
'''
|
"""
|
||||||
REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False
|
REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False
|
||||||
TEMPLATES_AUTO_RELOAD = (not os.getenv("TEMPLATES_AUTO_RELOAD")) # Defaults True
|
TEMPLATES_AUTO_RELOAD = not os.getenv("TEMPLATES_AUTO_RELOAD") # Defaults True
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None # Defaults False
|
SQLALCHEMY_TRACK_MODIFICATIONS = (
|
||||||
SWAGGER_UI = '/' if os.getenv("SWAGGER_UI") is not None else False # Defaults False
|
os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None
|
||||||
UPDATE_CHECK = (not os.getenv("UPDATE_CHECK")) # Defaults True
|
) # Defaults False
|
||||||
APPLICATION_ROOT = os.getenv('APPLICATION_ROOT') or '/'
|
SWAGGER_UI = "/" if os.getenv("SWAGGER_UI") is not None else False # Defaults False
|
||||||
|
UPDATE_CHECK = not os.getenv("UPDATE_CHECK") # Defaults True
|
||||||
|
APPLICATION_ROOT = os.getenv("APPLICATION_ROOT") or "/"
|
||||||
|
|
||||||
'''
|
"""
|
||||||
=== OAUTH ===
|
=== OAUTH ===
|
||||||
|
|
||||||
MajorLeagueCyber Integration
|
MajorLeagueCyber Integration
|
||||||
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here
|
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here
|
||||||
'''
|
"""
|
||||||
OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID")
|
OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID")
|
||||||
OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET")
|
OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET")
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA'
|
SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA"
|
||||||
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
||||||
TESTING = True
|
TESTING = True
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
SQLALCHEMY_DATABASE_URI = os.getenv('TESTING_DATABASE_URL') or 'sqlite://'
|
SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://"
|
||||||
SERVER_NAME = 'localhost'
|
SERVER_NAME = "localhost"
|
||||||
UPDATE_CHECK = False
|
UPDATE_CHECK = False
|
||||||
REDIS_URL = None
|
REDIS_URL = None
|
||||||
CACHE_TYPE = 'simple'
|
CACHE_TYPE = "simple"
|
||||||
CACHE_THRESHOLD = 500
|
CACHE_THRESHOLD = 500
|
||||||
SAFE_MODE = True
|
SAFE_MODE = True
|
||||||
|
|
|
@ -3,19 +3,19 @@ from flask import render_template
|
||||||
|
|
||||||
# 404
|
# 404
|
||||||
def page_not_found(error):
|
def page_not_found(error):
|
||||||
return render_template('errors/404.html', error=error.description), 404
|
return render_template("errors/404.html", error=error.description), 404
|
||||||
|
|
||||||
|
|
||||||
# 403
|
# 403
|
||||||
def forbidden(error):
|
def forbidden(error):
|
||||||
return render_template('errors/403.html', error=error.description), 403
|
return render_template("errors/403.html", error=error.description), 403
|
||||||
|
|
||||||
|
|
||||||
# 500
|
# 500
|
||||||
def general_error(error):
|
def general_error(error):
|
||||||
return render_template('errors/500.html'), 500
|
return render_template("errors/500.html"), 500
|
||||||
|
|
||||||
|
|
||||||
# 502
|
# 502
|
||||||
def gateway_error(error):
|
def gateway_error(error):
|
||||||
return render_template('errors/502.html', error=error.description), 502
|
return render_template("errors/502.html", error=error.description), 502
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import current_app, Blueprint, Response, stream_with_context
|
from flask import current_app, Blueprint, Response, stream_with_context
|
||||||
from CTFd.utils.decorators import authed_only, ratelimit
|
from CTFd.utils.decorators import authed_only, ratelimit
|
||||||
|
|
||||||
events = Blueprint('events', __name__)
|
events = Blueprint("events", __name__)
|
||||||
|
|
||||||
|
|
||||||
@events.route("/events")
|
@events.route("/events")
|
||||||
|
|
|
@ -20,29 +20,29 @@ def get_class_by_tablename(tablename):
|
||||||
:return: Class reference or None.
|
:return: Class reference or None.
|
||||||
"""
|
"""
|
||||||
for c in db.Model._decl_class_registry.values():
|
for c in db.Model._decl_class_registry.values():
|
||||||
if hasattr(c, '__tablename__') and c.__tablename__ == tablename:
|
if hasattr(c, "__tablename__") and c.__tablename__ == tablename:
|
||||||
return c
|
return c
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Notifications(db.Model):
|
class Notifications(db.Model):
|
||||||
__tablename__ = 'notifications'
|
__tablename__ = "notifications"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
title = db.Column(db.Text)
|
title = db.Column(db.Text)
|
||||||
content = db.Column(db.Text)
|
content = db.Column(db.Text)
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"))
|
||||||
|
|
||||||
user = db.relationship('Users', foreign_keys="Notifications.user_id", lazy='select')
|
user = db.relationship("Users", foreign_keys="Notifications.user_id", lazy="select")
|
||||||
team = db.relationship('Teams', foreign_keys="Notifications.team_id", lazy='select')
|
team = db.relationship("Teams", foreign_keys="Notifications.team_id", lazy="select")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Notifications, self).__init__(**kwargs)
|
super(Notifications, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Pages(db.Model):
|
class Pages(db.Model):
|
||||||
__tablename__ = 'pages'
|
__tablename__ = "pages"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
title = db.Column(db.String(80))
|
title = db.Column(db.String(80))
|
||||||
route = db.Column(db.String(128), unique=True)
|
route = db.Column(db.String(128), unique=True)
|
||||||
|
@ -62,7 +62,7 @@ class Pages(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Challenges(db.Model):
|
class Challenges(db.Model):
|
||||||
__tablename__ = 'challenges'
|
__tablename__ = "challenges"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(80))
|
name = db.Column(db.String(80))
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
|
@ -70,7 +70,7 @@ class Challenges(db.Model):
|
||||||
value = db.Column(db.Integer)
|
value = db.Column(db.Integer)
|
||||||
category = db.Column(db.String(80))
|
category = db.Column(db.String(80))
|
||||||
type = db.Column(db.String(80))
|
type = db.Column(db.String(80))
|
||||||
state = db.Column(db.String(80), nullable=False, default='visible')
|
state = db.Column(db.String(80), nullable=False, default="visible")
|
||||||
requirements = db.Column(db.JSON)
|
requirements = db.Column(db.JSON)
|
||||||
|
|
||||||
files = db.relationship("ChallengeFiles", backref="challenge")
|
files = db.relationship("ChallengeFiles", backref="challenge")
|
||||||
|
@ -78,31 +78,27 @@ class Challenges(db.Model):
|
||||||
hints = db.relationship("Hints", backref="challenge")
|
hints = db.relationship("Hints", backref="challenge")
|
||||||
flags = db.relationship("Flags", backref="challenge")
|
flags = db.relationship("Flags", backref="challenge")
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
'polymorphic_identity': 'standard',
|
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Challenges, self).__init__(**kwargs)
|
super(Challenges, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Challenge %r>' % self.name
|
return "<Challenge %r>" % self.name
|
||||||
|
|
||||||
|
|
||||||
class Hints(db.Model):
|
class Hints(db.Model):
|
||||||
__tablename__ = 'hints'
|
__tablename__ = "hints"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
type = db.Column(db.String(80), default='standard')
|
type = db.Column(db.String(80), default="standard")
|
||||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
challenge_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
content = db.Column(db.Text)
|
content = db.Column(db.Text)
|
||||||
cost = db.Column(db.Integer, default=0)
|
cost = db.Column(db.Integer, default=0)
|
||||||
requirements = db.Column(db.JSON)
|
requirements = db.Column(db.JSON)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
'polymorphic_identity': 'standard',
|
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -120,15 +116,15 @@ class Hints(db.Model):
|
||||||
super(Hints, self).__init__(**kwargs)
|
super(Hints, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Hint %r>' % self.content
|
return "<Hint %r>" % self.content
|
||||||
|
|
||||||
|
|
||||||
class Awards(db.Model):
|
class Awards(db.Model):
|
||||||
__tablename__ = 'awards'
|
__tablename__ = "awards"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
|
||||||
type = db.Column(db.String(80), default='standard')
|
type = db.Column(db.String(80), default="standard")
|
||||||
name = db.Column(db.String(80))
|
name = db.Column(db.String(80))
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
@ -137,33 +133,32 @@ class Awards(db.Model):
|
||||||
icon = db.Column(db.Text)
|
icon = db.Column(db.Text)
|
||||||
requirements = db.Column(db.JSON)
|
requirements = db.Column(db.JSON)
|
||||||
|
|
||||||
user = db.relationship('Users', foreign_keys="Awards.user_id", lazy='select')
|
user = db.relationship("Users", foreign_keys="Awards.user_id", lazy="select")
|
||||||
team = db.relationship('Teams', foreign_keys="Awards.team_id", lazy='select')
|
team = db.relationship("Teams", foreign_keys="Awards.team_id", lazy="select")
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
'polymorphic_identity': 'standard',
|
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def account_id(self):
|
def account_id(self):
|
||||||
user_mode = get_config('user_mode')
|
user_mode = get_config("user_mode")
|
||||||
if user_mode == 'teams':
|
if user_mode == "teams":
|
||||||
return self.team_id
|
return self.team_id
|
||||||
elif user_mode == 'users':
|
elif user_mode == "users":
|
||||||
return self.user_id
|
return self.user_id
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Awards, self).__init__(**kwargs)
|
super(Awards, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Award %r>' % self.name
|
return "<Award %r>" % self.name
|
||||||
|
|
||||||
|
|
||||||
class Tags(db.Model):
|
class Tags(db.Model):
|
||||||
__tablename__ = 'tags'
|
__tablename__ = "tags"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
challenge_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
value = db.Column(db.String(80))
|
value = db.Column(db.String(80))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -171,54 +166,51 @@ class Tags(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Files(db.Model):
|
class Files(db.Model):
|
||||||
__tablename__ = 'files'
|
__tablename__ = "files"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
type = db.Column(db.String(80), default='standard')
|
type = db.Column(db.String(80), default="standard")
|
||||||
location = db.Column(db.Text)
|
location = db.Column(db.Text)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
'polymorphic_identity': 'standard',
|
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Files, self).__init__(**kwargs)
|
super(Files, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<File type={type} location={location}>".format(type=self.type, location=self.location)
|
return "<File type={type} location={location}>".format(
|
||||||
|
type=self.type, location=self.location
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChallengeFiles(Files):
|
class ChallengeFiles(Files):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "challenge"}
|
||||||
'polymorphic_identity': 'challenge'
|
challenge_id = db.Column(
|
||||||
}
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ChallengeFiles, self).__init__(**kwargs)
|
super(ChallengeFiles, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class PageFiles(Files):
|
class PageFiles(Files):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "page"}
|
||||||
'polymorphic_identity': 'page'
|
page_id = db.Column(db.Integer, db.ForeignKey("pages.id"))
|
||||||
}
|
|
||||||
page_id = db.Column(db.Integer, db.ForeignKey('pages.id'))
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(PageFiles, self).__init__(**kwargs)
|
super(PageFiles, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Flags(db.Model):
|
class Flags(db.Model):
|
||||||
__tablename__ = 'flags'
|
__tablename__ = "flags"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
challenge_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
type = db.Column(db.String(80))
|
type = db.Column(db.String(80))
|
||||||
content = db.Column(db.Text)
|
content = db.Column(db.Text)
|
||||||
data = db.Column(db.Text)
|
data = db.Column(db.Text)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_on": type}
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Flags, self).__init__(**kwargs)
|
super(Flags, self).__init__(**kwargs)
|
||||||
|
@ -228,11 +220,8 @@ class Flags(db.Model):
|
||||||
|
|
||||||
|
|
||||||
class Users(db.Model):
|
class Users(db.Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = "users"
|
||||||
__table_args__ = (
|
__table_args__ = (db.UniqueConstraint("id", "oauth_id"), {})
|
||||||
db.UniqueConstraint('id', 'oauth_id'),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
# Core attributes
|
# Core attributes
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
oauth_id = db.Column(db.Integer, unique=True)
|
oauth_id = db.Column(db.Integer, unique=True)
|
||||||
|
@ -253,28 +242,25 @@ class Users(db.Model):
|
||||||
verified = db.Column(db.Boolean, default=False)
|
verified = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
# Relationship for Teams
|
# Relationship for Teams
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id"))
|
||||||
|
|
||||||
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "user", "polymorphic_on": type}
|
||||||
'polymorphic_identity': 'user',
|
|
||||||
'polymorphic_on': type
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Users, self).__init__(**kwargs)
|
super(Users, self).__init__(**kwargs)
|
||||||
|
|
||||||
@validates('password')
|
@validates("password")
|
||||||
def validate_password(self, key, plaintext):
|
def validate_password(self, key, plaintext):
|
||||||
return hash_password(str(plaintext))
|
return hash_password(str(plaintext))
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def account_id(self):
|
def account_id(self):
|
||||||
user_mode = get_config('user_mode')
|
user_mode = get_config("user_mode")
|
||||||
if user_mode == 'teams':
|
if user_mode == "teams":
|
||||||
return self.team_id
|
return self.team_id
|
||||||
elif user_mode == 'users':
|
elif user_mode == "users":
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -299,7 +285,7 @@ class Users(db.Model):
|
||||||
|
|
||||||
def get_solves(self, admin=False):
|
def get_solves(self, admin=False):
|
||||||
solves = Solves.query.filter_by(user_id=self.id)
|
solves = Solves.query.filter_by(user_id=self.id)
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
solves = solves.filter(Solves.date < dt)
|
solves = solves.filter(Solves.date < dt)
|
||||||
|
@ -307,7 +293,7 @@ class Users(db.Model):
|
||||||
|
|
||||||
def get_fails(self, admin=False):
|
def get_fails(self, admin=False):
|
||||||
fails = Fails.query.filter_by(user_id=self.id)
|
fails = Fails.query.filter_by(user_id=self.id)
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
fails = fails.filter(Fails.date < dt)
|
fails = fails.filter(Fails.date < dt)
|
||||||
|
@ -315,27 +301,26 @@ class Users(db.Model):
|
||||||
|
|
||||||
def get_awards(self, admin=False):
|
def get_awards(self, admin=False):
|
||||||
awards = Awards.query.filter_by(user_id=self.id)
|
awards = Awards.query.filter_by(user_id=self.id)
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
awards = awards.filter(Awards.date < dt)
|
awards = awards.filter(Awards.date < dt)
|
||||||
return awards.all()
|
return awards.all()
|
||||||
|
|
||||||
def get_score(self, admin=False):
|
def get_score(self, admin=False):
|
||||||
score = db.func.sum(Challenges.value).label('score')
|
score = db.func.sum(Challenges.value).label("score")
|
||||||
user = db.session.query(
|
user = (
|
||||||
Solves.user_id,
|
db.session.query(Solves.user_id, score)
|
||||||
score
|
.join(Users, Solves.user_id == Users.id)
|
||||||
) \
|
.join(Challenges, Solves.challenge_id == Challenges.id)
|
||||||
.join(Users, Solves.user_id == Users.id) \
|
|
||||||
.join(Challenges, Solves.challenge_id == Challenges.id) \
|
|
||||||
.filter(Users.id == self.id)
|
.filter(Users.id == self.id)
|
||||||
|
)
|
||||||
|
|
||||||
award_score = db.func.sum(Awards.value).label('award_score')
|
award_score = db.func.sum(Awards.value).label("award_score")
|
||||||
award = db.session.query(award_score).filter_by(user_id=self.id)
|
award = db.session.query(award_score).filter_by(user_id=self.id)
|
||||||
|
|
||||||
if not admin:
|
if not admin:
|
||||||
freeze = Configs.query.filter_by(key='freeze').first()
|
freeze = Configs.query.filter_by(key="freeze").first()
|
||||||
if freeze and freeze.value:
|
if freeze and freeze.value:
|
||||||
freeze = int(freeze.value)
|
freeze = int(freeze.value)
|
||||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
|
@ -361,50 +346,63 @@ class Users(db.Model):
|
||||||
to no imports within the CTFd application as importing from the
|
to no imports within the CTFd application as importing from the
|
||||||
application itself will result in a circular import.
|
application itself will result in a circular import.
|
||||||
"""
|
"""
|
||||||
scores = db.session.query(
|
scores = (
|
||||||
Solves.user_id.label('user_id'),
|
db.session.query(
|
||||||
db.func.sum(Challenges.value).label('score'),
|
Solves.user_id.label("user_id"),
|
||||||
db.func.max(Solves.id).label('id'),
|
db.func.sum(Challenges.value).label("score"),
|
||||||
db.func.max(Solves.date).label('date')
|
db.func.max(Solves.id).label("id"),
|
||||||
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id)
|
db.func.max(Solves.date).label("date"),
|
||||||
|
)
|
||||||
|
.join(Challenges)
|
||||||
|
.filter(Challenges.value != 0)
|
||||||
|
.group_by(Solves.user_id)
|
||||||
|
)
|
||||||
|
|
||||||
awards = db.session.query(
|
awards = (
|
||||||
Awards.user_id.label('user_id'),
|
db.session.query(
|
||||||
db.func.sum(Awards.value).label('score'),
|
Awards.user_id.label("user_id"),
|
||||||
db.func.max(Awards.id).label('id'),
|
db.func.sum(Awards.value).label("score"),
|
||||||
db.func.max(Awards.date).label('date')
|
db.func.max(Awards.id).label("id"),
|
||||||
).filter(Awards.value != 0).group_by(Awards.user_id)
|
db.func.max(Awards.date).label("date"),
|
||||||
|
)
|
||||||
|
.filter(Awards.value != 0)
|
||||||
|
.group_by(Awards.user_id)
|
||||||
|
)
|
||||||
|
|
||||||
if not admin:
|
if not admin:
|
||||||
freeze = Configs.query.filter_by(key='freeze').first()
|
freeze = Configs.query.filter_by(key="freeze").first()
|
||||||
if freeze and freeze.value:
|
if freeze and freeze.value:
|
||||||
freeze = int(freeze.value)
|
freeze = int(freeze.value)
|
||||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
scores = scores.filter(Solves.date < freeze)
|
scores = scores.filter(Solves.date < freeze)
|
||||||
awards = awards.filter(Awards.date < freeze)
|
awards = awards.filter(Awards.date < freeze)
|
||||||
|
|
||||||
results = union_all(scores, awards).alias('results')
|
results = union_all(scores, awards).alias("results")
|
||||||
|
|
||||||
sumscores = db.session.query(
|
sumscores = (
|
||||||
results.columns.user_id,
|
db.session.query(
|
||||||
db.func.sum(results.columns.score).label('score'),
|
results.columns.user_id,
|
||||||
db.func.max(results.columns.id).label('id'),
|
db.func.sum(results.columns.score).label("score"),
|
||||||
db.func.max(results.columns.date).label('date')
|
db.func.max(results.columns.id).label("id"),
|
||||||
).group_by(results.columns.user_id).subquery()
|
db.func.max(results.columns.date).label("date"),
|
||||||
|
)
|
||||||
|
.group_by(results.columns.user_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
if admin:
|
if admin:
|
||||||
standings_query = db.session.query(
|
standings_query = (
|
||||||
Users.id.label('user_id'),
|
db.session.query(Users.id.label("user_id"))
|
||||||
) \
|
.join(sumscores, Users.id == sumscores.columns.user_id)
|
||||||
.join(sumscores, Users.id == sumscores.columns.user_id) \
|
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
standings_query = db.session.query(
|
standings_query = (
|
||||||
Users.id.label('user_id'),
|
db.session.query(Users.id.label("user_id"))
|
||||||
) \
|
.join(sumscores, Users.id == sumscores.columns.user_id)
|
||||||
.join(sumscores, Users.id == sumscores.columns.user_id) \
|
.filter(Users.banned == False, Users.hidden == False)
|
||||||
.filter(Users.banned == False, Users.hidden == False) \
|
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
)
|
||||||
|
|
||||||
standings = standings_query.all()
|
standings = standings_query.all()
|
||||||
|
|
||||||
|
@ -415,24 +413,19 @@ class Users(db.Model):
|
||||||
return i
|
return i
|
||||||
else:
|
else:
|
||||||
k = i % 10
|
k = i % 10
|
||||||
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
|
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k :: 4])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class Admins(Users):
|
class Admins(Users):
|
||||||
__tablename__ = 'admins'
|
__tablename__ = "admins"
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "admin"}
|
||||||
'polymorphic_identity': 'admin'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Teams(db.Model):
|
class Teams(db.Model):
|
||||||
__tablename__ = 'teams'
|
__tablename__ = "teams"
|
||||||
__table_args__ = (
|
__table_args__ = (db.UniqueConstraint("id", "oauth_id"), {})
|
||||||
db.UniqueConstraint('id', 'oauth_id'),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
# Core attributes
|
# Core attributes
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
oauth_id = db.Column(db.Integer, unique=True)
|
oauth_id = db.Column(db.Integer, unique=True)
|
||||||
|
@ -442,7 +435,7 @@ class Teams(db.Model):
|
||||||
password = db.Column(db.String(128))
|
password = db.Column(db.String(128))
|
||||||
secret = db.Column(db.String(128))
|
secret = db.Column(db.String(128))
|
||||||
|
|
||||||
members = db.relationship("Users", backref="team", foreign_keys='Users.team_id')
|
members = db.relationship("Users", backref="team", foreign_keys="Users.team_id")
|
||||||
|
|
||||||
# Supplementary attributes
|
# Supplementary attributes
|
||||||
website = db.Column(db.String(128))
|
website = db.Column(db.String(128))
|
||||||
|
@ -453,7 +446,7 @@ class Teams(db.Model):
|
||||||
banned = db.Column(db.Boolean, default=False)
|
banned = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
# Relationship for Users
|
# Relationship for Users
|
||||||
captain_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'))
|
captain_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL"))
|
||||||
captain = db.relationship("Users", foreign_keys=[captain_id])
|
captain = db.relationship("Users", foreign_keys=[captain_id])
|
||||||
|
|
||||||
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
@ -461,7 +454,7 @@ class Teams(db.Model):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(Teams, self).__init__(**kwargs)
|
super(Teams, self).__init__(**kwargs)
|
||||||
|
|
||||||
@validates('password')
|
@validates("password")
|
||||||
def validate_password(self, key, plaintext):
|
def validate_password(self, key, plaintext):
|
||||||
return hash_password(str(plaintext))
|
return hash_password(str(plaintext))
|
||||||
|
|
||||||
|
@ -488,13 +481,11 @@ class Teams(db.Model):
|
||||||
def get_solves(self, admin=False):
|
def get_solves(self, admin=False):
|
||||||
member_ids = [member.id for member in self.members]
|
member_ids = [member.id for member in self.members]
|
||||||
|
|
||||||
solves = Solves.query.filter(
|
solves = Solves.query.filter(Solves.user_id.in_(member_ids)).order_by(
|
||||||
Solves.user_id.in_(member_ids)
|
|
||||||
).order_by(
|
|
||||||
Solves.date.asc()
|
Solves.date.asc()
|
||||||
)
|
)
|
||||||
|
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
solves = solves.filter(Solves.date < dt)
|
solves = solves.filter(Solves.date < dt)
|
||||||
|
@ -504,13 +495,11 @@ class Teams(db.Model):
|
||||||
def get_fails(self, admin=False):
|
def get_fails(self, admin=False):
|
||||||
member_ids = [member.id for member in self.members]
|
member_ids = [member.id for member in self.members]
|
||||||
|
|
||||||
fails = Fails.query.filter(
|
fails = Fails.query.filter(Fails.user_id.in_(member_ids)).order_by(
|
||||||
Fails.user_id.in_(member_ids)
|
|
||||||
).order_by(
|
|
||||||
Fails.date.asc()
|
Fails.date.asc()
|
||||||
)
|
)
|
||||||
|
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
fails = fails.filter(Fails.date < dt)
|
fails = fails.filter(Fails.date < dt)
|
||||||
|
@ -520,13 +509,11 @@ class Teams(db.Model):
|
||||||
def get_awards(self, admin=False):
|
def get_awards(self, admin=False):
|
||||||
member_ids = [member.id for member in self.members]
|
member_ids = [member.id for member in self.members]
|
||||||
|
|
||||||
awards = Awards.query.filter(
|
awards = Awards.query.filter(Awards.user_id.in_(member_ids)).order_by(
|
||||||
Awards.user_id.in_(member_ids)
|
|
||||||
).order_by(
|
|
||||||
Awards.date.asc()
|
Awards.date.asc()
|
||||||
)
|
)
|
||||||
|
|
||||||
freeze = get_config('freeze')
|
freeze = get_config("freeze")
|
||||||
if freeze and admin is False:
|
if freeze and admin is False:
|
||||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
awards = awards.filter(Awards.date < dt)
|
awards = awards.filter(Awards.date < dt)
|
||||||
|
@ -546,50 +533,63 @@ class Teams(db.Model):
|
||||||
to no imports within the CTFd application as importing from the
|
to no imports within the CTFd application as importing from the
|
||||||
application itself will result in a circular import.
|
application itself will result in a circular import.
|
||||||
"""
|
"""
|
||||||
scores = db.session.query(
|
scores = (
|
||||||
Solves.team_id.label('team_id'),
|
db.session.query(
|
||||||
db.func.sum(Challenges.value).label('score'),
|
Solves.team_id.label("team_id"),
|
||||||
db.func.max(Solves.id).label('id'),
|
db.func.sum(Challenges.value).label("score"),
|
||||||
db.func.max(Solves.date).label('date')
|
db.func.max(Solves.id).label("id"),
|
||||||
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id)
|
db.func.max(Solves.date).label("date"),
|
||||||
|
)
|
||||||
|
.join(Challenges)
|
||||||
|
.filter(Challenges.value != 0)
|
||||||
|
.group_by(Solves.team_id)
|
||||||
|
)
|
||||||
|
|
||||||
awards = db.session.query(
|
awards = (
|
||||||
Awards.team_id.label('team_id'),
|
db.session.query(
|
||||||
db.func.sum(Awards.value).label('score'),
|
Awards.team_id.label("team_id"),
|
||||||
db.func.max(Awards.id).label('id'),
|
db.func.sum(Awards.value).label("score"),
|
||||||
db.func.max(Awards.date).label('date')
|
db.func.max(Awards.id).label("id"),
|
||||||
).filter(Awards.value != 0).group_by(Awards.team_id)
|
db.func.max(Awards.date).label("date"),
|
||||||
|
)
|
||||||
|
.filter(Awards.value != 0)
|
||||||
|
.group_by(Awards.team_id)
|
||||||
|
)
|
||||||
|
|
||||||
if not admin:
|
if not admin:
|
||||||
freeze = Configs.query.filter_by(key='freeze').first()
|
freeze = Configs.query.filter_by(key="freeze").first()
|
||||||
if freeze and freeze.value:
|
if freeze and freeze.value:
|
||||||
freeze = int(freeze.value)
|
freeze = int(freeze.value)
|
||||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||||
scores = scores.filter(Solves.date < freeze)
|
scores = scores.filter(Solves.date < freeze)
|
||||||
awards = awards.filter(Awards.date < freeze)
|
awards = awards.filter(Awards.date < freeze)
|
||||||
|
|
||||||
results = union_all(scores, awards).alias('results')
|
results = union_all(scores, awards).alias("results")
|
||||||
|
|
||||||
sumscores = db.session.query(
|
sumscores = (
|
||||||
results.columns.team_id,
|
db.session.query(
|
||||||
db.func.sum(results.columns.score).label('score'),
|
results.columns.team_id,
|
||||||
db.func.max(results.columns.id).label('id'),
|
db.func.sum(results.columns.score).label("score"),
|
||||||
db.func.max(results.columns.date).label('date')
|
db.func.max(results.columns.id).label("id"),
|
||||||
).group_by(results.columns.team_id).subquery()
|
db.func.max(results.columns.date).label("date"),
|
||||||
|
)
|
||||||
|
.group_by(results.columns.team_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
|
||||||
if admin:
|
if admin:
|
||||||
standings_query = db.session.query(
|
standings_query = (
|
||||||
Teams.id.label('team_id'),
|
db.session.query(Teams.id.label("team_id"))
|
||||||
) \
|
.join(sumscores, Teams.id == sumscores.columns.team_id)
|
||||||
.join(sumscores, Teams.id == sumscores.columns.team_id) \
|
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
standings_query = db.session.query(
|
standings_query = (
|
||||||
Teams.id.label('team_id'),
|
db.session.query(Teams.id.label("team_id"))
|
||||||
) \
|
.join(sumscores, Teams.id == sumscores.columns.team_id)
|
||||||
.join(sumscores, Teams.id == sumscores.columns.team_id) \
|
.filter(Teams.banned == False)
|
||||||
.filter(Teams.banned == False) \
|
|
||||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||||
|
)
|
||||||
|
|
||||||
standings = standings_query.all()
|
standings = standings_query.all()
|
||||||
|
|
||||||
|
@ -597,45 +597,47 @@ class Teams(db.Model):
|
||||||
try:
|
try:
|
||||||
i = standings.index((self.id,)) + 1
|
i = standings.index((self.id,)) + 1
|
||||||
k = i % 10
|
k = i % 10
|
||||||
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
|
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k :: 4])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class Submissions(db.Model):
|
class Submissions(db.Model):
|
||||||
__tablename__ = 'submissions'
|
__tablename__ = "submissions"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
challenge_id = db.Column(
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
|
)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
|
||||||
ip = db.Column(db.String(46))
|
ip = db.Column(db.String(46))
|
||||||
provided = db.Column(db.Text)
|
provided = db.Column(db.Text)
|
||||||
type = db.Column(db.String(32))
|
type = db.Column(db.String(32))
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
user = db.relationship('Users', foreign_keys="Submissions.user_id", lazy='select')
|
user = db.relationship("Users", foreign_keys="Submissions.user_id", lazy="select")
|
||||||
team = db.relationship('Teams', foreign_keys="Submissions.team_id", lazy='select')
|
team = db.relationship("Teams", foreign_keys="Submissions.team_id", lazy="select")
|
||||||
challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select')
|
challenge = db.relationship(
|
||||||
|
"Challenges", foreign_keys="Submissions.challenge_id", lazy="select"
|
||||||
|
)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_on": type}
|
||||||
'polymorphic_on': type,
|
|
||||||
}
|
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def account_id(self):
|
def account_id(self):
|
||||||
user_mode = get_config('user_mode')
|
user_mode = get_config("user_mode")
|
||||||
if user_mode == 'teams':
|
if user_mode == "teams":
|
||||||
return self.team_id
|
return self.team_id
|
||||||
elif user_mode == 'users':
|
elif user_mode == "users":
|
||||||
return self.user_id
|
return self.user_id
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def account(self):
|
def account(self):
|
||||||
user_mode = get_config('user_mode')
|
user_mode = get_config("user_mode")
|
||||||
if user_mode == 'teams':
|
if user_mode == "teams":
|
||||||
return self.team
|
return self.team
|
||||||
elif user_mode == 'users':
|
elif user_mode == "users":
|
||||||
return self.user
|
return self.user
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -647,91 +649,95 @@ class Submissions(db.Model):
|
||||||
return child_classes[type]
|
return child_classes[type]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Submission {}, {}, {}, {}>'.format(self.team_id, self.challenge_id, self.ip, self.provided)
|
return "<Submission {}, {}, {}, {}>".format(
|
||||||
|
self.team_id, self.challenge_id, self.ip, self.provided
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Solves(Submissions):
|
class Solves(Submissions):
|
||||||
__tablename__ = 'solves'
|
__tablename__ = "solves"
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint('challenge_id', 'user_id'),
|
db.UniqueConstraint("challenge_id", "user_id"),
|
||||||
db.UniqueConstraint('challenge_id', 'team_id'),
|
db.UniqueConstraint("challenge_id", "team_id"),
|
||||||
{}
|
{},
|
||||||
|
)
|
||||||
|
id = db.Column(
|
||||||
|
None, db.ForeignKey("submissions.id", ondelete="CASCADE"), primary_key=True
|
||||||
|
)
|
||||||
|
challenge_id = column_property(
|
||||||
|
db.Column(db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")),
|
||||||
|
Submissions.challenge_id,
|
||||||
|
)
|
||||||
|
user_id = column_property(
|
||||||
|
db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")),
|
||||||
|
Submissions.user_id,
|
||||||
|
)
|
||||||
|
team_id = column_property(
|
||||||
|
db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")),
|
||||||
|
Submissions.team_id,
|
||||||
)
|
)
|
||||||
id = db.Column(None, db.ForeignKey('submissions.id', ondelete='CASCADE'), primary_key=True)
|
|
||||||
challenge_id = column_property(db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')),
|
|
||||||
Submissions.challenge_id)
|
|
||||||
user_id = column_property(db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')), Submissions.user_id)
|
|
||||||
team_id = column_property(db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')), Submissions.team_id)
|
|
||||||
|
|
||||||
user = db.relationship('Users', foreign_keys="Solves.user_id", lazy='select')
|
user = db.relationship("Users", foreign_keys="Solves.user_id", lazy="select")
|
||||||
team = db.relationship('Teams', foreign_keys="Solves.team_id", lazy='select')
|
team = db.relationship("Teams", foreign_keys="Solves.team_id", lazy="select")
|
||||||
challenge = db.relationship('Challenges', foreign_keys="Solves.challenge_id", lazy='select')
|
challenge = db.relationship(
|
||||||
|
"Challenges", foreign_keys="Solves.challenge_id", lazy="select"
|
||||||
|
)
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "correct"}
|
||||||
'polymorphic_identity': 'correct'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Fails(Submissions):
|
class Fails(Submissions):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "incorrect"}
|
||||||
'polymorphic_identity': 'incorrect'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Unlocks(db.Model):
|
class Unlocks(db.Model):
|
||||||
__tablename__ = 'unlocks'
|
__tablename__ = "unlocks"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
|
||||||
target = db.Column(db.Integer)
|
target = db.Column(db.Integer)
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
type = db.Column(db.String(32))
|
type = db.Column(db.String(32))
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_on": type}
|
||||||
'polymorphic_on': type,
|
|
||||||
}
|
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def account_id(self):
|
def account_id(self):
|
||||||
user_mode = get_config('user_mode')
|
user_mode = get_config("user_mode")
|
||||||
if user_mode == 'teams':
|
if user_mode == "teams":
|
||||||
return self.team_id
|
return self.team_id
|
||||||
elif user_mode == 'users':
|
elif user_mode == "users":
|
||||||
return self.user_id
|
return self.user_id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Unlock %r>' % self.id
|
return "<Unlock %r>" % self.id
|
||||||
|
|
||||||
|
|
||||||
class HintUnlocks(Unlocks):
|
class HintUnlocks(Unlocks):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_identity": "hints"}
|
||||||
'polymorphic_identity': 'hints'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Tracking(db.Model):
|
class Tracking(db.Model):
|
||||||
__tablename__ = 'tracking'
|
__tablename__ = "tracking"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
type = db.Column(db.String(32))
|
type = db.Column(db.String(32))
|
||||||
ip = db.Column(db.String(46))
|
ip = db.Column(db.String(46))
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
user = db.relationship('Users', foreign_keys="Tracking.user_id", lazy='select')
|
user = db.relationship("Users", foreign_keys="Tracking.user_id", lazy="select")
|
||||||
|
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {"polymorphic_on": type}
|
||||||
'polymorphic_on': type,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Tracking, self).__init__(**kwargs)
|
super(Tracking, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Tracking %r>' % self.ip
|
return "<Tracking %r>" % self.ip
|
||||||
|
|
||||||
|
|
||||||
class Configs(db.Model):
|
class Configs(db.Model):
|
||||||
__tablename__ = 'config'
|
__tablename__ = "config"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
key = db.Column(db.Text)
|
key = db.Column(db.Text)
|
||||||
value = db.Column(db.Text)
|
value = db.Column(db.Text)
|
||||||
|
@ -751,9 +757,9 @@ def get_config(key):
|
||||||
if value and value.isdigit():
|
if value and value.isdigit():
|
||||||
return int(value)
|
return int(value)
|
||||||
elif value and isinstance(value, six.string_types):
|
elif value and isinstance(value, six.string_types):
|
||||||
if value.lower() == 'true':
|
if value.lower() == "true":
|
||||||
return True
|
return True
|
||||||
elif value.lower() == 'false':
|
elif value.lower() == "false":
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -10,12 +10,12 @@ from CTFd.utils.plugins import (
|
||||||
register_script as utils_register_plugin_script,
|
register_script as utils_register_plugin_script,
|
||||||
register_stylesheet as utils_register_plugin_stylesheet,
|
register_stylesheet as utils_register_plugin_stylesheet,
|
||||||
register_admin_script as utils_register_admin_plugin_script,
|
register_admin_script as utils_register_admin_plugin_script,
|
||||||
register_admin_stylesheet as utils_register_admin_plugin_stylesheet
|
register_admin_stylesheet as utils_register_admin_plugin_stylesheet,
|
||||||
)
|
)
|
||||||
from CTFd.utils.config.pages import get_pages
|
from CTFd.utils.config.pages import get_pages
|
||||||
|
|
||||||
|
|
||||||
Menu = namedtuple('Menu', ['title', 'route'])
|
Menu = namedtuple("Menu", ["title", "route"])
|
||||||
|
|
||||||
|
|
||||||
def register_plugin_assets_directory(app, base_path, admins_only=False):
|
def register_plugin_assets_directory(app, base_path, admins_only=False):
|
||||||
|
@ -27,12 +27,12 @@ def register_plugin_assets_directory(app, base_path, admins_only=False):
|
||||||
:param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public
|
:param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
base_path = base_path.strip('/')
|
base_path = base_path.strip("/")
|
||||||
|
|
||||||
def assets_handler(path):
|
def assets_handler(path):
|
||||||
return send_from_directory(base_path, path)
|
return send_from_directory(base_path, path)
|
||||||
|
|
||||||
rule = '/' + base_path + '/<path:path>'
|
rule = "/" + base_path + "/<path:path>"
|
||||||
app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler)
|
app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,14 +45,14 @@ def register_plugin_asset(app, asset_path, admins_only=False):
|
||||||
:param boolean admins_only: Whether or not this file should be accessible to the public
|
:param boolean admins_only: Whether or not this file should be accessible to the public
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
asset_path = asset_path.strip('/')
|
asset_path = asset_path.strip("/")
|
||||||
|
|
||||||
def asset_handler():
|
def asset_handler():
|
||||||
return send_file(asset_path)
|
return send_file(asset_path)
|
||||||
|
|
||||||
if admins_only:
|
if admins_only:
|
||||||
asset_handler = admins_only_wrapper(asset_handler)
|
asset_handler = admins_only_wrapper(asset_handler)
|
||||||
rule = '/' + asset_path
|
rule = "/" + asset_path
|
||||||
app.add_url_rule(rule=rule, endpoint=asset_path, view_func=asset_handler)
|
app.add_url_rule(rule=rule, endpoint=asset_path, view_func=asset_handler)
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,14 +170,14 @@ def init_plugins(app):
|
||||||
app.admin_plugin_menu_bar = []
|
app.admin_plugin_menu_bar = []
|
||||||
app.plugin_menu_bar = []
|
app.plugin_menu_bar = []
|
||||||
|
|
||||||
if app.config.get('SAFE_MODE', False) is False:
|
if app.config.get("SAFE_MODE", False) is False:
|
||||||
modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
|
modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
|
||||||
blacklist = {'__pycache__'}
|
blacklist = {"__pycache__"}
|
||||||
for module in modules:
|
for module in modules:
|
||||||
module_name = os.path.basename(module)
|
module_name = os.path.basename(module)
|
||||||
if os.path.isdir(module) and module_name not in blacklist:
|
if os.path.isdir(module) and module_name not in blacklist:
|
||||||
module = '.' + module_name
|
module = "." + module_name
|
||||||
module = importlib.import_module(module, package='CTFd.plugins')
|
module = importlib.import_module(module, package="CTFd.plugins")
|
||||||
module.load(app)
|
module.load(app)
|
||||||
print(" * Loaded module, %s" % module)
|
print(" * Loaded module, %s" % module)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
from CTFd.plugins import register_plugin_assets_directory
|
from CTFd.plugins import register_plugin_assets_directory
|
||||||
from CTFd.plugins.flags import get_flag_class
|
from CTFd.plugins.flags import get_flag_class
|
||||||
from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints
|
from CTFd.models import (
|
||||||
|
db,
|
||||||
|
Solves,
|
||||||
|
Fails,
|
||||||
|
Flags,
|
||||||
|
Challenges,
|
||||||
|
ChallengeFiles,
|
||||||
|
Tags,
|
||||||
|
Hints,
|
||||||
|
)
|
||||||
from CTFd.utils.user import get_ip
|
from CTFd.utils.user import get_ip
|
||||||
from CTFd.utils.uploads import delete_file
|
from CTFd.utils.uploads import delete_file
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
@ -17,19 +26,21 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
id = "standard" # Unique identifier used to register challenges
|
id = "standard" # Unique identifier used to register challenges
|
||||||
name = "standard" # Name of a challenge type
|
name = "standard" # Name of a challenge type
|
||||||
templates = { # Templates used for each aspect of challenge editing & viewing
|
templates = { # Templates used for each aspect of challenge editing & viewing
|
||||||
'create': '/plugins/challenges/assets/create.html',
|
"create": "/plugins/challenges/assets/create.html",
|
||||||
'update': '/plugins/challenges/assets/update.html',
|
"update": "/plugins/challenges/assets/update.html",
|
||||||
'view': '/plugins/challenges/assets/view.html',
|
"view": "/plugins/challenges/assets/view.html",
|
||||||
}
|
}
|
||||||
scripts = { # Scripts that are loaded when a template is loaded
|
scripts = { # Scripts that are loaded when a template is loaded
|
||||||
'create': '/plugins/challenges/assets/create.js',
|
"create": "/plugins/challenges/assets/create.js",
|
||||||
'update': '/plugins/challenges/assets/update.js',
|
"update": "/plugins/challenges/assets/update.js",
|
||||||
'view': '/plugins/challenges/assets/view.js',
|
"view": "/plugins/challenges/assets/view.js",
|
||||||
}
|
}
|
||||||
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
||||||
route = '/plugins/challenges/assets/'
|
route = "/plugins/challenges/assets/"
|
||||||
# Blueprint used to access the static_folder directory.
|
# Blueprint used to access the static_folder directory.
|
||||||
blueprint = Blueprint('standard', __name__, template_folder='templates', static_folder='assets')
|
blueprint = Blueprint(
|
||||||
|
"standard", __name__, template_folder="templates", static_folder="assets"
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(request):
|
def create(request):
|
||||||
|
@ -57,20 +68,20 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
:return: Challenge object, data dictionary to be returned to the user
|
:return: Challenge object, data dictionary to be returned to the user
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
'id': challenge.id,
|
"id": challenge.id,
|
||||||
'name': challenge.name,
|
"name": challenge.name,
|
||||||
'value': challenge.value,
|
"value": challenge.value,
|
||||||
'description': challenge.description,
|
"description": challenge.description,
|
||||||
'category': challenge.category,
|
"category": challenge.category,
|
||||||
'state': challenge.state,
|
"state": challenge.state,
|
||||||
'max_attempts': challenge.max_attempts,
|
"max_attempts": challenge.max_attempts,
|
||||||
'type': challenge.type,
|
"type": challenge.type,
|
||||||
'type_data': {
|
"type_data": {
|
||||||
'id': CTFdStandardChallenge.id,
|
"id": CTFdStandardChallenge.id,
|
||||||
'name': CTFdStandardChallenge.name,
|
"name": CTFdStandardChallenge.name,
|
||||||
'templates': CTFdStandardChallenge.templates,
|
"templates": CTFdStandardChallenge.templates,
|
||||||
'scripts': CTFdStandardChallenge.scripts,
|
"scripts": CTFdStandardChallenge.scripts,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -123,12 +134,12 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
:return: (boolean, string)
|
:return: (boolean, string)
|
||||||
"""
|
"""
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
if get_flag_class(flag.type).compare(flag, submission):
|
if get_flag_class(flag.type).compare(flag, submission):
|
||||||
return True, 'Correct'
|
return True, "Correct"
|
||||||
return False, 'Incorrect'
|
return False, "Incorrect"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def solve(user, team, challenge, request):
|
def solve(user, team, challenge, request):
|
||||||
|
@ -141,13 +152,13 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
solve = Solves(
|
solve = Solves(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
team_id=team.id if team else None,
|
team_id=team.id if team else None,
|
||||||
challenge_id=challenge.id,
|
challenge_id=challenge.id,
|
||||||
ip=get_ip(req=request),
|
ip=get_ip(req=request),
|
||||||
provided=submission
|
provided=submission,
|
||||||
)
|
)
|
||||||
db.session.add(solve)
|
db.session.add(solve)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -164,13 +175,13 @@ class CTFdStandardChallenge(BaseChallenge):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
wrong = Fails(
|
wrong = Fails(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
team_id=team.id if team else None,
|
team_id=team.id if team else None,
|
||||||
challenge_id=challenge.id,
|
challenge_id=challenge.id,
|
||||||
ip=get_ip(request),
|
ip=get_ip(request),
|
||||||
provided=submission
|
provided=submission,
|
||||||
)
|
)
|
||||||
db.session.add(wrong)
|
db.session.add(wrong)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -194,10 +205,8 @@ def get_chal_class(class_id):
|
||||||
Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register
|
Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register
|
||||||
your Challenge Type.
|
your Challenge Type.
|
||||||
"""
|
"""
|
||||||
CHALLENGE_CLASSES = {
|
CHALLENGE_CLASSES = {"standard": CTFdStandardChallenge}
|
||||||
"standard": CTFdStandardChallenge
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def load(app):
|
def load(app):
|
||||||
register_plugin_assets_directory(app, base_path='/plugins/challenges/assets/')
|
register_plugin_assets_directory(app, base_path="/plugins/challenges/assets/")
|
||||||
|
|
|
@ -2,7 +2,16 @@ from __future__ import division # Use floating point for math calculations
|
||||||
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
|
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
|
||||||
from CTFd.plugins import register_plugin_assets_directory
|
from CTFd.plugins import register_plugin_assets_directory
|
||||||
from CTFd.plugins.flags import get_flag_class
|
from CTFd.plugins.flags import get_flag_class
|
||||||
from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints
|
from CTFd.models import (
|
||||||
|
db,
|
||||||
|
Solves,
|
||||||
|
Fails,
|
||||||
|
Flags,
|
||||||
|
Challenges,
|
||||||
|
ChallengeFiles,
|
||||||
|
Tags,
|
||||||
|
Hints,
|
||||||
|
)
|
||||||
from CTFd.utils.user import get_ip
|
from CTFd.utils.user import get_ip
|
||||||
from CTFd.utils.uploads import delete_file
|
from CTFd.utils.uploads import delete_file
|
||||||
from CTFd.utils.modes import get_model
|
from CTFd.utils.modes import get_model
|
||||||
|
@ -14,19 +23,24 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
id = "dynamic" # Unique identifier used to register challenges
|
id = "dynamic" # Unique identifier used to register challenges
|
||||||
name = "dynamic" # Name of a challenge type
|
name = "dynamic" # Name of a challenge type
|
||||||
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
|
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
|
||||||
'create': '/plugins/dynamic_challenges/assets/create.html',
|
"create": "/plugins/dynamic_challenges/assets/create.html",
|
||||||
'update': '/plugins/dynamic_challenges/assets/update.html',
|
"update": "/plugins/dynamic_challenges/assets/update.html",
|
||||||
'view': '/plugins/dynamic_challenges/assets/view.html',
|
"view": "/plugins/dynamic_challenges/assets/view.html",
|
||||||
}
|
}
|
||||||
scripts = { # Scripts that are loaded when a template is loaded
|
scripts = { # Scripts that are loaded when a template is loaded
|
||||||
'create': '/plugins/dynamic_challenges/assets/create.js',
|
"create": "/plugins/dynamic_challenges/assets/create.js",
|
||||||
'update': '/plugins/dynamic_challenges/assets/update.js',
|
"update": "/plugins/dynamic_challenges/assets/update.js",
|
||||||
'view': '/plugins/dynamic_challenges/assets/view.js',
|
"view": "/plugins/dynamic_challenges/assets/view.js",
|
||||||
}
|
}
|
||||||
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
||||||
route = '/plugins/dynamic_challenges/assets/'
|
route = "/plugins/dynamic_challenges/assets/"
|
||||||
# Blueprint used to access the static_folder directory.
|
# Blueprint used to access the static_folder directory.
|
||||||
blueprint = Blueprint('dynamic_challenges', __name__, template_folder='templates', static_folder='assets')
|
blueprint = Blueprint(
|
||||||
|
"dynamic_challenges",
|
||||||
|
__name__,
|
||||||
|
template_folder="templates",
|
||||||
|
static_folder="assets",
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(request):
|
def create(request):
|
||||||
|
@ -54,23 +68,23 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
"""
|
"""
|
||||||
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
||||||
data = {
|
data = {
|
||||||
'id': challenge.id,
|
"id": challenge.id,
|
||||||
'name': challenge.name,
|
"name": challenge.name,
|
||||||
'value': challenge.value,
|
"value": challenge.value,
|
||||||
'initial': challenge.initial,
|
"initial": challenge.initial,
|
||||||
'decay': challenge.decay,
|
"decay": challenge.decay,
|
||||||
'minimum': challenge.minimum,
|
"minimum": challenge.minimum,
|
||||||
'description': challenge.description,
|
"description": challenge.description,
|
||||||
'category': challenge.category,
|
"category": challenge.category,
|
||||||
'state': challenge.state,
|
"state": challenge.state,
|
||||||
'max_attempts': challenge.max_attempts,
|
"max_attempts": challenge.max_attempts,
|
||||||
'type': challenge.type,
|
"type": challenge.type,
|
||||||
'type_data': {
|
"type_data": {
|
||||||
'id': DynamicValueChallenge.id,
|
"id": DynamicValueChallenge.id,
|
||||||
'name': DynamicValueChallenge.name,
|
"name": DynamicValueChallenge.name,
|
||||||
'templates': DynamicValueChallenge.templates,
|
"templates": DynamicValueChallenge.templates,
|
||||||
'scripts': DynamicValueChallenge.scripts,
|
"scripts": DynamicValueChallenge.scripts,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -88,20 +102,28 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
|
|
||||||
for attr, value in data.items():
|
for attr, value in data.items():
|
||||||
# We need to set these to floats so that the next operations don't operate on strings
|
# We need to set these to floats so that the next operations don't operate on strings
|
||||||
if attr in ('initial', 'minimum', 'decay'):
|
if attr in ("initial", "minimum", "decay"):
|
||||||
value = float(value)
|
value = float(value)
|
||||||
setattr(challenge, attr, value)
|
setattr(challenge, attr, value)
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
solve_count = Solves.query \
|
solve_count = (
|
||||||
.join(Model, Solves.account_id == Model.id) \
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \
|
.filter(
|
||||||
|
Solves.challenge_id == challenge.id,
|
||||||
|
Model.hidden == False,
|
||||||
|
Model.banned == False,
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
# It is important that this calculation takes into account floats.
|
# It is important that this calculation takes into account floats.
|
||||||
# Hence this file uses from __future__ import division
|
# Hence this file uses from __future__ import division
|
||||||
value = (((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) * (solve_count ** 2)) + challenge.initial
|
value = (
|
||||||
|
((challenge.minimum - challenge.initial) / (challenge.decay ** 2))
|
||||||
|
* (solve_count ** 2)
|
||||||
|
) + challenge.initial
|
||||||
|
|
||||||
value = math.ceil(value)
|
value = math.ceil(value)
|
||||||
|
|
||||||
|
@ -146,12 +168,12 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
:return: (boolean, string)
|
:return: (boolean, string)
|
||||||
"""
|
"""
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
if get_flag_class(flag.type).compare(flag, submission):
|
if get_flag_class(flag.type).compare(flag, submission):
|
||||||
return True, 'Correct'
|
return True, "Correct"
|
||||||
return False, 'Incorrect'
|
return False, "Incorrect"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def solve(user, team, challenge, request):
|
def solve(user, team, challenge, request):
|
||||||
|
@ -165,7 +187,7 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
"""
|
"""
|
||||||
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
|
|
||||||
Model = get_model()
|
Model = get_model()
|
||||||
|
|
||||||
|
@ -174,14 +196,19 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
team_id=team.id if team else None,
|
team_id=team.id if team else None,
|
||||||
challenge_id=challenge.id,
|
challenge_id=challenge.id,
|
||||||
ip=get_ip(req=request),
|
ip=get_ip(req=request),
|
||||||
provided=submission
|
provided=submission,
|
||||||
)
|
)
|
||||||
db.session.add(solve)
|
db.session.add(solve)
|
||||||
|
|
||||||
solve_count = Solves.query \
|
solve_count = (
|
||||||
.join(Model, Solves.account_id == Model.id) \
|
Solves.query.join(Model, Solves.account_id == Model.id)
|
||||||
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \
|
.filter(
|
||||||
|
Solves.challenge_id == challenge.id,
|
||||||
|
Model.hidden == False,
|
||||||
|
Model.banned == False,
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
# We subtract -1 to allow the first solver to get max point value
|
# We subtract -1 to allow the first solver to get max point value
|
||||||
solve_count -= 1
|
solve_count -= 1
|
||||||
|
@ -189,9 +216,7 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
# It is important that this calculation takes into account floats.
|
# It is important that this calculation takes into account floats.
|
||||||
# Hence this file uses from __future__ import division
|
# Hence this file uses from __future__ import division
|
||||||
value = (
|
value = (
|
||||||
(
|
((chal.minimum - chal.initial) / (chal.decay ** 2)) * (solve_count ** 2)
|
||||||
(chal.minimum - chal.initial) / (chal.decay**2)
|
|
||||||
) * (solve_count**2)
|
|
||||||
) + chal.initial
|
) + chal.initial
|
||||||
|
|
||||||
value = math.ceil(value)
|
value = math.ceil(value)
|
||||||
|
@ -215,13 +240,13 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
data = request.form or request.get_json()
|
data = request.form or request.get_json()
|
||||||
submission = data['submission'].strip()
|
submission = data["submission"].strip()
|
||||||
wrong = Fails(
|
wrong = Fails(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
team_id=team.id if team else None,
|
team_id=team.id if team else None,
|
||||||
challenge_id=challenge.id,
|
challenge_id=challenge.id,
|
||||||
ip=get_ip(request),
|
ip=get_ip(request),
|
||||||
provided=submission
|
provided=submission,
|
||||||
)
|
)
|
||||||
db.session.add(wrong)
|
db.session.add(wrong)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -229,19 +254,21 @@ class DynamicValueChallenge(BaseChallenge):
|
||||||
|
|
||||||
|
|
||||||
class DynamicChallenge(Challenges):
|
class DynamicChallenge(Challenges):
|
||||||
__mapper_args__ = {'polymorphic_identity': 'dynamic'}
|
__mapper_args__ = {"polymorphic_identity": "dynamic"}
|
||||||
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True)
|
id = db.Column(None, db.ForeignKey("challenges.id"), primary_key=True)
|
||||||
initial = db.Column(db.Integer, default=0)
|
initial = db.Column(db.Integer, default=0)
|
||||||
minimum = db.Column(db.Integer, default=0)
|
minimum = db.Column(db.Integer, default=0)
|
||||||
decay = db.Column(db.Integer, default=0)
|
decay = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DynamicChallenge, self).__init__(**kwargs)
|
super(DynamicChallenge, self).__init__(**kwargs)
|
||||||
self.initial = kwargs['value']
|
self.initial = kwargs["value"]
|
||||||
|
|
||||||
|
|
||||||
def load(app):
|
def load(app):
|
||||||
# upgrade()
|
# upgrade()
|
||||||
app.db.create_all()
|
app.db.create_all()
|
||||||
CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge
|
CHALLENGE_CLASSES["dynamic"] = DynamicValueChallenge
|
||||||
register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/')
|
register_plugin_assets_directory(
|
||||||
|
app, base_path="/plugins/dynamic_challenges/assets/"
|
||||||
|
)
|
||||||
|
|
|
@ -15,8 +15,8 @@ class BaseFlag(object):
|
||||||
class CTFdStaticFlag(BaseFlag):
|
class CTFdStaticFlag(BaseFlag):
|
||||||
name = "static"
|
name = "static"
|
||||||
templates = { # Nunjucks templates used for key editing & viewing
|
templates = { # Nunjucks templates used for key editing & viewing
|
||||||
'create': '/plugins/flags/assets/static/create.html',
|
"create": "/plugins/flags/assets/static/create.html",
|
||||||
'update': '/plugins/flags/assets/static/edit.html',
|
"update": "/plugins/flags/assets/static/edit.html",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -40,8 +40,8 @@ class CTFdStaticFlag(BaseFlag):
|
||||||
class CTFdRegexFlag(BaseFlag):
|
class CTFdRegexFlag(BaseFlag):
|
||||||
name = "regex"
|
name = "regex"
|
||||||
templates = { # Nunjucks templates used for key editing & viewing
|
templates = { # Nunjucks templates used for key editing & viewing
|
||||||
'create': '/plugins/flags/assets/regex/create.html',
|
"create": "/plugins/flags/assets/regex/create.html",
|
||||||
'update': '/plugins/flags/assets/regex/edit.html',
|
"update": "/plugins/flags/assets/regex/edit.html",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -57,10 +57,7 @@ class CTFdRegexFlag(BaseFlag):
|
||||||
return res and res.group() == provided
|
return res and res.group() == provided
|
||||||
|
|
||||||
|
|
||||||
FLAG_CLASSES = {
|
FLAG_CLASSES = {"static": CTFdStaticFlag, "regex": CTFdRegexFlag}
|
||||||
'static': CTFdStaticFlag,
|
|
||||||
'regex': CTFdRegexFlag
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_flag_class(class_id):
|
def get_flag_class(class_id):
|
||||||
|
@ -71,4 +68,4 @@ def get_flag_class(class_id):
|
||||||
|
|
||||||
|
|
||||||
def load(app):
|
def load(app):
|
||||||
register_plugin_assets_directory(app, base_path='/plugins/flags/assets/')
|
register_plugin_assets_directory(app, base_path="/plugins/flags/assets/")
|
||||||
|
|
|
@ -6,43 +6,43 @@ class AwardSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Awards
|
model = Awards
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'date')
|
dump_only = ("id", "date")
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'admin': [
|
"admin": [
|
||||||
'category',
|
"category",
|
||||||
'user_id',
|
"user_id",
|
||||||
'name',
|
"name",
|
||||||
'description',
|
"description",
|
||||||
'value',
|
"value",
|
||||||
'team_id',
|
"team_id",
|
||||||
'user',
|
"user",
|
||||||
'team',
|
"team",
|
||||||
'date',
|
"date",
|
||||||
'requirements',
|
"requirements",
|
||||||
'id',
|
"id",
|
||||||
'icon'
|
"icon",
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
"category",
|
||||||
|
"user_id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"value",
|
||||||
|
"team_id",
|
||||||
|
"user",
|
||||||
|
"team",
|
||||||
|
"date",
|
||||||
|
"id",
|
||||||
|
"icon",
|
||||||
],
|
],
|
||||||
'user': [
|
|
||||||
'category',
|
|
||||||
'user_id',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'value',
|
|
||||||
'team_id',
|
|
||||||
'user',
|
|
||||||
'team',
|
|
||||||
'date',
|
|
||||||
'id',
|
|
||||||
'icon'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(AwardSchema, self).__init__(*args, **kwargs)
|
super(AwardSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -5,4 +5,4 @@ class ChallengeSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Challenges
|
model = Challenges
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id',)
|
dump_only = ("id",)
|
||||||
|
|
|
@ -6,21 +6,15 @@ class ConfigSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Configs
|
model = Configs
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id',)
|
dump_only = ("id",)
|
||||||
|
|
||||||
views = {
|
views = {"admin": ["id", "key", "value"]}
|
||||||
'admin': [
|
|
||||||
'id',
|
|
||||||
'key',
|
|
||||||
'value'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(ConfigSchema, self).__init__(*args, **kwargs)
|
super(ConfigSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,13 +6,13 @@ class FileSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Files
|
model = Files
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'type', 'location')
|
dump_only = ("id", "type", "location")
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(FileSchema, self).__init__(*args, **kwargs)
|
super(FileSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,13 +6,13 @@ class FlagSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Flags
|
model = Flags
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id',)
|
dump_only = ("id",)
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(FlagSchema, self).__init__(*args, **kwargs)
|
super(FlagSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,37 +6,19 @@ class HintSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Hints
|
model = Hints
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'type')
|
dump_only = ("id", "type")
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'locked': [
|
"locked": ["id", "type", "challenge", "cost"],
|
||||||
'id',
|
"unlocked": ["id", "type", "challenge", "content", "cost"],
|
||||||
'type',
|
"admin": ["id", "type", "challenge", "content", "cost", "requirements"],
|
||||||
'challenge',
|
|
||||||
'cost'
|
|
||||||
],
|
|
||||||
'unlocked': [
|
|
||||||
'id',
|
|
||||||
'type',
|
|
||||||
'challenge',
|
|
||||||
'content',
|
|
||||||
'cost'
|
|
||||||
],
|
|
||||||
'admin': [
|
|
||||||
'id',
|
|
||||||
'type',
|
|
||||||
'challenge',
|
|
||||||
'content',
|
|
||||||
'cost',
|
|
||||||
'requirements'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(HintSchema, self).__init__(*args, **kwargs)
|
super(HintSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,13 +6,13 @@ class NotificationSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Notifications
|
model = Notifications
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'date')
|
dump_only = ("id", "date")
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(NotificationSchema, self).__init__(*args, **kwargs)
|
super(NotificationSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -7,19 +7,19 @@ class PageSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pages
|
model = Pages
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', )
|
dump_only = ("id",)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_route(self, data):
|
def validate_route(self, data):
|
||||||
route = data.get('route')
|
route = data.get("route")
|
||||||
if route and route.startswith('/'):
|
if route and route.startswith("/"):
|
||||||
data['route'] = route.strip('/')
|
data["route"] = route.strip("/")
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(PageSchema, self).__init__(*args, **kwargs)
|
super(PageSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -5,41 +5,33 @@ from CTFd.utils import string_types
|
||||||
|
|
||||||
|
|
||||||
class SubmissionSchema(ma.ModelSchema):
|
class SubmissionSchema(ma.ModelSchema):
|
||||||
challenge = fields.Nested(ChallengeSchema, only=['name', 'category', 'value'])
|
challenge = fields.Nested(ChallengeSchema, only=["name", "category", "value"])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Submissions
|
model = Submissions
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', )
|
dump_only = ("id",)
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'admin': [
|
"admin": [
|
||||||
'provided',
|
"provided",
|
||||||
'ip',
|
"ip",
|
||||||
'challenge_id',
|
"challenge_id",
|
||||||
'challenge',
|
"challenge",
|
||||||
'user',
|
"user",
|
||||||
'team',
|
"team",
|
||||||
'date',
|
"date",
|
||||||
'type',
|
"type",
|
||||||
'id'
|
"id",
|
||||||
],
|
],
|
||||||
'user': [
|
"user": ["challenge_id", "challenge", "user", "team", "date", "type", "id"],
|
||||||
'challenge_id',
|
|
||||||
'challenge',
|
|
||||||
'user',
|
|
||||||
'team',
|
|
||||||
'date',
|
|
||||||
'type',
|
|
||||||
'id'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(SubmissionSchema, self).__init__(*args, **kwargs)
|
super(SubmissionSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,24 +6,15 @@ class TagSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Tags
|
model = Tags
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id',)
|
dump_only = ("id",)
|
||||||
|
|
||||||
views = {
|
views = {"admin": ["id", "challenge", "value"], "user": ["value"]}
|
||||||
'admin': [
|
|
||||||
'id',
|
|
||||||
'challenge',
|
|
||||||
'value'
|
|
||||||
],
|
|
||||||
'user': [
|
|
||||||
'value'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(TagSchema, self).__init__(*args, **kwargs)
|
super(TagSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -12,44 +12,40 @@ class TeamSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Teams
|
model = Teams
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'oauth_id', 'created', 'members')
|
dump_only = ("id", "oauth_id", "created", "members")
|
||||||
load_only = ('password',)
|
load_only = ("password",)
|
||||||
|
|
||||||
name = field_for(
|
name = field_for(
|
||||||
Teams,
|
Teams,
|
||||||
'name',
|
"name",
|
||||||
required=True,
|
required=True,
|
||||||
validate=[
|
validate=[
|
||||||
validate.Length(min=1, max=128, error='Team names must not be empty')
|
validate.Length(min=1, max=128, error="Team names must not be empty")
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
email = field_for(
|
email = field_for(
|
||||||
Teams,
|
Teams,
|
||||||
'email',
|
"email",
|
||||||
validate=validate.Email('Emails must be a properly formatted email address')
|
validate=validate.Email("Emails must be a properly formatted email address"),
|
||||||
)
|
)
|
||||||
website = field_for(
|
website = field_for(
|
||||||
Teams,
|
Teams,
|
||||||
'website',
|
"website",
|
||||||
validate=[
|
validate=[
|
||||||
# This is a dirty hack to let website accept empty strings so you can remove your website
|
# This is a dirty hack to let website accept empty strings so you can remove your website
|
||||||
lambda website: validate.URL(
|
lambda website: validate.URL(
|
||||||
error='Websites must be a proper URL starting with http or https',
|
error="Websites must be a proper URL starting with http or https",
|
||||||
schemes={'http', 'https'}
|
schemes={"http", "https"},
|
||||||
)(website) if website else True
|
)(website)
|
||||||
]
|
if website
|
||||||
)
|
else True
|
||||||
country = field_for(
|
],
|
||||||
Teams,
|
|
||||||
'country',
|
|
||||||
validate=[
|
|
||||||
validate_country_code
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
country = field_for(Teams, "country", validate=[validate_country_code])
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_name(self, data):
|
def validate_name(self, data):
|
||||||
name = data.get('name')
|
name = data.get("name")
|
||||||
if name is None:
|
if name is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -57,57 +53,73 @@ class TeamSchema(ma.ModelSchema):
|
||||||
current_team = get_current_team()
|
current_team = get_current_team()
|
||||||
# Admins should be able to patch anyone but they cannot cause a collision.
|
# Admins should be able to patch anyone but they cannot cause a collision.
|
||||||
if is_admin():
|
if is_admin():
|
||||||
team_id = int(data.get('id', 0))
|
team_id = int(data.get("id", 0))
|
||||||
if team_id:
|
if team_id:
|
||||||
if existing_team and existing_team.id != team_id:
|
if existing_team and existing_team.id != team_id:
|
||||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Team name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# If there's no Team ID it means that the admin is creating a team with no ID.
|
# If there's no Team ID it means that the admin is creating a team with no ID.
|
||||||
if existing_team:
|
if existing_team:
|
||||||
if current_team:
|
if current_team:
|
||||||
if current_team.id != existing_team.id:
|
if current_team.id != existing_team.id:
|
||||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Team name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Team name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# We need to allow teams to edit themselves and allow the "conflict"
|
# We need to allow teams to edit themselves and allow the "conflict"
|
||||||
if data['name'] == current_team.name:
|
if data["name"] == current_team.name:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
name_changes = get_config('name_changes', default=True)
|
name_changes = get_config("name_changes", default=True)
|
||||||
if bool(name_changes) is False:
|
if bool(name_changes) is False:
|
||||||
raise ValidationError('Name changes are disabled', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Name changes are disabled", field_names=["name"]
|
||||||
|
)
|
||||||
|
|
||||||
if existing_team:
|
if existing_team:
|
||||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Team name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_email(self, data):
|
def validate_email(self, data):
|
||||||
email = data.get('email')
|
email = data.get("email")
|
||||||
if email is None:
|
if email is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_team = Teams.query.filter_by(email=email).first()
|
existing_team = Teams.query.filter_by(email=email).first()
|
||||||
if is_admin():
|
if is_admin():
|
||||||
team_id = data.get('id')
|
team_id = data.get("id")
|
||||||
if team_id:
|
if team_id:
|
||||||
if existing_team and existing_team.id != team_id:
|
if existing_team and existing_team.id != team_id:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if existing_team:
|
if existing_team:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
current_team = get_current_team()
|
current_team = get_current_team()
|
||||||
if email == current_team.email:
|
if email == current_team.email:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
if existing_team:
|
if existing_team:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_password_confirmation(self, data):
|
def validate_password_confirmation(self, data):
|
||||||
password = data.get('password')
|
password = data.get("password")
|
||||||
confirm = data.get('confirm')
|
confirm = data.get("confirm")
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
pass
|
pass
|
||||||
|
@ -116,29 +128,38 @@ class TeamSchema(ma.ModelSchema):
|
||||||
current_user = get_current_user()
|
current_user = get_current_user()
|
||||||
|
|
||||||
if current_team.captain_id != current_user.id:
|
if current_team.captain_id != current_user.id:
|
||||||
raise ValidationError('Only the captain can change the team password', field_names=['captain_id'])
|
raise ValidationError(
|
||||||
|
"Only the captain can change the team password",
|
||||||
|
field_names=["captain_id"],
|
||||||
|
)
|
||||||
|
|
||||||
if password and (bool(confirm) is False):
|
if password and (bool(confirm) is False):
|
||||||
raise ValidationError('Please confirm your current password', field_names=['confirm'])
|
raise ValidationError(
|
||||||
|
"Please confirm your current password", field_names=["confirm"]
|
||||||
|
)
|
||||||
|
|
||||||
if password and confirm:
|
if password and confirm:
|
||||||
test = verify_password(plaintext=confirm, ciphertext=current_team.password)
|
test = verify_password(
|
||||||
|
plaintext=confirm, ciphertext=current_team.password
|
||||||
|
)
|
||||||
if test is True:
|
if test is True:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Your previous password is incorrect', field_names=['confirm'])
|
raise ValidationError(
|
||||||
|
"Your previous password is incorrect", field_names=["confirm"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data.pop('password', None)
|
data.pop("password", None)
|
||||||
data.pop('confirm', None)
|
data.pop("confirm", None)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_captain_id(self, data):
|
def validate_captain_id(self, data):
|
||||||
captain_id = data.get('captain_id')
|
captain_id = data.get("captain_id")
|
||||||
if captain_id is None:
|
if captain_id is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
team_id = data.get('id')
|
team_id = data.get("id")
|
||||||
if team_id:
|
if team_id:
|
||||||
target_team = Teams.query.filter_by(id=team_id).first()
|
target_team = Teams.query.filter_by(id=team_id).first()
|
||||||
else:
|
else:
|
||||||
|
@ -147,64 +168,67 @@ class TeamSchema(ma.ModelSchema):
|
||||||
if captain in target_team.members:
|
if captain in target_team.members:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Invalid Captain ID', field_names=['captain_id'])
|
raise ValidationError("Invalid Captain ID", field_names=["captain_id"])
|
||||||
else:
|
else:
|
||||||
current_team = get_current_team()
|
current_team = get_current_team()
|
||||||
current_user = get_current_user()
|
current_user = get_current_user()
|
||||||
if current_team.captain_id == current_user.id:
|
if current_team.captain_id == current_user.id:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Only the captain can change team captain', field_names=['captain_id'])
|
raise ValidationError(
|
||||||
|
"Only the captain can change team captain",
|
||||||
|
field_names=["captain_id"],
|
||||||
|
)
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'user': [
|
"user": [
|
||||||
'website',
|
"website",
|
||||||
'name',
|
"name",
|
||||||
'country',
|
"country",
|
||||||
'affiliation',
|
"affiliation",
|
||||||
'bracket',
|
"bracket",
|
||||||
'members',
|
"members",
|
||||||
'id',
|
"id",
|
||||||
'oauth_id',
|
"oauth_id",
|
||||||
'captain_id',
|
"captain_id",
|
||||||
],
|
],
|
||||||
'self': [
|
"self": [
|
||||||
'website',
|
"website",
|
||||||
'name',
|
"name",
|
||||||
'email',
|
"email",
|
||||||
'country',
|
"country",
|
||||||
'affiliation',
|
"affiliation",
|
||||||
'bracket',
|
"bracket",
|
||||||
'members',
|
"members",
|
||||||
'id',
|
"id",
|
||||||
'oauth_id',
|
"oauth_id",
|
||||||
'password',
|
"password",
|
||||||
'captain_id',
|
"captain_id",
|
||||||
|
],
|
||||||
|
"admin": [
|
||||||
|
"website",
|
||||||
|
"name",
|
||||||
|
"created",
|
||||||
|
"country",
|
||||||
|
"banned",
|
||||||
|
"email",
|
||||||
|
"affiliation",
|
||||||
|
"secret",
|
||||||
|
"bracket",
|
||||||
|
"members",
|
||||||
|
"hidden",
|
||||||
|
"id",
|
||||||
|
"oauth_id",
|
||||||
|
"password",
|
||||||
|
"captain_id",
|
||||||
],
|
],
|
||||||
'admin': [
|
|
||||||
'website',
|
|
||||||
'name',
|
|
||||||
'created',
|
|
||||||
'country',
|
|
||||||
'banned',
|
|
||||||
'email',
|
|
||||||
'affiliation',
|
|
||||||
'secret',
|
|
||||||
'bracket',
|
|
||||||
'members',
|
|
||||||
'hidden',
|
|
||||||
'id',
|
|
||||||
'oauth_id',
|
|
||||||
'password',
|
|
||||||
'captain_id',
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(TeamSchema, self).__init__(*args, **kwargs)
|
super(TeamSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -6,30 +6,18 @@ class UnlockSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Unlocks
|
model = Unlocks
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'date')
|
dump_only = ("id", "date")
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'admin': [
|
"admin": ["user_id", "target", "team_id", "date", "type", "id"],
|
||||||
'user_id',
|
"user": ["target", "date", "type", "id"],
|
||||||
'target',
|
|
||||||
'team_id',
|
|
||||||
'date',
|
|
||||||
'type',
|
|
||||||
'id'
|
|
||||||
],
|
|
||||||
'user': [
|
|
||||||
'target',
|
|
||||||
'date',
|
|
||||||
'type',
|
|
||||||
'id'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(UnlockSchema, self).__init__(*args, **kwargs)
|
super(UnlockSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -13,181 +13,199 @@ class UserSchema(ma.ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Users
|
model = Users
|
||||||
include_fk = True
|
include_fk = True
|
||||||
dump_only = ('id', 'oauth_id', 'created')
|
dump_only = ("id", "oauth_id", "created")
|
||||||
load_only = ('password',)
|
load_only = ("password",)
|
||||||
|
|
||||||
name = field_for(
|
name = field_for(
|
||||||
Users,
|
Users,
|
||||||
'name',
|
"name",
|
||||||
required=True,
|
required=True,
|
||||||
validate=[
|
validate=[
|
||||||
validate.Length(min=1, max=128, error='User names must not be empty')
|
validate.Length(min=1, max=128, error="User names must not be empty")
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
email = field_for(
|
email = field_for(
|
||||||
Users,
|
Users,
|
||||||
'email',
|
"email",
|
||||||
validate=[
|
validate=[
|
||||||
validate.Email('Emails must be a properly formatted email address'),
|
validate.Email("Emails must be a properly formatted email address"),
|
||||||
validate.Length(min=1, max=128, error='Emails must not be empty'),
|
validate.Length(min=1, max=128, error="Emails must not be empty"),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
website = field_for(
|
website = field_for(
|
||||||
Users,
|
Users,
|
||||||
'website',
|
"website",
|
||||||
validate=[
|
validate=[
|
||||||
# This is a dirty hack to let website accept empty strings so you can remove your website
|
# This is a dirty hack to let website accept empty strings so you can remove your website
|
||||||
lambda website: validate.URL(
|
lambda website: validate.URL(
|
||||||
error='Websites must be a proper URL starting with http or https',
|
error="Websites must be a proper URL starting with http or https",
|
||||||
schemes={'http', 'https'}
|
schemes={"http", "https"},
|
||||||
)(website) if website else True
|
)(website)
|
||||||
]
|
if website
|
||||||
)
|
else True
|
||||||
country = field_for(
|
],
|
||||||
Users,
|
|
||||||
'country',
|
|
||||||
validate=[
|
|
||||||
validate_country_code
|
|
||||||
]
|
|
||||||
)
|
|
||||||
password = field_for(
|
|
||||||
Users,
|
|
||||||
'password',
|
|
||||||
)
|
)
|
||||||
|
country = field_for(Users, "country", validate=[validate_country_code])
|
||||||
|
password = field_for(Users, "password")
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_name(self, data):
|
def validate_name(self, data):
|
||||||
name = data.get('name')
|
name = data.get("name")
|
||||||
if name is None:
|
if name is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_user = Users.query.filter_by(name=name).first()
|
existing_user = Users.query.filter_by(name=name).first()
|
||||||
current_user = get_current_user()
|
current_user = get_current_user()
|
||||||
if is_admin():
|
if is_admin():
|
||||||
user_id = data.get('id')
|
user_id = data.get("id")
|
||||||
if user_id:
|
if user_id:
|
||||||
if existing_user and existing_user.id != user_id:
|
if existing_user and existing_user.id != user_id:
|
||||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"User name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if existing_user:
|
if existing_user:
|
||||||
if current_user:
|
if current_user:
|
||||||
if current_user.id != existing_user.id:
|
if current_user.id != existing_user.id:
|
||||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"User name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"User name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if name == current_user.name:
|
if name == current_user.name:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
name_changes = get_config('name_changes', default=True)
|
name_changes = get_config("name_changes", default=True)
|
||||||
if bool(name_changes) is False:
|
if bool(name_changes) is False:
|
||||||
raise ValidationError('Name changes are disabled', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"Name changes are disabled", field_names=["name"]
|
||||||
|
)
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
raise ValidationError(
|
||||||
|
"User name has already been taken", field_names=["name"]
|
||||||
|
)
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_email(self, data):
|
def validate_email(self, data):
|
||||||
email = data.get('email')
|
email = data.get("email")
|
||||||
if email is None:
|
if email is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_user = Users.query.filter_by(email=email).first()
|
existing_user = Users.query.filter_by(email=email).first()
|
||||||
current_user = get_current_user()
|
current_user = get_current_user()
|
||||||
if is_admin():
|
if is_admin():
|
||||||
user_id = data.get('id')
|
user_id = data.get("id")
|
||||||
if user_id:
|
if user_id:
|
||||||
if existing_user and existing_user.id != user_id:
|
if existing_user and existing_user.id != user_id:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if existing_user:
|
if existing_user:
|
||||||
if current_user:
|
if current_user:
|
||||||
if current_user.id != existing_user.id:
|
if current_user.id != existing_user.id:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used",
|
||||||
|
field_names=["email"],
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if email == current_user.email:
|
if email == current_user.email:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
raise ValidationError(
|
||||||
|
"Email address has already been used", field_names=["email"]
|
||||||
|
)
|
||||||
if check_email_is_whitelisted(email) is False:
|
if check_email_is_whitelisted(email) is False:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Only email addresses under {domains} may register".format(
|
"Only email addresses under {domains} may register".format(
|
||||||
domains=get_config('domain_whitelist')
|
domains=get_config("domain_whitelist")
|
||||||
),
|
),
|
||||||
field_names=['email']
|
field_names=["email"],
|
||||||
)
|
)
|
||||||
if get_config('verify_emails'):
|
if get_config("verify_emails"):
|
||||||
current_user.verified = False
|
current_user.verified = False
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def validate_password_confirmation(self, data):
|
def validate_password_confirmation(self, data):
|
||||||
password = data.get('password')
|
password = data.get("password")
|
||||||
confirm = data.get('confirm')
|
confirm = data.get("confirm")
|
||||||
target_user = get_current_user()
|
target_user = get_current_user()
|
||||||
|
|
||||||
if is_admin():
|
if is_admin():
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if password and (bool(confirm) is False):
|
if password and (bool(confirm) is False):
|
||||||
raise ValidationError('Please confirm your current password', field_names=['confirm'])
|
raise ValidationError(
|
||||||
|
"Please confirm your current password", field_names=["confirm"]
|
||||||
|
)
|
||||||
|
|
||||||
if password and confirm:
|
if password and confirm:
|
||||||
test = verify_password(plaintext=confirm, ciphertext=target_user.password)
|
test = verify_password(
|
||||||
|
plaintext=confirm, ciphertext=target_user.password
|
||||||
|
)
|
||||||
if test is True:
|
if test is True:
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
raise ValidationError('Your previous password is incorrect', field_names=['confirm'])
|
raise ValidationError(
|
||||||
|
"Your previous password is incorrect", field_names=["confirm"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
data.pop('password', None)
|
data.pop("password", None)
|
||||||
data.pop('confirm', None)
|
data.pop("confirm", None)
|
||||||
|
|
||||||
views = {
|
views = {
|
||||||
'user': [
|
"user": [
|
||||||
'website',
|
"website",
|
||||||
'name',
|
"name",
|
||||||
'country',
|
"country",
|
||||||
'affiliation',
|
"affiliation",
|
||||||
'bracket',
|
"bracket",
|
||||||
'id',
|
"id",
|
||||||
'oauth_id',
|
"oauth_id",
|
||||||
],
|
],
|
||||||
'self': [
|
"self": [
|
||||||
'website',
|
"website",
|
||||||
'name',
|
"name",
|
||||||
'email',
|
"email",
|
||||||
'country',
|
"country",
|
||||||
'affiliation',
|
"affiliation",
|
||||||
'bracket',
|
"bracket",
|
||||||
'id',
|
"id",
|
||||||
'oauth_id',
|
"oauth_id",
|
||||||
'password'
|
"password",
|
||||||
|
],
|
||||||
|
"admin": [
|
||||||
|
"website",
|
||||||
|
"name",
|
||||||
|
"created",
|
||||||
|
"country",
|
||||||
|
"banned",
|
||||||
|
"email",
|
||||||
|
"affiliation",
|
||||||
|
"secret",
|
||||||
|
"bracket",
|
||||||
|
"hidden",
|
||||||
|
"id",
|
||||||
|
"oauth_id",
|
||||||
|
"password",
|
||||||
|
"type",
|
||||||
|
"verified",
|
||||||
],
|
],
|
||||||
'admin': [
|
|
||||||
'website',
|
|
||||||
'name',
|
|
||||||
'created',
|
|
||||||
'country',
|
|
||||||
'banned',
|
|
||||||
'email',
|
|
||||||
'affiliation',
|
|
||||||
'secret',
|
|
||||||
'bracket',
|
|
||||||
'hidden',
|
|
||||||
'id',
|
|
||||||
'oauth_id',
|
|
||||||
'password',
|
|
||||||
'type',
|
|
||||||
'verified'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, view=None, *args, **kwargs):
|
def __init__(self, view=None, *args, **kwargs):
|
||||||
if view:
|
if view:
|
||||||
if isinstance(view, string_types):
|
if isinstance(view, string_types):
|
||||||
kwargs['only'] = self.views[view]
|
kwargs["only"] = self.views[view]
|
||||||
elif isinstance(view, list):
|
elif isinstance(view, list):
|
||||||
kwargs['only'] = view
|
kwargs["only"] = view
|
||||||
|
|
||||||
super(UserSchema, self).__init__(*args, **kwargs)
|
super(UserSchema, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -5,15 +5,15 @@ from CTFd.utils.decorators.visibility import check_score_visibility
|
||||||
|
|
||||||
from CTFd.utils.scores import get_standings
|
from CTFd.utils.scores import get_standings
|
||||||
|
|
||||||
scoreboard = Blueprint('scoreboard', __name__)
|
scoreboard = Blueprint("scoreboard", __name__)
|
||||||
|
|
||||||
|
|
||||||
@scoreboard.route('/scoreboard')
|
@scoreboard.route("/scoreboard")
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
def listing():
|
def listing():
|
||||||
standings = get_standings()
|
standings = get_standings()
|
||||||
return render_template(
|
return render_template(
|
||||||
'scoreboard.html',
|
"scoreboard.html",
|
||||||
standings=standings,
|
standings=standings,
|
||||||
score_frozen=config.is_scoreboard_frozen()
|
score_frozen=config.is_scoreboard_frozen(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,17 +5,20 @@ from CTFd.utils.decorators.modes import require_team_mode
|
||||||
from CTFd.utils import config
|
from CTFd.utils import config
|
||||||
from CTFd.utils.user import get_current_user
|
from CTFd.utils.user import get_current_user
|
||||||
from CTFd.utils.crypto import verify_password
|
from CTFd.utils.crypto import verify_password
|
||||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
from CTFd.utils.decorators.visibility import (
|
||||||
|
check_account_visibility,
|
||||||
|
check_score_visibility,
|
||||||
|
)
|
||||||
from CTFd.utils.helpers import get_errors
|
from CTFd.utils.helpers import get_errors
|
||||||
|
|
||||||
teams = Blueprint('teams', __name__)
|
teams = Blueprint("teams", __name__)
|
||||||
|
|
||||||
|
|
||||||
@teams.route('/teams')
|
@teams.route("/teams")
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
@require_team_mode
|
@require_team_mode
|
||||||
def listing():
|
def listing():
|
||||||
page = abs(request.args.get('page', 1, type=int))
|
page = abs(request.args.get("page", 1, type=int))
|
||||||
results_per_page = 50
|
results_per_page = 50
|
||||||
page_start = results_per_page * (page - 1)
|
page_start = results_per_page * (page - 1)
|
||||||
page_end = results_per_page * (page - 1) + results_per_page
|
page_end = results_per_page * (page - 1) + results_per_page
|
||||||
|
@ -26,21 +29,25 @@ def listing():
|
||||||
# teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all()
|
# teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all()
|
||||||
# else:
|
# else:
|
||||||
count = Teams.query.filter_by(hidden=False, banned=False).count()
|
count = Teams.query.filter_by(hidden=False, banned=False).count()
|
||||||
teams = Teams.query.filter_by(hidden=False, banned=False).slice(page_start, page_end).all()
|
teams = (
|
||||||
|
Teams.query.filter_by(hidden=False, banned=False)
|
||||||
|
.slice(page_start, page_end)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||||
return render_template('teams/teams.html', teams=teams, pages=pages, curr_page=page)
|
return render_template("teams/teams.html", teams=teams, pages=pages, curr_page=page)
|
||||||
|
|
||||||
|
|
||||||
@teams.route('/teams/join', methods=['GET', 'POST'])
|
@teams.route("/teams/join", methods=["GET", "POST"])
|
||||||
@authed_only
|
@authed_only
|
||||||
@require_team_mode
|
@require_team_mode
|
||||||
def join():
|
def join():
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
return render_template('teams/join_team.html')
|
return render_template("teams/join_team.html")
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
teamname = request.form.get('name')
|
teamname = request.form.get("name")
|
||||||
passphrase = request.form.get('password', '').strip()
|
passphrase = request.form.get("password", "").strip()
|
||||||
|
|
||||||
team = Teams.query.filter_by(name=teamname).first()
|
team = Teams.query.filter_by(name=teamname).first()
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
@ -52,57 +59,51 @@ def join():
|
||||||
team.captain_id = user.id
|
team.captain_id = user.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
else:
|
else:
|
||||||
errors = ['That information is incorrect']
|
errors = ["That information is incorrect"]
|
||||||
return render_template('teams/join_team.html', errors=errors)
|
return render_template("teams/join_team.html", errors=errors)
|
||||||
|
|
||||||
|
|
||||||
@teams.route('/teams/new', methods=['GET', 'POST'])
|
@teams.route("/teams/new", methods=["GET", "POST"])
|
||||||
@authed_only
|
@authed_only
|
||||||
@require_team_mode
|
@require_team_mode
|
||||||
def new():
|
def new():
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
return render_template("teams/new_team.html")
|
return render_template("teams/new_team.html")
|
||||||
elif request.method == 'POST':
|
elif request.method == "POST":
|
||||||
teamname = request.form.get('name')
|
teamname = request.form.get("name")
|
||||||
passphrase = request.form.get('password', '').strip()
|
passphrase = request.form.get("password", "").strip()
|
||||||
errors = get_errors()
|
errors = get_errors()
|
||||||
|
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
|
|
||||||
existing_team = Teams.query.filter_by(name=teamname).first()
|
existing_team = Teams.query.filter_by(name=teamname).first()
|
||||||
if existing_team:
|
if existing_team:
|
||||||
errors.append('That team name is already taken')
|
errors.append("That team name is already taken")
|
||||||
if not teamname:
|
if not teamname:
|
||||||
errors.append('That team name is invalid')
|
errors.append("That team name is invalid")
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return render_template("teams/new_team.html", errors=errors)
|
return render_template("teams/new_team.html", errors=errors)
|
||||||
|
|
||||||
team = Teams(
|
team = Teams(name=teamname, password=passphrase, captain_id=user.id)
|
||||||
name=teamname,
|
|
||||||
password=passphrase,
|
|
||||||
captain_id=user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(team)
|
db.session.add(team)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
user.team_id = team.id
|
user.team_id = team.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('challenges.listing'))
|
return redirect(url_for("challenges.listing"))
|
||||||
|
|
||||||
|
|
||||||
@teams.route('/team')
|
@teams.route("/team")
|
||||||
@authed_only
|
@authed_only
|
||||||
@require_team_mode
|
@require_team_mode
|
||||||
def private():
|
def private():
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
if not user.team_id:
|
if not user.team_id:
|
||||||
return render_template(
|
return render_template("teams/team_enrollment.html")
|
||||||
'teams/team_enrollment.html',
|
|
||||||
)
|
|
||||||
|
|
||||||
team_id = user.team_id
|
team_id = user.team_id
|
||||||
|
|
||||||
|
@ -114,18 +115,18 @@ def private():
|
||||||
score = team.score
|
score = team.score
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'teams/private.html',
|
"teams/private.html",
|
||||||
solves=solves,
|
solves=solves,
|
||||||
awards=awards,
|
awards=awards,
|
||||||
user=user,
|
user=user,
|
||||||
team=team,
|
team=team,
|
||||||
score=score,
|
score=score,
|
||||||
place=place,
|
place=place,
|
||||||
score_frozen=config.is_scoreboard_frozen()
|
score_frozen=config.is_scoreboard_frozen(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@teams.route('/teams/<int:team_id>')
|
@teams.route("/teams/<int:team_id>")
|
||||||
@check_account_visibility
|
@check_account_visibility
|
||||||
@check_score_visibility
|
@check_score_visibility
|
||||||
@require_team_mode
|
@require_team_mode
|
||||||
|
@ -139,14 +140,14 @@ def public(team_id):
|
||||||
score = team.score
|
score = team.score
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return render_template('teams/public.html', team=team, errors=errors)
|
return render_template("teams/public.html", team=team, errors=errors)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'teams/public.html',
|
"teams/public.html",
|
||||||
solves=solves,
|
solves=solves,
|
||||||
awards=awards,
|
awards=awards,
|
||||||
team=team,
|
team=team,
|
||||||
score=score,
|
score=score,
|
||||||
place=place,
|
place=place,
|
||||||
score_frozen=config.is_scoreboard_frozen()
|
score_frozen=config.is_scoreboard_frozen(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,196 +1,200 @@
|
||||||
html, body, .container {
|
html,
|
||||||
font-family: 'Lato', 'LatoOffline', sans-serif;
|
body,
|
||||||
|
.container {
|
||||||
|
font-family: "Lato", "LatoOffline", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1,
|
||||||
font-family: 'Raleway', 'RalewayOffline', sans-serif;
|
h2 {
|
||||||
font-weight: 500;
|
font-family: "Raleway", "RalewayOffline", sans-serif;
|
||||||
letter-spacing: 2px;
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table > thead > tr > td {
|
table > thead > tr > td {
|
||||||
/* Remove border line from thead of all tables */
|
/* Remove border line from thead of all tables */
|
||||||
/* It can overlap with other element styles */
|
/* It can overlap with other element styles */
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td, .table th {
|
.table td,
|
||||||
vertical-align: inherit;
|
.table th {
|
||||||
|
vertical-align: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-spin.spinner {
|
.fa-spin.spinner {
|
||||||
margin-top: 225px;
|
margin-top: 225px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-error {
|
.spinner-error {
|
||||||
padding-top: 20vh;
|
padding-top: 20vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron {
|
.jumbotron {
|
||||||
/*background-color: #343a40;*/
|
/*background-color: #343a40;*/
|
||||||
/*color: #FFF;*/
|
/*color: #FFF;*/
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
/*padding: 0.8em;*/
|
/*padding: 0.8em;*/
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
/*background: #f0f0f0;*/
|
/*background: #f0f0f0;*/
|
||||||
/*color: #aaa;*/
|
/*color: #aaa;*/
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #a3d39c;
|
border-color: #a3d39c;
|
||||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-filled-valid {
|
.input-filled-valid {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #a3d39c;
|
border-color: #a3d39c;
|
||||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-filled-invalid {
|
.input-filled-invalid {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #d46767;
|
border-color: #d46767;
|
||||||
box-shadow: 0 0 0 0.2rem #d46767;
|
box-shadow: 0 0 0 0.2rem #d46767;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined.btn-theme {
|
.btn-outlined.btn-theme {
|
||||||
background: none;
|
background: none;
|
||||||
color: #545454;
|
color: #545454;
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined {
|
.btn-outlined {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
border-color: #5B7290 !important;
|
border-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#score-graph {
|
#score-graph {
|
||||||
height: 450px;
|
height: 450px;
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
#solves-graph {
|
#solves-graph {
|
||||||
display: block;
|
display: block;
|
||||||
height: 350px;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keys-pie-graph {
|
#keys-pie-graph {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#categories-pie-graph {
|
#categories-pie-graph {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#solve-percentages-graph {
|
#solve-percentages-graph {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-decoration {
|
.no-decoration {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-decoration:hover {
|
.no-decoration:hover {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-fa {
|
.btn-fa {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-help {
|
.cursor-help {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
-webkit-border-radius: 0 !important;
|
-webkit-border-radius: 0 !important;
|
||||||
-moz-border-radius: 0 !important;
|
-moz-border-radius: 0 !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-break {
|
.text-break {
|
||||||
/* TODO: This is .text-break cloned from Bootstrap 4.3 with a fix for browsers not supporting break-word. Remove later. */
|
/* TODO: This is .text-break cloned from Bootstrap 4.3 with a fix for browsers not supporting break-word. Remove later. */
|
||||||
word-break: break-all !important;
|
word-break: break-all !important;
|
||||||
word-break: break-word !important;
|
word-break: break-word !important;
|
||||||
overflow-wrap: break-word !important;
|
overflow-wrap: break-word !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-disabled {
|
.fa-disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,58 @@
|
||||||
.chal-desc {
|
.chal-desc {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chal-desc img {
|
.chal-desc img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.challenge-button {
|
.challenge-button {
|
||||||
box-shadow: 3px 3px 3px grey;
|
box-shadow: 3px 3px 3px grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solved-challenge {
|
.solved-challenge {
|
||||||
background-color: #37d63e !important;
|
background-color: #37d63e !important;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner-button-check {
|
.corner-button-check {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-submit .btn {
|
.key-submit .btn {
|
||||||
height: 51px;
|
height: 51px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#challenge-window .form-control {
|
#challenge-window .form-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
||||||
body {
|
body {
|
||||||
padding-top: 3.5rem;
|
padding-top: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
/* Sticky footer styles
|
/* Sticky footer styles
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
html {
|
html {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
|
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px; /* Set the fixed height of the footer here */
|
height: 60px; /* Set the fixed height of the footer here */
|
||||||
/*line-height: 60px; !* Vertically center the text there *!*/
|
/*line-height: 60px; !* Vertically center the text there *!*/
|
||||||
/*background-color: #f5f5f5;*/
|
/*background-color: #f5f5f5;*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
tbody tr:hover {
|
tbody tr:hover {
|
||||||
background-color: rgba(0, 0, 0, .1) !important;
|
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr[data-href] {
|
tr[data-href] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,195 +1,237 @@
|
||||||
function renderSubmissionResponse(response, cb) {
|
function renderSubmissionResponse(response, cb) {
|
||||||
var result = response.data;
|
var result = response.data;
|
||||||
|
|
||||||
var result_message = $('#result-message');
|
var result_message = $("#result-message");
|
||||||
var result_notification = $('#result-notification');
|
var result_notification = $("#result-notification");
|
||||||
var answer_input = $("#submission-input");
|
var answer_input = $("#submission-input");
|
||||||
result_notification.removeClass();
|
result_notification.removeClass();
|
||||||
result_message.text(result.message);
|
result_message.text(result.message);
|
||||||
|
|
||||||
if (result.status === "authentication_required") {
|
if (result.status === "authentication_required") {
|
||||||
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash;
|
window.location =
|
||||||
return
|
script_root +
|
||||||
}
|
"/login?next=" +
|
||||||
else if (result.status === "incorrect") { // Incorrect key
|
script_root +
|
||||||
result_notification.addClass('alert alert-danger alert-dismissable text-center');
|
window.location.pathname +
|
||||||
result_notification.slideDown();
|
window.location.hash;
|
||||||
|
return;
|
||||||
|
} else if (result.status === "incorrect") {
|
||||||
|
// Incorrect key
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-danger alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
answer_input.removeClass("correct");
|
answer_input.removeClass("correct");
|
||||||
answer_input.addClass("wrong");
|
answer_input.addClass("wrong");
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
answer_input.removeClass("wrong");
|
answer_input.removeClass("wrong");
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
else if (result.status === "correct") { // Challenge Solved
|
|
||||||
result_notification.addClass('alert alert-success alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
|
|
||||||
$('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves"));
|
|
||||||
|
|
||||||
answer_input.val("");
|
|
||||||
answer_input.removeClass("wrong");
|
|
||||||
answer_input.addClass("correct");
|
|
||||||
}
|
|
||||||
else if (result.status === "already_solved") { // Challenge already solved
|
|
||||||
result_notification.addClass('alert alert-info alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
|
|
||||||
answer_input.addClass("correct");
|
|
||||||
}
|
|
||||||
else if (result.status === "paused") { // CTF is paused
|
|
||||||
result_notification.addClass('alert alert-warning alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
}
|
|
||||||
else if (result.status === "ratelimited") { // 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);
|
}, 3000);
|
||||||
|
} else if (result.status === "correct") {
|
||||||
|
// Challenge Solved
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-success alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
if (cb) {
|
$(".challenge-solves").text(
|
||||||
cb(result);
|
parseInt(
|
||||||
}
|
$(".challenge-solves")
|
||||||
|
.text()
|
||||||
|
.split(" ")[0]
|
||||||
|
) +
|
||||||
|
1 +
|
||||||
|
" Solves"
|
||||||
|
);
|
||||||
|
|
||||||
|
answer_input.val("");
|
||||||
|
answer_input.removeClass("wrong");
|
||||||
|
answer_input.addClass("correct");
|
||||||
|
} else if (result.status === "already_solved") {
|
||||||
|
// Challenge already solved
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-info alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
|
answer_input.addClass("correct");
|
||||||
|
} else if (result.status === "paused") {
|
||||||
|
// CTF is paused
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-warning alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
} else if (result.status === "ratelimited") {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('.preview-challenge').click(function (e) {
|
$(".preview-challenge").click(function(e) {
|
||||||
window.challenge = new Object();
|
window.challenge = new Object();
|
||||||
$.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function (response) {
|
$.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function(
|
||||||
var challenge_data = response.data;
|
response
|
||||||
challenge_data['solves'] = null;
|
) {
|
||||||
|
var challenge_data = response.data;
|
||||||
|
challenge_data["solves"] = null;
|
||||||
|
|
||||||
$.getScript(script_root + challenge_data.type_data.scripts.view, function () {
|
$.getScript(
|
||||||
$.get(script_root + challenge_data.type_data.templates.view, function (template_data) {
|
script_root + challenge_data.type_data.scripts.view,
|
||||||
|
function() {
|
||||||
|
$.get(script_root + challenge_data.type_data.templates.view, function(
|
||||||
|
template_data
|
||||||
|
) {
|
||||||
|
$("#challenge-window").empty();
|
||||||
|
var template = nunjucks.compile(template_data);
|
||||||
|
window.challenge.data = challenge_data;
|
||||||
|
window.challenge.preRender();
|
||||||
|
|
||||||
$('#challenge-window').empty();
|
challenge_data["description"] = window.challenge.render(
|
||||||
var template = nunjucks.compile(template_data);
|
challenge_data["description"]
|
||||||
window.challenge.data = challenge_data;
|
);
|
||||||
window.challenge.preRender();
|
challenge_data["script_root"] = script_root;
|
||||||
|
|
||||||
challenge_data['description'] = window.challenge.render(challenge_data['description']);
|
$("#challenge-window").append(template.render(challenge_data));
|
||||||
challenge_data['script_root'] = script_root;
|
|
||||||
|
|
||||||
$('#challenge-window').append(template.render(challenge_data));
|
$(".challenge-solves").click(function(e) {
|
||||||
|
getsolves($("#challenge-id").val());
|
||||||
$('.challenge-solves').click(function (e) {
|
|
||||||
getsolves($('#challenge-id').val())
|
|
||||||
});
|
|
||||||
$('.nav-tabs a').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).tab('show')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle modal toggling
|
|
||||||
$('#challenge-window').on('hide.bs.modal', function (event) {
|
|
||||||
$("#submission-input").removeClass("wrong");
|
|
||||||
$("#submission-input").removeClass("correct");
|
|
||||||
$("#incorrect-key").slideUp();
|
|
||||||
$("#correct-key").slideUp();
|
|
||||||
$("#already-solved").slideUp();
|
|
||||||
$("#too-fast").slideUp();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#submit-key').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('#submit-key').addClass("disabled-button");
|
|
||||||
$('#submit-key').prop('disabled', true);
|
|
||||||
window.challenge.submit(function (data) {
|
|
||||||
renderSubmissionResponse(data)
|
|
||||||
}, true);
|
|
||||||
// Preview passed as true
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#submission-input").keyup(function (event) {
|
|
||||||
if (event.keyCode == 13) {
|
|
||||||
$("#submit-key").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".input-field").bind({
|
|
||||||
focus: function () {
|
|
||||||
$(this).parent().addClass('input--filled');
|
|
||||||
$label = $(this).siblings(".input-label");
|
|
||||||
},
|
|
||||||
blur: function () {
|
|
||||||
if ($(this).val() === '') {
|
|
||||||
$(this).parent().removeClass('input--filled');
|
|
||||||
$label = $(this).siblings(".input-label");
|
|
||||||
$label.removeClass('input--hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.challenge.postRender();
|
|
||||||
window.location.replace(window.location.href.split('#')[0] + '#preview');
|
|
||||||
|
|
||||||
$('#challenge-window').modal();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
$(".nav-tabs a").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).tab("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal toggling
|
||||||
|
$("#challenge-window").on("hide.bs.modal", function(event) {
|
||||||
|
$("#submission-input").removeClass("wrong");
|
||||||
|
$("#submission-input").removeClass("correct");
|
||||||
|
$("#incorrect-key").slideUp();
|
||||||
|
$("#correct-key").slideUp();
|
||||||
|
$("#already-solved").slideUp();
|
||||||
|
$("#too-fast").slideUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#submit-key").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$("#submit-key").addClass("disabled-button");
|
||||||
|
$("#submit-key").prop("disabled", true);
|
||||||
|
window.challenge.submit(function(data) {
|
||||||
|
renderSubmissionResponse(data);
|
||||||
|
}, true);
|
||||||
|
// Preview passed as true
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#submission-input").keyup(function(event) {
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
$("#submit-key").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".input-field").bind({
|
||||||
|
focus: function() {
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.addClass("input--filled");
|
||||||
|
$label = $(this).siblings(".input-label");
|
||||||
|
},
|
||||||
|
blur: function() {
|
||||||
|
if ($(this).val() === "") {
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.removeClass("input--filled");
|
||||||
|
$label = $(this).siblings(".input-label");
|
||||||
|
$label.removeClass("input--hide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.challenge.postRender();
|
||||||
|
window.location.replace(
|
||||||
|
window.location.href.split("#")[0] + "#preview"
|
||||||
|
);
|
||||||
|
|
||||||
|
$("#challenge-window").modal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.delete-challenge').click(function(e){
|
$(".delete-challenge").click(function(e) {
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete Challenge",
|
title: "Delete Challenge",
|
||||||
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(CHALLENGE_NAME) + "</strong>"),
|
body: "Are you sure you want to delete {0}".format(
|
||||||
success: function () {
|
"<strong>" + htmlentities(CHALLENGE_NAME) + "</strong>"
|
||||||
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
|
),
|
||||||
method: 'DELETE',
|
success: function() {
|
||||||
}).then(function (response) {
|
CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
|
||||||
return response.json();
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
if (response.success) {
|
.then(function(response) {
|
||||||
window.location = script_root + '/admin/challenges';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#challenge-update-container > form').submit(function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var params = $(e.target).serializeJSON(true);
|
|
||||||
console.log(params);
|
|
||||||
|
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
|
|
||||||
method: 'PATCH',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then(function (data) {
|
})
|
||||||
if (data.success) {
|
.then(function(response) {
|
||||||
ezal({
|
if (response.success) {
|
||||||
title: "Success",
|
window.location = script_root + "/admin/challenges";
|
||||||
body: "Your challenge has been updated!",
|
|
||||||
button: "OK"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (window.location.hash) {
|
$("#challenge-update-container > form").submit(function(e) {
|
||||||
let hash = window.location.hash.replace("<>[]'\"", "");
|
e.preventDefault();
|
||||||
$('nav a[href="' + hash + '"]').tab('show');
|
var params = $(e.target).serializeJSON(true);
|
||||||
}
|
console.log(params);
|
||||||
|
|
||||||
$('.nav-tabs a').click(function (e) {
|
CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
|
||||||
$(this).tab('show');
|
method: "PATCH",
|
||||||
window.location.hash = this.hash;
|
credentials: "same-origin",
|
||||||
});
|
headers: {
|
||||||
});
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
ezal({
|
||||||
|
title: "Success",
|
||||||
|
body: "Your challenge has been updated!",
|
||||||
|
button: "OK"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.location.hash) {
|
||||||
|
let hash = window.location.hash.replace("<>[]'\"", "");
|
||||||
|
$('nav a[href="' + hash + '"]').tab("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".nav-tabs a").click(function(e) {
|
||||||
|
$(this).tab("show");
|
||||||
|
window.location.hash = this.hash;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {});
|
||||||
});
|
|
||||||
|
|
|
@ -1,75 +1,75 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#file-add-form').submit(function (e) {
|
$("#file-add-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var formData = new FormData(e.target);
|
var formData = new FormData(e.target);
|
||||||
formData.append('nonce', csrf_nonce);
|
formData.append("nonce", csrf_nonce);
|
||||||
formData.append('challenge', CHALLENGE_ID);
|
formData.append("challenge", CHALLENGE_ID);
|
||||||
formData.append('type', 'challenge');
|
formData.append("type", "challenge");
|
||||||
var pg = ezpg({
|
var pg = ezpg({
|
||||||
width: 0,
|
width: 0,
|
||||||
title: "Upload Progress",
|
title: "Upload Progress"
|
||||||
});
|
|
||||||
$.ajax({
|
|
||||||
url: script_root + '/api/v1/files',
|
|
||||||
data: formData,
|
|
||||||
type: 'POST',
|
|
||||||
cache: false,
|
|
||||||
contentType: false,
|
|
||||||
processData: false,
|
|
||||||
xhr: function () {
|
|
||||||
var xhr = $.ajaxSettings.xhr();
|
|
||||||
xhr.upload.onprogress = function (e) {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
var width = (e.loaded / e.total) * 100;
|
|
||||||
pg = ezpg({
|
|
||||||
target: pg,
|
|
||||||
width: width
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return xhr;
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
// TODO: Refresh files on submit
|
|
||||||
e.target.reset();
|
|
||||||
|
|
||||||
// Refresh modal
|
|
||||||
pg = ezpg({
|
|
||||||
target: pg,
|
|
||||||
width: 100,
|
|
||||||
});
|
|
||||||
setTimeout(
|
|
||||||
function () {
|
|
||||||
pg.modal('hide');
|
|
||||||
}, 500
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(
|
|
||||||
function () {
|
|
||||||
window.location.reload();
|
|
||||||
}, 700
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
$.ajax({
|
||||||
|
url: script_root + "/api/v1/files",
|
||||||
|
data: formData,
|
||||||
|
type: "POST",
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
xhr: function() {
|
||||||
|
var xhr = $.ajaxSettings.xhr();
|
||||||
|
xhr.upload.onprogress = function(e) {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
var width = (e.loaded / e.total) * 100;
|
||||||
|
pg = ezpg({
|
||||||
|
target: pg,
|
||||||
|
width: width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
// TODO: Refresh files on submit
|
||||||
|
e.target.reset();
|
||||||
|
|
||||||
$('.delete-file').click(function(e){
|
// Refresh modal
|
||||||
var file_id = $(this).attr('file-id');
|
pg = ezpg({
|
||||||
var row = $(this).parent().parent();
|
target: pg,
|
||||||
ezq({
|
width: 100
|
||||||
title: "Delete Files",
|
|
||||||
body: "Are you sure you want to delete this file?",
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/files/' + file_id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
row.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
pg.modal("hide");
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".delete-file").click(function(e) {
|
||||||
|
var file_id = $(this).attr("file-id");
|
||||||
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
ezq({
|
||||||
|
title: "Delete Files",
|
||||||
|
body: "Are you sure you want to delete this file?",
|
||||||
|
success: function() {
|
||||||
|
CTFd.fetch("/api/v1/files/" + file_id, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,116 +1,134 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#flag-add-button').click(function (e) {
|
$("#flag-add-button").click(function(e) {
|
||||||
$.get(script_root + '/api/v1/flags/types', function (response) {
|
$.get(script_root + "/api/v1/flags/types", function(response) {
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
var flag_type_select = $("#flags-create-select");
|
var flag_type_select = $("#flags-create-select");
|
||||||
flag_type_select.empty();
|
flag_type_select.empty();
|
||||||
|
|
||||||
var option = "<option> -- </option>";
|
var option = "<option> -- </option>";
|
||||||
flag_type_select.append(option);
|
flag_type_select.append(option);
|
||||||
|
|
||||||
for (var key in data) {
|
for (var key in data) {
|
||||||
if (data.hasOwnProperty(key)) {
|
if (data.hasOwnProperty(key)) {
|
||||||
option = "<option value='{0}'>{1}</option>".format(key, data[key].name);
|
option = "<option value='{0}'>{1}</option>".format(
|
||||||
flag_type_select.append(option);
|
key,
|
||||||
}
|
data[key].name
|
||||||
}
|
);
|
||||||
$("#flag-edit-modal").modal();
|
flag_type_select.append(option);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
$('#flag-edit-modal form').submit(function(e){
|
$("#flag-edit-modal").modal();
|
||||||
e.preventDefault();
|
|
||||||
var params = $(this).serializeJSON(true);
|
|
||||||
params['challenge'] = CHALLENGE_ID;
|
|
||||||
CTFd.fetch('/api/v1/flags', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json()
|
|
||||||
}).then(function (response) {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
$('#flag-edit-modal').modal();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#flag-edit-modal form").submit(function(e) {
|
||||||
$("#flags-create-select").change(function (e) {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
var params = $(this).serializeJSON(true);
|
||||||
var flag_type_name = $(this).find("option:selected").text();
|
params["challenge"] = CHALLENGE_ID;
|
||||||
|
CTFd.fetch("/api/v1/flags", {
|
||||||
$.get(script_root + '/api/v1/flags/types/' + flag_type_name, function (response) {
|
method: "POST",
|
||||||
var data = response.data;
|
credentials: "same-origin",
|
||||||
$.get(script_root + data.templates.create, function (template_data) {
|
headers: {
|
||||||
var template = nunjucks.compile(template_data);
|
Accept: "application/json",
|
||||||
$("#create-keys-entry-div").html(template.render());
|
"Content-Type": "application/json"
|
||||||
$("#create-keys-button-div").show();
|
},
|
||||||
});
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
})
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
$("#flag-edit-modal").modal();
|
||||||
|
});
|
||||||
|
|
||||||
$('.edit-flag').click(function (e) {
|
$("#flags-create-select").change(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var flag_id = $(this).attr('flag-id');
|
var flag_type_name = $(this)
|
||||||
var row = $(this).parent().parent();
|
.find("option:selected")
|
||||||
|
.text();
|
||||||
|
|
||||||
$.get(script_root + '/api/v1/flags/' + flag_id, function (response) {
|
$.get(script_root + "/api/v1/flags/types/" + flag_type_name, function(
|
||||||
var data = response.data;
|
response
|
||||||
$.get(script_root + data.templates.update, function (template_data) {
|
) {
|
||||||
$('#edit-flags form').empty();
|
var data = response.data;
|
||||||
|
$.get(script_root + data.templates.create, function(template_data) {
|
||||||
|
var template = nunjucks.compile(template_data);
|
||||||
|
$("#create-keys-entry-div").html(template.render());
|
||||||
|
$("#create-keys-button-div").show();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var template = nunjucks.compile(template_data);
|
$(".edit-flag").click(function(e) {
|
||||||
$('#edit-flags form').append(template.render(data));
|
e.preventDefault();
|
||||||
|
var flag_id = $(this).attr("flag-id");
|
||||||
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
$('#edit-flags form').submit(function (e) {
|
$.get(script_root + "/api/v1/flags/" + flag_id, function(response) {
|
||||||
e.preventDefault();
|
var data = response.data;
|
||||||
var params = $('#edit-flags form').serializeJSON();
|
$.get(script_root + data.templates.update, function(template_data) {
|
||||||
|
$("#edit-flags form").empty();
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/flags/' + flag_id, {
|
var template = nunjucks.compile(template_data);
|
||||||
method: 'PATCH',
|
$("#edit-flags form").append(template.render(data));
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
$("#edit-flags form").submit(function(e) {
|
||||||
'Accept': 'application/json',
|
e.preventDefault();
|
||||||
'Content-Type': 'application/json'
|
var params = $("#edit-flags form").serializeJSON();
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
CTFd.fetch("/api/v1/flags/" + flag_id, {
|
||||||
}).then(function (response) {
|
method: "PATCH",
|
||||||
return response.json();
|
credentials: "same-origin",
|
||||||
}).then(function (response) {
|
headers: {
|
||||||
if (response.success) {
|
Accept: "application/json",
|
||||||
$(row).find('.flag-content').text(response.data.content);
|
"Content-Type": "application/json"
|
||||||
$('#edit-flags').modal('toggle');
|
},
|
||||||
}
|
body: JSON.stringify(params)
|
||||||
});
|
})
|
||||||
});
|
.then(function(response) {
|
||||||
$('#edit-flags').modal();
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$(row)
|
||||||
|
.find(".flag-content")
|
||||||
|
.text(response.data.content);
|
||||||
|
$("#edit-flags").modal("toggle");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$("#edit-flags").modal();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.delete-flag').click(function (e) {
|
$(".delete-flag").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var flag_id = $(this).attr('flag-id');
|
var flag_id = $(this).attr("flag-id");
|
||||||
var row = $(this).parent().parent();
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete Flag",
|
title: "Delete Flag",
|
||||||
body: "Are you sure you want to delete this flag?",
|
body: "Are you sure you want to delete this flag?",
|
||||||
success: function () {
|
success: function() {
|
||||||
CTFd.fetch('/api/v1/flags/' + flag_id, {
|
CTFd.fetch("/api/v1/flags/" + flag_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
row.remove();
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
});
|
row.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,145 +1,157 @@
|
||||||
function hint(id) {
|
function hint(id) {
|
||||||
return CTFd.fetch('/api/v1/hints/' + id + '?preview=true', {
|
return CTFd.fetch("/api/v1/hints/" + id + "?preview=true", {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}).then(function (response) {
|
}).then(function(response) {
|
||||||
return response.json();
|
return response.json();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadhint(hintid) {
|
function loadhint(hintid) {
|
||||||
var md = window.markdownit({
|
var md = window.markdownit({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true
|
||||||
});
|
});
|
||||||
|
|
||||||
hint(hintid).then(function (response) {
|
hint(hintid).then(function(response) {
|
||||||
if (response.data.content) {
|
if (response.data.content) {
|
||||||
ezal({
|
ezal({
|
||||||
title: "Hint",
|
title: "Hint",
|
||||||
body: md.render(response.data.content),
|
body: md.render(response.data.content),
|
||||||
button: "Got it!"
|
button: "Got it!"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ezal({
|
ezal({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
body: "Error loading hint!",
|
body: "Error loading hint!",
|
||||||
button: "OK"
|
button: "OK"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#hint-add-button').click(function (e) {
|
$("#hint-add-button").click(function(e) {
|
||||||
$('#hint-edit-modal form').find("input, textarea").val("");
|
$("#hint-edit-modal form")
|
||||||
|
.find("input, textarea")
|
||||||
|
.val("");
|
||||||
|
|
||||||
// Markdown Preview
|
// Markdown Preview
|
||||||
$('#new-hint-edit').on('shown.bs.tab', function (event) {
|
$("#new-hint-edit").on("shown.bs.tab", function(event) {
|
||||||
console.log(event.target.hash);
|
console.log(event.target.hash);
|
||||||
if (event.target.hash == '#hint-preview') {
|
if (event.target.hash == "#hint-preview") {
|
||||||
console.log(event.target.hash);
|
console.log(event.target.hash);
|
||||||
var renderer = window.markdownit({
|
var renderer = window.markdownit({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true
|
||||||
});
|
|
||||||
var editor_value = $('#hint-write textarea').val();
|
|
||||||
$(event.target.hash).html(renderer.render(editor_value));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
var editor_value = $("#hint-write textarea").val();
|
||||||
$('#hint-edit-modal').modal();
|
$(event.target.hash).html(renderer.render(editor_value));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.delete-hint').click(function(e){
|
$("#hint-edit-modal").modal();
|
||||||
e.preventDefault();
|
});
|
||||||
var hint_id = $(this).attr('hint-id');
|
|
||||||
var row = $(this).parent().parent();
|
|
||||||
ezq({
|
|
||||||
title: "Delete Hint",
|
|
||||||
body: "Are you sure you want to delete this hint?",
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/hints/' + hint_id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
row.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.edit-hint').click(function (e) {
|
$(".delete-hint").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var hint_id = $(this).attr('hint-id');
|
var hint_id = $(this).attr("hint-id");
|
||||||
|
var row = $(this)
|
||||||
CTFd.fetch('/api/v1/hints/' + hint_id + '?preview=true', {
|
.parent()
|
||||||
method: 'GET',
|
.parent();
|
||||||
credentials: 'same-origin',
|
ezq({
|
||||||
headers: {
|
title: "Delete Hint",
|
||||||
'Accept': 'application/json',
|
body: "Are you sure you want to delete this hint?",
|
||||||
'Content-Type': 'application/json'
|
success: function() {
|
||||||
}
|
CTFd.fetch("/api/v1/hints/" + hint_id, {
|
||||||
}).then(function (response) {
|
method: "DELETE"
|
||||||
return response.json()
|
})
|
||||||
}).then(function (response) {
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$('#hint-edit-form input[name=content],textarea[name=content]').val(response.data.content);
|
row.remove();
|
||||||
$('#hint-edit-form input[name=cost]').val(response.data.cost);
|
|
||||||
$('#hint-edit-form input[name=id]').val(response.data.id);
|
|
||||||
|
|
||||||
// Markdown Preview
|
|
||||||
$('#new-hint-edit').on('shown.bs.tab', function (event) {
|
|
||||||
console.log(event.target.hash);
|
|
||||||
if (event.target.hash == '#hint-preview') {
|
|
||||||
console.log(event.target.hash);
|
|
||||||
var renderer = new markdownit({
|
|
||||||
html: true,
|
|
||||||
linkify: true,
|
|
||||||
});
|
|
||||||
var editor_value = $('#hint-write textarea').val();
|
|
||||||
$(event.target.hash).html(renderer.render(editor_value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#hint-edit-modal').modal();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('#hint-edit-form').submit(function (e) {
|
$(".edit-hint").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var params = $(this).serializeJSON(true);
|
var hint_id = $(this).attr("hint-id");
|
||||||
params['challenge'] = CHALLENGE_ID;
|
|
||||||
|
|
||||||
var method = 'POST';
|
CTFd.fetch("/api/v1/hints/" + hint_id + "?preview=true", {
|
||||||
var url = '/api/v1/hints';
|
method: "GET",
|
||||||
if (params.id){
|
credentials: "same-origin",
|
||||||
method = 'PATCH';
|
headers: {
|
||||||
url = '/api/v1/hints/' + params.id;
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$("#hint-edit-form input[name=content],textarea[name=content]").val(
|
||||||
|
response.data.content
|
||||||
|
);
|
||||||
|
$("#hint-edit-form input[name=cost]").val(response.data.cost);
|
||||||
|
$("#hint-edit-form input[name=id]").val(response.data.id);
|
||||||
|
|
||||||
|
// Markdown Preview
|
||||||
|
$("#new-hint-edit").on("shown.bs.tab", function(event) {
|
||||||
|
console.log(event.target.hash);
|
||||||
|
if (event.target.hash == "#hint-preview") {
|
||||||
|
console.log(event.target.hash);
|
||||||
|
var renderer = new markdownit({
|
||||||
|
html: true,
|
||||||
|
linkify: true
|
||||||
|
});
|
||||||
|
var editor_value = $("#hint-write textarea").val();
|
||||||
|
$(event.target.hash).html(renderer.render(editor_value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#hint-edit-modal").modal();
|
||||||
}
|
}
|
||||||
CTFd.fetch(url, {
|
});
|
||||||
method: method,
|
});
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
$("#hint-edit-form").submit(function(e) {
|
||||||
'Accept': 'application/json',
|
e.preventDefault();
|
||||||
'Content-Type': 'application/json'
|
var params = $(this).serializeJSON(true);
|
||||||
},
|
params["challenge"] = CHALLENGE_ID;
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
var method = "POST";
|
||||||
return response.json()
|
var url = "/api/v1/hints";
|
||||||
}).then(function(response) {
|
if (params.id) {
|
||||||
if (response.success){
|
method = "PATCH";
|
||||||
// TODO: Refresh hints on submit.
|
url = "/api/v1/hints/" + params.id;
|
||||||
window.location.reload();
|
}
|
||||||
}
|
CTFd.fetch(url, {
|
||||||
});
|
method: method,
|
||||||
});
|
credentials: "same-origin",
|
||||||
});
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
// TODO: Refresh hints on submit.
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -2,61 +2,68 @@ $.ajaxSetup({ cache: false });
|
||||||
|
|
||||||
window.challenge = new Object();
|
window.challenge = new Object();
|
||||||
|
|
||||||
function load_chal_template(challenge){
|
function load_chal_template(challenge) {
|
||||||
$.getScript(script_root + challenge.scripts.view, function () {
|
$.getScript(script_root + challenge.scripts.view, function() {
|
||||||
console.log('loaded renderer');
|
console.log("loaded renderer");
|
||||||
$.get(script_root + challenge.templates.create, function (template_data) {
|
$.get(script_root + challenge.templates.create, function(template_data) {
|
||||||
var template = nunjucks.compile(template_data);
|
var template = nunjucks.compile(template_data);
|
||||||
$("#create-chal-entry-div").html(template.render({'nonce': nonce, 'script_root': script_root}));
|
$("#create-chal-entry-div").html(
|
||||||
$.getScript(script_root + challenge.scripts.create, function () {
|
template.render({ nonce: nonce, script_root: script_root })
|
||||||
console.log('loaded');
|
);
|
||||||
$("#create-chal-entry-div form").submit(function (e) {
|
$.getScript(script_root + challenge.scripts.create, function() {
|
||||||
e.preventDefault();
|
console.log("loaded");
|
||||||
var params = $("#create-chal-entry-div form").serializeJSON();
|
$("#create-chal-entry-div form").submit(function(e) {
|
||||||
CTFd.fetch('/api/v1/challenges', {
|
e.preventDefault();
|
||||||
method: 'POST',
|
var params = $("#create-chal-entry-div form").serializeJSON();
|
||||||
credentials: 'same-origin',
|
CTFd.fetch("/api/v1/challenges", {
|
||||||
headers: {
|
method: "POST",
|
||||||
'Accept': 'application/json',
|
credentials: "same-origin",
|
||||||
'Content-Type': 'application/json'
|
headers: {
|
||||||
},
|
Accept: "application/json",
|
||||||
body: JSON.stringify(params)
|
"Content-Type": "application/json"
|
||||||
}).then(function (response) {
|
},
|
||||||
return response.json();
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
if (response.success) {
|
.then(function(response) {
|
||||||
window.location = script_root + '/admin/challenges/' + response.data.id;
|
return response.json();
|
||||||
}
|
})
|
||||||
});
|
.then(function(response) {
|
||||||
});
|
if (response.success) {
|
||||||
|
window.location =
|
||||||
|
script_root + "/admin/challenges/" + response.data.id;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$.get(script_root + '/api/v1/challenges/types', function(response){
|
$.get(script_root + "/api/v1/challenges/types", function(response) {
|
||||||
$("#create-chals-select").empty();
|
$("#create-chals-select").empty();
|
||||||
var data = response.data;
|
var data = response.data;
|
||||||
var chal_type_amt = Object.keys(data).length;
|
var chal_type_amt = Object.keys(data).length;
|
||||||
if (chal_type_amt > 1){
|
if (chal_type_amt > 1) {
|
||||||
var option = "<option> -- </option>";
|
var option = "<option> -- </option>";
|
||||||
$("#create-chals-select").append(option);
|
$("#create-chals-select").append(option);
|
||||||
for (var key in data){
|
for (var key in data) {
|
||||||
var challenge = data[key];
|
var challenge = data[key];
|
||||||
var option = $("<option/>");
|
var option = $("<option/>");
|
||||||
option.attr('value', challenge.type);
|
option.attr("value", challenge.type);
|
||||||
option.text(challenge.name);
|
option.text(challenge.name);
|
||||||
option.data('meta', challenge);
|
option.data("meta", challenge);
|
||||||
$("#create-chals-select").append(option);
|
$("#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").empty();
|
|
||||||
load_chal_template(data[key]);
|
|
||||||
}
|
}
|
||||||
|
$("#create-chals-select-div").show();
|
||||||
|
} else if (chal_type_amt == 1) {
|
||||||
|
var key = Object.keys(data)[0];
|
||||||
|
$("#create-chals-select").empty();
|
||||||
|
load_chal_template(data[key]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$('#create-chals-select').change(function(){
|
$("#create-chals-select").change(function() {
|
||||||
var challenge = $(this).find("option:selected").data('meta');
|
var challenge = $(this)
|
||||||
load_chal_template(challenge);
|
.find("option:selected")
|
||||||
|
.data("meta");
|
||||||
|
load_chal_template(challenge);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,57 +1,63 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#prerequisite-add-form').submit(function (e) {
|
$("#prerequisite-add-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var requirements = $('#prerequisite-add-form').serializeJSON();
|
var requirements = $("#prerequisite-add-form").serializeJSON();
|
||||||
CHALLENGE_REQUIREMENTS.prerequisites.push(
|
CHALLENGE_REQUIREMENTS.prerequisites.push(
|
||||||
parseInt(requirements['prerequisite'])
|
parseInt(requirements["prerequisite"])
|
||||||
);
|
);
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'requirements': CHALLENGE_REQUIREMENTS
|
requirements: CHALLENGE_REQUIREMENTS
|
||||||
};
|
};
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
|
CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (data) {
|
return response.json();
|
||||||
if (data.success) {
|
})
|
||||||
// TODO: Make this refresh requirements
|
.then(function(data) {
|
||||||
window.location.reload();
|
if (data.success) {
|
||||||
}
|
// TODO: Make this refresh requirements
|
||||||
});
|
window.location.reload();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.delete-requirement').click(function (e) {
|
$(".delete-requirement").click(function(e) {
|
||||||
var challenge_id = $(this).attr('challenge-id');
|
var challenge_id = $(this).attr("challenge-id");
|
||||||
var row = $(this).parent().parent();
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
CHALLENGE_REQUIREMENTS.prerequisites.pop(challenge_id);
|
CHALLENGE_REQUIREMENTS.prerequisites.pop(challenge_id);
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'requirements': CHALLENGE_REQUIREMENTS
|
requirements: CHALLENGE_REQUIREMENTS
|
||||||
};
|
};
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
|
CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (data) {
|
return response.json();
|
||||||
if (data.success) {
|
})
|
||||||
row.remove();
|
.then(function(data) {
|
||||||
}
|
if (data.success) {
|
||||||
});
|
row.remove();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,51 +1,55 @@
|
||||||
function delete_tag(elem){
|
function delete_tag(elem) {
|
||||||
var elem = $(elem);
|
var elem = $(elem);
|
||||||
var tag_id = elem.attr('tag-id');
|
var tag_id = elem.attr("tag-id");
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/tags/' + tag_id, {
|
CTFd.fetch("/api/v1/tags/" + tag_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
$(elem).parent().remove()
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
|
$(elem)
|
||||||
|
.parent()
|
||||||
|
.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#tags-add-input').keyup(function (e) {
|
$("#tags-add-input").keyup(function(e) {
|
||||||
if (e.keyCode == 13) {
|
if (e.keyCode == 13) {
|
||||||
var tag = $('#tags-add-input').val();
|
var tag = $("#tags-add-input").val();
|
||||||
var params = {
|
var params = {
|
||||||
value: tag,
|
value: tag,
|
||||||
challenge: CHALLENGE_ID
|
challenge: CHALLENGE_ID
|
||||||
};
|
};
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/tags', {
|
CTFd.fetch("/api/v1/tags", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success){
|
})
|
||||||
var tpl = "<span class='badge badge-primary mx-1 challenge-tag'>" +
|
.then(function(response) {
|
||||||
"<span>{0}</span>" +
|
if (response.success) {
|
||||||
"<a class='btn-fa delete-tag' tag-id='{1}' onclick='delete_tag(this)'>×</a></span>";
|
var tpl =
|
||||||
tag = tpl.format(
|
"<span class='badge badge-primary mx-1 challenge-tag'>" +
|
||||||
response.data.value,
|
"<span>{0}</span>" +
|
||||||
response.data.id
|
"<a class='btn-fa delete-tag' tag-id='{1}' onclick='delete_tag(this)'>×</a></span>";
|
||||||
);
|
tag = tpl.format(response.data.value, response.data.id);
|
||||||
$('#challenge-tags').append(tag);
|
$("#challenge-tags").append(tag);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#tags-add-input').val("");
|
$("#tags-add-input").val("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,300 +1,317 @@
|
||||||
var months = {
|
var months = {
|
||||||
'January': 1,
|
January: 1,
|
||||||
'February': 2,
|
February: 2,
|
||||||
'March': 3,
|
March: 3,
|
||||||
'April': 4,
|
April: 4,
|
||||||
'May': 5,
|
May: 5,
|
||||||
'June': 6,
|
June: 6,
|
||||||
'July': 7,
|
July: 7,
|
||||||
'August': 8,
|
August: 8,
|
||||||
'September': 9,
|
September: 9,
|
||||||
'October': 10,
|
October: 10,
|
||||||
'November': 11,
|
November: 11,
|
||||||
'December': 12,
|
December: 12
|
||||||
};
|
};
|
||||||
|
|
||||||
function load_timestamp(place, timestamp) {
|
function load_timestamp(place, timestamp) {
|
||||||
if (typeof timestamp == "string") {
|
if (typeof timestamp == "string") {
|
||||||
var timestamp = parseInt(timestamp);
|
var timestamp = parseInt(timestamp);
|
||||||
}
|
}
|
||||||
var m = moment(timestamp * 1000);
|
var m = moment(timestamp * 1000);
|
||||||
console.log('Loading ' + place);
|
console.log("Loading " + place);
|
||||||
console.log(timestamp);
|
console.log(timestamp);
|
||||||
console.log(m.toISOString());
|
console.log(m.toISOString());
|
||||||
console.log(m.unix());
|
console.log(m.unix());
|
||||||
var month = $('#' + place + '-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/)
|
var month = $("#" + place + "-month").val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/)
|
||||||
var day = $('#' + place + '-day').val(m.date());
|
var day = $("#" + place + "-day").val(m.date());
|
||||||
var year = $('#' + place + '-year').val(m.year());
|
var year = $("#" + place + "-year").val(m.year());
|
||||||
var hour = $('#' + place + '-hour').val(m.hour());
|
var hour = $("#" + place + "-hour").val(m.hour());
|
||||||
var minute = $('#' + place + '-minute').val(m.minute());
|
var minute = $("#" + place + "-minute").val(m.minute());
|
||||||
load_date_values(place);
|
load_date_values(place);
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_date_values(place) {
|
function load_date_values(place) {
|
||||||
var month = $('#' + place + '-month').val();
|
var month = $("#" + place + "-month").val();
|
||||||
var day = $('#' + place + '-day').val();
|
var day = $("#" + place + "-day").val();
|
||||||
var year = $('#' + place + '-year').val();
|
var year = $("#" + place + "-year").val();
|
||||||
var hour = $('#' + place + '-hour').val();
|
var hour = $("#" + place + "-hour").val();
|
||||||
var minute = $('#' + place + '-minute').val();
|
var minute = $("#" + place + "-minute").val();
|
||||||
var timezone = $('#' + place + '-timezone').val();
|
var timezone = $("#" + place + "-timezone").val();
|
||||||
|
|
||||||
var utc = convert_date_to_moment(month, day, year, hour, minute, timezone);
|
var utc = convert_date_to_moment(month, day, year, hour, minute, timezone);
|
||||||
if (isNaN(utc.unix())) {
|
if (isNaN(utc.unix())) {
|
||||||
$('#' + place).val('');
|
$("#" + place).val("");
|
||||||
$('#' + place + '-local').val('');
|
$("#" + place + "-local").val("");
|
||||||
$('#' + place + '-zonetime').val('');
|
$("#" + place + "-zonetime").val("");
|
||||||
} else {
|
} else {
|
||||||
$('#' + place).val(utc.unix());
|
$("#" + place).val(utc.unix());
|
||||||
$('#' + place + '-local').val(utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
|
$("#" + place + "-local").val(
|
||||||
$('#' + place + '-zonetime').val(utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
|
utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz")
|
||||||
}
|
);
|
||||||
|
$("#" + place + "-zonetime").val(
|
||||||
|
utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert_date_to_moment(month, day, year, hour, minute, timezone) {
|
function convert_date_to_moment(month, day, year, hour, minute, timezone) {
|
||||||
var month_num = month.toString();
|
var month_num = month.toString();
|
||||||
if (month_num.length == 1) {
|
if (month_num.length == 1) {
|
||||||
var month_num = "0" + month_num;
|
var month_num = "0" + month_num;
|
||||||
}
|
}
|
||||||
|
|
||||||
var day_str = day.toString();
|
var day_str = day.toString();
|
||||||
if (day_str.length == 1) {
|
if (day_str.length == 1) {
|
||||||
day_str = "0" + day_str;
|
day_str = "0" + day_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hour_str = hour.toString();
|
var hour_str = hour.toString();
|
||||||
if (hour_str.length == 1) {
|
if (hour_str.length == 1) {
|
||||||
hour_str = "0" + hour_str;
|
hour_str = "0" + hour_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
var min_str = minute.toString();
|
var min_str = minute.toString();
|
||||||
if (min_str.length == 1) {
|
if (min_str.length == 1) {
|
||||||
min_str = "0" + min_str;
|
min_str = "0" + min_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2013-02-08 24:00
|
// 2013-02-08 24:00
|
||||||
var date_string = year.toString() + '-' + month_num + '-' + day_str + ' ' + hour_str + ':' + min_str + ':00';
|
var date_string =
|
||||||
var m = moment(date_string, moment.ISO_8601);
|
year.toString() +
|
||||||
return m;
|
"-" +
|
||||||
|
month_num +
|
||||||
|
"-" +
|
||||||
|
day_str +
|
||||||
|
" " +
|
||||||
|
hour_str +
|
||||||
|
":" +
|
||||||
|
min_str +
|
||||||
|
":00";
|
||||||
|
var m = moment(date_string, moment.ISO_8601);
|
||||||
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_configs(obj){
|
function update_configs(obj) {
|
||||||
var target = '/api/v1/configs';
|
var target = "/api/v1/configs";
|
||||||
var method = 'PATCH';
|
var method = "PATCH";
|
||||||
|
|
||||||
var params = {};
|
var params = {};
|
||||||
|
|
||||||
if (obj.mail_useauth === false) {
|
if (obj.mail_useauth === false) {
|
||||||
obj.mail_username = null;
|
obj.mail_username = null;
|
||||||
obj.mail_password = null;
|
obj.mail_password = null;
|
||||||
} else {
|
} else {
|
||||||
if (obj.mail_username === "") {
|
if (obj.mail_username === "") {
|
||||||
delete obj.mail_username;
|
delete obj.mail_username;
|
||||||
}
|
|
||||||
if (obj.mail_password === "") {
|
|
||||||
delete obj.mail_password;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (obj.mail_password === "") {
|
||||||
|
delete obj.mail_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(obj).forEach(function (x) {
|
Object.keys(obj).forEach(function(x) {
|
||||||
if (obj[x] === "true") {
|
if (obj[x] === "true") {
|
||||||
params[x] = true;
|
params[x] = true;
|
||||||
} else if (obj[x] === "false") {
|
} else if (obj[x] === "false") {
|
||||||
params[x] = false;
|
params[x] = false;
|
||||||
} else {
|
} else {
|
||||||
params[x] = obj[x];
|
params[x] = obj[x];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
CTFd.fetch(target, {
|
CTFd.fetch(target, {
|
||||||
method: method,
|
method: method,
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function(response) {
|
})
|
||||||
return response.json()
|
.then(function(response) {
|
||||||
}).then(function(data) {
|
return response.json();
|
||||||
window.location.reload();
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
window.location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function upload_logo(form) {
|
function upload_logo(form) {
|
||||||
upload_files(form, function (response) {
|
upload_files(form, function(response) {
|
||||||
var upload = response.data[0];
|
var upload = response.data[0];
|
||||||
if (upload.location) {
|
if (upload.location) {
|
||||||
var params = {
|
var params = {
|
||||||
'value': upload.location
|
value: upload.location
|
||||||
};
|
};
|
||||||
CTFd.fetch('/api/v1/configs/ctf_logo', {
|
CTFd.fetch("/api/v1/configs/ctf_logo", {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
window.location.reload()
|
.then(function(response) {
|
||||||
} else {
|
if (response.success) {
|
||||||
ezal({
|
window.location.reload();
|
||||||
title: "Error!",
|
} else {
|
||||||
body: "Logo uploading failed!",
|
ezal({
|
||||||
button: "Okay"
|
title: "Error!",
|
||||||
});
|
body: "Logo uploading failed!",
|
||||||
}
|
button: "Okay"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove_logo() {
|
function remove_logo() {
|
||||||
ezq({
|
ezq({
|
||||||
title: "Remove logo",
|
title: "Remove logo",
|
||||||
body: "Are you sure you'd like to remove the CTF logo?",
|
body: "Are you sure you'd like to remove the CTF logo?",
|
||||||
success: function () {
|
success: function() {
|
||||||
var params = {
|
var params = {
|
||||||
'value': null
|
value: null
|
||||||
};
|
};
|
||||||
CTFd.fetch('/api/v1/configs/ctf_logo', {
|
CTFd.fetch("/api/v1/configs/ctf_logo", {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (data) {
|
return response.json();
|
||||||
window.location.reload();
|
})
|
||||||
});
|
.then(function(data) {
|
||||||
}
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function () {
|
$(function() {
|
||||||
$('.config-section > form:not(.form-upload)').submit(function(e){
|
$(".config-section > form:not(.form-upload)").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var obj = $(this).serializeJSON();
|
var obj = $(this).serializeJSON();
|
||||||
update_configs(obj);
|
update_configs(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#logo-upload").submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var form = e.target;
|
||||||
|
upload_logo(form);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".start-date").change(function() {
|
||||||
|
load_date_values("start");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".end-date").change(function() {
|
||||||
|
load_date_values("end");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".freeze-date").change(function() {
|
||||||
|
load_date_values("freeze");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#export-button").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var href = script_root + "/admin/export";
|
||||||
|
window.location.href = $("#export-button").attr("href");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#import-button").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var import_file = document.getElementById("import-file").files[0];
|
||||||
|
|
||||||
|
var form_data = new FormData();
|
||||||
|
form_data.append("backup", import_file);
|
||||||
|
form_data.append("nonce", csrf_nonce);
|
||||||
|
|
||||||
|
var pg = ezpg({
|
||||||
|
width: 0,
|
||||||
|
title: "Upload Progress"
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#logo-upload').submit(function(e){
|
$.ajax({
|
||||||
e.preventDefault();
|
url: script_root + "/admin/import",
|
||||||
var form = e.target;
|
type: "POST",
|
||||||
upload_logo(form);
|
data: form_data,
|
||||||
});
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
statusCode: {
|
||||||
$('.start-date').change(function () {
|
500: function(resp) {
|
||||||
load_date_values('start');
|
console.log(resp.responseText);
|
||||||
});
|
alert(resp.responseText);
|
||||||
|
}
|
||||||
$('.end-date').change(function () {
|
},
|
||||||
load_date_values('end');
|
xhr: function() {
|
||||||
});
|
var xhr = $.ajaxSettings.xhr();
|
||||||
|
xhr.upload.onprogress = function(e) {
|
||||||
$('.freeze-date').change(function () {
|
if (e.lengthComputable) {
|
||||||
load_date_values('freeze');
|
var width = (e.loaded / e.total) * 100;
|
||||||
});
|
pg = ezpg({
|
||||||
|
target: pg,
|
||||||
$('#export-button').click(function (e) {
|
width: width
|
||||||
e.preventDefault();
|
});
|
||||||
var href = script_root + '/admin/export';
|
}
|
||||||
window.location.href = $('#export-button').attr('href');
|
};
|
||||||
});
|
return xhr;
|
||||||
|
},
|
||||||
$('#import-button').click(function (e) {
|
success: function(data) {
|
||||||
e.preventDefault();
|
// Refresh modal
|
||||||
var import_file = document.getElementById('import-file').files[0];
|
pg = ezpg({
|
||||||
|
target: pg,
|
||||||
var form_data = new FormData();
|
width: 100
|
||||||
form_data.append('backup', import_file);
|
|
||||||
form_data.append('nonce', csrf_nonce);
|
|
||||||
|
|
||||||
var pg = ezpg({
|
|
||||||
width: 0,
|
|
||||||
title: "Upload Progress",
|
|
||||||
});
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
pg.modal("hide");
|
||||||
|
}, 500);
|
||||||
|
|
||||||
$.ajax({
|
setTimeout(function() {
|
||||||
url: script_root + '/admin/import',
|
window.location.reload();
|
||||||
type: 'POST',
|
}, 700);
|
||||||
data: form_data,
|
}
|
||||||
processData: false,
|
|
||||||
contentType: false,
|
|
||||||
statusCode: {
|
|
||||||
500: function (resp) {
|
|
||||||
console.log(resp.responseText);
|
|
||||||
alert(resp.responseText);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xhr: function () {
|
|
||||||
var xhr = $.ajaxSettings.xhr();
|
|
||||||
xhr.upload.onprogress = function (e) {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
var width = (e.loaded / e.total) * 100;
|
|
||||||
pg = ezpg({
|
|
||||||
target: pg,
|
|
||||||
width: width
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return xhr;
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
// Refresh modal
|
|
||||||
pg = ezpg({
|
|
||||||
target: pg,
|
|
||||||
width: 100,
|
|
||||||
});
|
|
||||||
setTimeout(
|
|
||||||
function () {
|
|
||||||
pg.modal('hide');
|
|
||||||
}, 500
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(
|
|
||||||
function () {
|
|
||||||
window.location.reload();
|
|
||||||
}, 700
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var hash = window.location.hash;
|
var hash = window.location.hash;
|
||||||
if (hash) {
|
if (hash) {
|
||||||
hash = hash.replace("<>[]'\"", "");
|
hash = hash.replace("<>[]'\"", "");
|
||||||
$('ul.nav a[href="' + hash + '"]').tab('show');
|
$('ul.nav a[href="' + hash + '"]').tab("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.nav-pills a').click(function (e) {
|
$(".nav-pills a").click(function(e) {
|
||||||
$(this).tab('show');
|
$(this).tab("show");
|
||||||
window.location.hash = this.hash;
|
window.location.hash = this.hash;
|
||||||
});
|
});
|
||||||
|
|
||||||
var start = $('#start').val();
|
var start = $("#start").val();
|
||||||
var end = $('#end').val();
|
var end = $("#end").val();
|
||||||
var freeze = $('#freeze').val();
|
var freeze = $("#freeze").val();
|
||||||
|
|
||||||
if (start) {
|
if (start) {
|
||||||
load_timestamp('start', start);
|
load_timestamp("start", start);
|
||||||
}
|
}
|
||||||
if (end) {
|
if (end) {
|
||||||
load_timestamp('end', end);
|
load_timestamp("end", end);
|
||||||
}
|
}
|
||||||
if (freeze) {
|
if (freeze) {
|
||||||
load_timestamp('freeze', freeze);
|
load_timestamp("freeze", freeze);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle username and password based on stored value
|
// Toggle username and password based on stored value
|
||||||
$('#mail_useauth').change(function () {
|
$("#mail_useauth")
|
||||||
$('#mail_username_password').toggle(this.checked);
|
.change(function() {
|
||||||
}).change();
|
$("#mail_username_password").toggle(this.checked);
|
||||||
});
|
})
|
||||||
|
.change();
|
||||||
|
});
|
||||||
|
|
|
@ -1,48 +1,46 @@
|
||||||
function upload_files(form, cb) {
|
function upload_files(form, cb) {
|
||||||
if (form instanceof jQuery) {
|
if (form instanceof jQuery) {
|
||||||
form = form[0];
|
form = form[0];
|
||||||
}
|
}
|
||||||
var formData = new FormData(form);
|
var formData = new FormData(form);
|
||||||
formData.append('nonce', csrf_nonce);
|
formData.append("nonce", csrf_nonce);
|
||||||
var pg = ezpg({
|
var pg = ezpg({
|
||||||
width: 0,
|
width: 0,
|
||||||
title: "Upload Progress",
|
title: "Upload Progress"
|
||||||
});
|
});
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: script_root + '/api/v1/files',
|
url: script_root + "/api/v1/files",
|
||||||
data: formData,
|
data: formData,
|
||||||
type: 'POST',
|
type: "POST",
|
||||||
cache: false,
|
cache: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
processData: false,
|
processData: false,
|
||||||
xhr: function () {
|
xhr: function() {
|
||||||
var xhr = $.ajaxSettings.xhr();
|
var xhr = $.ajaxSettings.xhr();
|
||||||
xhr.upload.onprogress = function (e) {
|
xhr.upload.onprogress = function(e) {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
var width = (e.loaded / e.total) * 100;
|
var width = (e.loaded / e.total) * 100;
|
||||||
pg = ezpg({
|
pg = ezpg({
|
||||||
target: pg,
|
target: pg,
|
||||||
width: width
|
width: width
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
return xhr;
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
// Refresh modal
|
|
||||||
pg = ezpg({
|
|
||||||
target: pg,
|
|
||||||
width: 100,
|
|
||||||
});
|
|
||||||
setTimeout(
|
|
||||||
function () {
|
|
||||||
pg.modal('hide');
|
|
||||||
}, 500
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cb) {
|
|
||||||
cb(data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
// Refresh modal
|
||||||
|
pg = ezpg({
|
||||||
|
target: pg,
|
||||||
|
width: 100
|
||||||
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
pg.modal("hide");
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
$(document).ready(function(){
|
$(document).ready(function() {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,47 +1,51 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#notifications_form').submit(function(e){
|
$("#notifications_form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var form = $('#notifications_form');
|
var form = $("#notifications_form");
|
||||||
var params = form.serializeJSON();
|
var params = form.serializeJSON();
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/notifications', {
|
CTFd.fetch("/api/v1/notifications", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.reload();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".delete-notification").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var elem = $(this);
|
||||||
|
var notif_id = elem.attr("notif-id");
|
||||||
|
|
||||||
|
ezq({
|
||||||
|
title: "Delete Notification",
|
||||||
|
body: "Are you sure you want to delete this notification?",
|
||||||
|
success: function() {
|
||||||
|
CTFd.fetch("/api/v1/notifications/" + notif_id, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}).then(function (response) {
|
})
|
||||||
|
.then(function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setTimeout(function () {
|
elem.parent().remove();
|
||||||
window.location.reload();
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
$('.delete-notification').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var elem = $(this);
|
|
||||||
var notif_id = elem.attr("notif-id");
|
|
||||||
|
|
||||||
ezq({
|
|
||||||
title: 'Delete Notification',
|
|
||||||
body: "Are you sure you want to delete this notification?",
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/notifications/' + notif_id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
elem.parent().remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,198 +1,214 @@
|
||||||
var editor = CodeMirror.fromTextArea(
|
var editor = CodeMirror.fromTextArea(
|
||||||
document.getElementById("admin-pages-editor"), {
|
document.getElementById("admin-pages-editor"),
|
||||||
lineNumbers: true,
|
{
|
||||||
lineWrapping: true,
|
lineNumbers: true,
|
||||||
mode: "xml",
|
lineWrapping: true,
|
||||||
htmlMode: true,
|
mode: "xml",
|
||||||
}
|
htmlMode: true
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function show_files(data) {
|
function show_files(data) {
|
||||||
var list = $('#media-library-list');
|
var list = $("#media-library-list");
|
||||||
list.empty();
|
list.empty();
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var f = data[i];
|
var f = data[i];
|
||||||
var fname = f.location.split('/').pop();
|
var fname = f.location.split("/").pop();
|
||||||
var ext = get_filetype_icon_class(f.location);
|
var ext = get_filetype_icon_class(f.location);
|
||||||
|
|
||||||
var wrapper = $('<div>').attr('class', 'media-item-wrapper');
|
var wrapper = $("<div>").attr("class", "media-item-wrapper");
|
||||||
|
|
||||||
var link = $('<a>');
|
var link = $("<a>");
|
||||||
link.attr('href', '##');
|
link.attr("href", "##");
|
||||||
|
|
||||||
if (ext === undefined){
|
if (ext === undefined) {
|
||||||
link.append('<i class="far fa-file" aria-hidden="true"></i> '.format(ext));
|
link.append(
|
||||||
} else {
|
'<i class="far fa-file" aria-hidden="true"></i> '.format(ext)
|
||||||
link.append('<i class="far {0}" aria-hidden="true"></i> '.format(ext));
|
);
|
||||||
}
|
} else {
|
||||||
|
link.append('<i class="far {0}" aria-hidden="true"></i> '.format(ext));
|
||||||
link.append($('<small>').attr('class', 'media-item-title').text(fname));
|
|
||||||
|
|
||||||
link.click(function (e) {
|
|
||||||
var media_div = $(this).parent();
|
|
||||||
var icon = $(this).find('i')[0];
|
|
||||||
var f_loc = media_div.attr('data-location');
|
|
||||||
var fname = media_div.attr('data-filename');
|
|
||||||
var f_id = media_div.attr('data-id');
|
|
||||||
$('#media-delete').attr('data-id', f_id);
|
|
||||||
$('#media-link').val(f_loc);
|
|
||||||
$('#media-filename').html(
|
|
||||||
$('<a>').attr('href', f_loc).attr('target', '_blank').text(fname)
|
|
||||||
);
|
|
||||||
|
|
||||||
$('#media-icon').empty();
|
|
||||||
if ($(icon).hasClass('fa-file-image')) {
|
|
||||||
$('#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
|
|
||||||
var copy_icon = $(icon).clone();
|
|
||||||
$(copy_icon).addClass('fa-4x');
|
|
||||||
$('#media-icon').append(copy_icon);
|
|
||||||
}
|
|
||||||
$('#media-item').show();
|
|
||||||
});
|
|
||||||
wrapper.append(link);
|
|
||||||
wrapper.attr('data-location', script_root + '/files/' + f.location);
|
|
||||||
wrapper.attr('data-id', f.id);
|
|
||||||
wrapper.attr('data-filename', fname);
|
|
||||||
list.append(wrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link.append(
|
||||||
|
$("<small>")
|
||||||
|
.attr("class", "media-item-title")
|
||||||
|
.text(fname)
|
||||||
|
);
|
||||||
|
|
||||||
|
link.click(function(e) {
|
||||||
|
var media_div = $(this).parent();
|
||||||
|
var icon = $(this).find("i")[0];
|
||||||
|
var f_loc = media_div.attr("data-location");
|
||||||
|
var fname = media_div.attr("data-filename");
|
||||||
|
var f_id = media_div.attr("data-id");
|
||||||
|
$("#media-delete").attr("data-id", f_id);
|
||||||
|
$("#media-link").val(f_loc);
|
||||||
|
$("#media-filename").html(
|
||||||
|
$("<a>")
|
||||||
|
.attr("href", f_loc)
|
||||||
|
.attr("target", "_blank")
|
||||||
|
.text(fname)
|
||||||
|
);
|
||||||
|
|
||||||
|
$("#media-icon").empty();
|
||||||
|
if ($(icon).hasClass("fa-file-image")) {
|
||||||
|
$("#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
|
||||||
|
var copy_icon = $(icon).clone();
|
||||||
|
$(copy_icon).addClass("fa-4x");
|
||||||
|
$("#media-icon").append(copy_icon);
|
||||||
|
}
|
||||||
|
$("#media-item").show();
|
||||||
|
});
|
||||||
|
wrapper.append(link);
|
||||||
|
wrapper.attr("data-location", script_root + "/files/" + f.location);
|
||||||
|
wrapper.attr("data-id", f.id);
|
||||||
|
wrapper.attr("data-filename", fname);
|
||||||
|
list.append(wrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refresh_files(cb) {
|
||||||
function refresh_files(cb){
|
get_page_files().then(function(response) {
|
||||||
get_page_files().then(function (response) {
|
var data = response.data;
|
||||||
var data = response.data;
|
show_files(data);
|
||||||
show_files(data);
|
if (cb) {
|
||||||
if (cb) {
|
cb();
|
||||||
cb();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function insert_at_cursor(editor, text) {
|
function insert_at_cursor(editor, text) {
|
||||||
var doc = editor.getDoc();
|
var doc = editor.getDoc();
|
||||||
var cursor = doc.getCursor();
|
var cursor = doc.getCursor();
|
||||||
doc.replaceRange(text, cursor);
|
doc.replaceRange(text, cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit_form() {
|
function submit_form() {
|
||||||
editor.save(); // Save the CodeMirror data to the Textarea
|
editor.save(); // Save the CodeMirror data to the Textarea
|
||||||
var params = $("#page-edit").serializeJSON();
|
var params = $("#page-edit").serializeJSON();
|
||||||
var target = '/api/v1/pages';
|
var target = "/api/v1/pages";
|
||||||
var method = 'POST';
|
var method = "POST";
|
||||||
|
|
||||||
if (params.id){
|
if (params.id) {
|
||||||
target += '/' + params.id;
|
target += "/" + params.id;
|
||||||
method = 'PATCH';
|
method = "PATCH";
|
||||||
}
|
}
|
||||||
|
|
||||||
CTFd.fetch(target, {
|
CTFd.fetch(target, {
|
||||||
method: method,
|
method: method,
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function(response){
|
return response.json();
|
||||||
if (method === 'PATCH' && response.success) {
|
})
|
||||||
ezal({
|
.then(function(response) {
|
||||||
title: 'Saved',
|
if (method === "PATCH" && response.success) {
|
||||||
body: 'Your changes have been saved',
|
ezal({
|
||||||
button: 'Okay'
|
title: "Saved",
|
||||||
});
|
body: "Your changes have been saved",
|
||||||
} else {
|
button: "Okay"
|
||||||
window.location = script_root + '/admin/pages/' + response.data.id;
|
});
|
||||||
}
|
} else {
|
||||||
|
window.location = script_root + "/admin/pages/" + response.data.id;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function preview_page() {
|
function preview_page() {
|
||||||
editor.save(); // Save the CodeMirror data to the Textarea
|
editor.save(); // Save the CodeMirror data to the Textarea
|
||||||
$('#page-edit').attr('action', script_root + '/admin/pages/preview');
|
$("#page-edit").attr("action", script_root + "/admin/pages/preview");
|
||||||
$('#page-edit').attr('target', '_blank');
|
$("#page-edit").attr("target", "_blank");
|
||||||
$('#page-edit').submit();
|
$("#page-edit").submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function upload_media() {
|
function upload_media() {
|
||||||
upload_files($('#media-library-upload'), function (data) {
|
upload_files($("#media-library-upload"), function(data) {
|
||||||
refresh_files();
|
refresh_files();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$("#media-insert").click(function(e) {
|
||||||
|
var tag = "";
|
||||||
|
try {
|
||||||
|
tag = $("#media-icon")
|
||||||
|
.children()[0]
|
||||||
|
.nodeName.toLowerCase();
|
||||||
|
} catch (err) {
|
||||||
|
tag = "";
|
||||||
|
}
|
||||||
|
var link = $("#media-link").val();
|
||||||
|
var fname = $("#media-filename").text();
|
||||||
|
var entry = null;
|
||||||
|
if (tag === "img") {
|
||||||
|
entry = "![{0}]({1})".format(fname, link);
|
||||||
|
} else {
|
||||||
|
entry = "[{0}]({1})".format(fname, link);
|
||||||
|
}
|
||||||
|
insert_at_cursor(editor, entry);
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(function () {
|
$("#media-download").click(function(e) {
|
||||||
$('#media-insert').click(function (e) {
|
var link = $("#media-link").val();
|
||||||
var tag = '';
|
window.open(link, "_blank");
|
||||||
try {
|
});
|
||||||
tag = $('#media-icon').children()[0].nodeName.toLowerCase();
|
|
||||||
} catch (err) {
|
|
||||||
tag = '';
|
|
||||||
}
|
|
||||||
var link = $('#media-link').val();
|
|
||||||
var fname = $('#media-filename').text();
|
|
||||||
var entry = null;
|
|
||||||
if (tag === 'img') {
|
|
||||||
entry = '![{0}]({1})'.format(fname, link);
|
|
||||||
} else {
|
|
||||||
entry = '[{0}]({1})'.format(fname, link);
|
|
||||||
}
|
|
||||||
insert_at_cursor(editor, entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#media-download').click(function (e) {
|
$("#media-delete").click(function(e) {
|
||||||
var link = $('#media-link').val();
|
var file_id = $(this).attr("data-id");
|
||||||
window.open(link, "_blank");
|
ezq({
|
||||||
});
|
title: "Delete File?",
|
||||||
|
body: "Are you sure you want to delete this file?",
|
||||||
$('#media-delete').click(function (e) {
|
success: function() {
|
||||||
var file_id = $(this).attr('data-id');
|
CTFd.fetch("/api/v1/files/" + file_id, {
|
||||||
ezq({
|
method: "DELETE",
|
||||||
title: "Delete File?",
|
credentials: "same-origin",
|
||||||
body: "Are you sure you want to delete this file?",
|
headers: {
|
||||||
success: function () {
|
Accept: "application/json",
|
||||||
CTFd.fetch('/api/v1/files/' + file_id, {
|
"Content-Type": "application/json"
|
||||||
method: 'DELETE',
|
}
|
||||||
credentials: 'same-origin',
|
}).then(function(response) {
|
||||||
headers: {
|
if (response.status === 200) {
|
||||||
'Accept': 'application/json',
|
response.json().then(function(object) {
|
||||||
'Content-Type': 'application/json'
|
if (object.success) {
|
||||||
},
|
refresh_files();
|
||||||
}).then(function (response) {
|
}
|
||||||
if (response.status === 200) {
|
});
|
||||||
response.json().then(function (object) {
|
}
|
||||||
if (object.success) {
|
|
||||||
refresh_files();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('#save-page').click(function (e) {
|
$("#save-page").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submit_form();
|
submit_form();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#media-button').click(function () {
|
$("#media-button").click(function() {
|
||||||
$('#media-library-list').empty();
|
$("#media-library-list").empty();
|
||||||
refresh_files(function(){
|
refresh_files(function() {
|
||||||
$('#media-modal').modal('show');
|
$("#media-modal").modal("show");
|
||||||
});
|
|
||||||
// get_page_files().then(function (data) {
|
|
||||||
// var files = data;
|
|
||||||
// console.log(files);
|
|
||||||
// $('#media-modal').modal();
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
});
|
// get_page_files().then(function (data) {
|
||||||
|
// var files = data;
|
||||||
|
// console.log(files);
|
||||||
|
// $('#media-modal').modal();
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,61 +1,59 @@
|
||||||
function get_filetype_icon_class(filename){
|
function get_filetype_icon_class(filename) {
|
||||||
var mapping = {
|
var mapping = {
|
||||||
// Image Files
|
// Image Files
|
||||||
'png': 'fa-file-image',
|
png: "fa-file-image",
|
||||||
'jpg': 'fa-file-image',
|
jpg: "fa-file-image",
|
||||||
'jpeg': 'fa-file-image',
|
jpeg: "fa-file-image",
|
||||||
'gif': 'fa-file-image',
|
gif: "fa-file-image",
|
||||||
'bmp': 'fa-file-image',
|
bmp: "fa-file-image",
|
||||||
'svg': 'fa-file-image',
|
svg: "fa-file-image",
|
||||||
|
|
||||||
// Text Files
|
// Text Files
|
||||||
'txt': 'fa-file-alt',
|
txt: "fa-file-alt",
|
||||||
|
|
||||||
// Video Files
|
// Video Files
|
||||||
'mov': 'fa-file-video',
|
mov: "fa-file-video",
|
||||||
'mp4': 'fa-file-video',
|
mp4: "fa-file-video",
|
||||||
'wmv': 'fa-file-video',
|
wmv: "fa-file-video",
|
||||||
'flv': 'fa-file-video',
|
flv: "fa-file-video",
|
||||||
'mkv': 'fa-file-video',
|
mkv: "fa-file-video",
|
||||||
'avi': 'fa-file-video',
|
avi: "fa-file-video",
|
||||||
|
|
||||||
// PDF Files
|
// PDF Files
|
||||||
'pdf': 'fa-file-pdf',
|
pdf: "fa-file-pdf",
|
||||||
|
|
||||||
// Audio Files
|
// Audio Files
|
||||||
'mp3': 'fa-file-sound',
|
mp3: "fa-file-sound",
|
||||||
'wav': 'fa-file-sound',
|
wav: "fa-file-sound",
|
||||||
'aac': 'fa-file-sound',
|
aac: "fa-file-sound",
|
||||||
|
|
||||||
// Archive Files
|
// Archive Files
|
||||||
'zip': 'fa-file-archive',
|
zip: "fa-file-archive",
|
||||||
'gz': 'fa-file-archive',
|
gz: "fa-file-archive",
|
||||||
'tar': 'fa-file-archive',
|
tar: "fa-file-archive",
|
||||||
'7z': 'fa-file-archive',
|
"7z": "fa-file-archive",
|
||||||
'rar': 'fa-file-archive',
|
rar: "fa-file-archive",
|
||||||
|
|
||||||
// Code Files
|
// Code Files
|
||||||
'py': 'fa-file-code',
|
py: "fa-file-code",
|
||||||
'c': 'fa-file-code',
|
c: "fa-file-code",
|
||||||
'cpp': 'fa-file-code',
|
cpp: "fa-file-code",
|
||||||
'html': 'fa-file-code',
|
html: "fa-file-code",
|
||||||
'js': 'fa-file-code',
|
js: "fa-file-code",
|
||||||
'rb': 'fa-file-code',
|
rb: "fa-file-code",
|
||||||
'go': 'fa-file-code'
|
go: "fa-file-code"
|
||||||
};
|
};
|
||||||
|
|
||||||
var ext = filename.split('.').pop();
|
var ext = filename.split(".").pop();
|
||||||
return mapping[ext];
|
return mapping[ext];
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_page_files(){
|
function get_page_files() {
|
||||||
return CTFd.fetch(
|
return CTFd.fetch("/api/v1/files?type=page", {
|
||||||
'/api/v1/files?type=page', {
|
credentials: "same-origin"
|
||||||
credentials: 'same-origin',
|
}).then(function(response) {
|
||||||
}
|
return response.json();
|
||||||
).then(function (response) {
|
});
|
||||||
return response.json();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// .
|
// .
|
||||||
|
@ -63,4 +61,4 @@ function get_page_files(){
|
||||||
// data.map(function (f) {
|
// data.map(function (f) {
|
||||||
// console.log(f);
|
// console.log(f);
|
||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('.delete-page').click(function () {
|
$(".delete-page").click(function() {
|
||||||
var elem = $(this);
|
var elem = $(this);
|
||||||
var name = elem.attr("page-route");
|
var name = elem.attr("page-route");
|
||||||
var page_id = elem.attr("page-id");
|
var page_id = elem.attr("page-id");
|
||||||
ezq({
|
ezq({
|
||||||
title: 'Delete ' + name,
|
title: "Delete " + name,
|
||||||
body: "Are you sure you want to delete {0}?".format(
|
body: "Are you sure you want to delete {0}?".format(
|
||||||
"<strong>" + htmlentities(name) + "</strong>"
|
"<strong>" + htmlentities(name) + "</strong>"
|
||||||
),
|
),
|
||||||
success: function () {
|
success: function() {
|
||||||
CTFd.fetch('/api/v1/pages/' + page_id, {
|
CTFd.fetch("/api/v1/pages/" + page_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
elem.parent().parent().remove();
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
});
|
elem
|
||||||
|
.parent()
|
||||||
|
.parent()
|
||||||
|
.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,39 +1,41 @@
|
||||||
function toggle_account(elem) {
|
function toggle_account(elem) {
|
||||||
var btn = $(elem);
|
var btn = $(elem);
|
||||||
var teamId = btn.attr('team-id');
|
var teamId = btn.attr("team-id");
|
||||||
var state = btn.attr('state');
|
var state = btn.attr("state");
|
||||||
var hidden = undefined;
|
var hidden = undefined;
|
||||||
if (state == "visible") {
|
if (state == "visible") {
|
||||||
hidden = true;
|
hidden = true;
|
||||||
} else if (state == "hidden") {
|
} else if (state == "hidden") {
|
||||||
hidden = false;
|
hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
'hidden': hidden
|
hidden: hidden
|
||||||
};
|
};
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/'+ user_mode +'/' + teamId, {
|
CTFd.fetch("/api/v1/" + user_mode + "/" + teamId, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
if (hidden) {
|
.then(function(response) {
|
||||||
btn.attr('state', 'hidden');
|
if (response.success) {
|
||||||
btn.addClass('btn-danger').removeClass('btn-success');
|
if (hidden) {
|
||||||
btn.text('Hidden');
|
btn.attr("state", "hidden");
|
||||||
} else {
|
btn.addClass("btn-danger").removeClass("btn-success");
|
||||||
btn.attr('state', 'visible');
|
btn.text("Hidden");
|
||||||
btn.addClass('btn-success').removeClass('btn-danger');
|
} else {
|
||||||
btn.text('Visible');
|
btn.attr("state", "visible");
|
||||||
}
|
btn.addClass("btn-success").removeClass("btn-danger");
|
||||||
|
btn.text("Visible");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,202 +1,222 @@
|
||||||
function solves_graph() {
|
function solves_graph() {
|
||||||
$.get(script_root + '/api/v1/statistics/challenges/solves', function (response) {
|
$.get(script_root + "/api/v1/statistics/challenges/solves", function(
|
||||||
var data = response.data;
|
response
|
||||||
var res = $.parseJSON(JSON.stringify(data));
|
) {
|
||||||
var chals = [];
|
var data = response.data;
|
||||||
var counts = [];
|
var res = $.parseJSON(JSON.stringify(data));
|
||||||
var colors = [];
|
var chals = [];
|
||||||
var annotations = [];
|
var counts = [];
|
||||||
var i = 1;
|
var colors = [];
|
||||||
var solves = {};
|
var annotations = [];
|
||||||
for (var c = 0; c < res.length; c++) {
|
var i = 1;
|
||||||
solves[res[c]['id']] = {
|
var solves = {};
|
||||||
name: res[c]['name'],
|
for (var c = 0; c < res.length; c++) {
|
||||||
solves: res[c]['solves'],
|
solves[res[c]["id"]] = {
|
||||||
};
|
name: res[c]["name"],
|
||||||
}
|
solves: res[c]["solves"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var solves_order = Object.keys(solves).sort(function (a, b) {
|
var solves_order = Object.keys(solves).sort(function(a, b) {
|
||||||
return solves[b].solves - solves[a].solves
|
return solves[b].solves - solves[a].solves;
|
||||||
});
|
});
|
||||||
|
|
||||||
$.each(solves_order, function (key, value) {
|
$.each(solves_order, function(key, value) {
|
||||||
chals.push(solves[value].name);
|
chals.push(solves[value].name);
|
||||||
counts.push(solves[value].solves);
|
counts.push(solves[value].solves);
|
||||||
/*colors.push(colorhash(value));*/
|
/*colors.push(colorhash(value));*/
|
||||||
var result = {
|
var result = {
|
||||||
x: solves[value].name,
|
x: solves[value].name,
|
||||||
y: solves[value].solves,
|
y: solves[value].solves,
|
||||||
text: solves[value].solves,
|
text: solves[value].solves,
|
||||||
xanchor: 'center',
|
xanchor: "center",
|
||||||
yanchor: 'bottom',
|
yanchor: "bottom",
|
||||||
showarrow: false,
|
showarrow: false
|
||||||
};
|
};
|
||||||
annotations.push(result);
|
annotations.push(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
var data = [{
|
var data = [
|
||||||
type: 'bar',
|
{
|
||||||
x: chals,
|
type: "bar",
|
||||||
y: counts,
|
x: chals,
|
||||||
text: counts,
|
y: counts,
|
||||||
orientation: 'v',
|
text: counts,
|
||||||
/*marker: {
|
orientation: "v"
|
||||||
|
/*marker: {
|
||||||
color: colors
|
color: colors
|
||||||
},*/
|
},*/
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Solve Counts',
|
title: "Solve Counts",
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
xaxis: {
|
xaxis: {
|
||||||
title: 'Challenge Name'
|
title: "Challenge Name"
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
title: 'Amount of Solves'
|
title: "Amount of Solves"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#solves-graph').empty();
|
$("#solves-graph").empty();
|
||||||
document.getElementById('solves-graph').fn = 'CTFd_solves_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("solves-graph").fn =
|
||||||
Plotly.newPlot('solves-graph', data, layout);
|
"CTFd_solves_" + new Date().toISOString().slice(0, 19);
|
||||||
});
|
Plotly.newPlot("solves-graph", data, layout);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function keys_percentage_graph() {
|
function keys_percentage_graph() {
|
||||||
// Solves and Fails pie chart
|
// Solves and Fails pie chart
|
||||||
$.get(script_root + '/api/v1/statistics/submissions/type', function (response) {
|
$.get(script_root + "/api/v1/statistics/submissions/type", function(
|
||||||
var data = response.data;
|
response
|
||||||
var res = $.parseJSON(JSON.stringify(data));
|
) {
|
||||||
var solves = res['correct'];
|
var data = response.data;
|
||||||
var fails = res['incorrect'];
|
var res = $.parseJSON(JSON.stringify(data));
|
||||||
|
var solves = res["correct"];
|
||||||
|
var fails = res["incorrect"];
|
||||||
|
|
||||||
var data = [{
|
var data = [
|
||||||
values: [solves, fails],
|
{
|
||||||
labels: ['Correct', 'Incorrect'],
|
values: [solves, fails],
|
||||||
marker: {
|
labels: ["Correct", "Incorrect"],
|
||||||
colors: [
|
marker: {
|
||||||
"rgb(0, 209, 64)",
|
colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
|
||||||
"rgb(207, 38, 0)"
|
},
|
||||||
]
|
text: ["Solves", "Fails"],
|
||||||
},
|
hole: 0.4,
|
||||||
text: ['Solves', 'Fails'],
|
type: "pie"
|
||||||
hole: .4,
|
}
|
||||||
type: 'pie'
|
];
|
||||||
}];
|
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Submission Percentages'
|
title: "Submission Percentages"
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#keys-pie-graph').empty();
|
$("#keys-pie-graph").empty();
|
||||||
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("keys-pie-graph").fn =
|
||||||
Plotly.newPlot('keys-pie-graph', data, layout);
|
"CTFd_submissions_" + new Date().toISOString().slice(0, 19);
|
||||||
});
|
Plotly.newPlot("keys-pie-graph", data, layout);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function category_breakdown_graph() {
|
function category_breakdown_graph() {
|
||||||
$.get(script_root + '/api/v1/statistics/challenges/category', function (response) {
|
$.get(script_root + "/api/v1/statistics/challenges/category", function(
|
||||||
var data = response.data;
|
response
|
||||||
var res = $.parseJSON(JSON.stringify(data));
|
) {
|
||||||
|
var data = response.data;
|
||||||
|
var res = $.parseJSON(JSON.stringify(data));
|
||||||
|
|
||||||
var categories = [];
|
var categories = [];
|
||||||
var count = [];
|
var count = [];
|
||||||
|
|
||||||
for (var category in res) {
|
for (var category in res) {
|
||||||
if (res.hasOwnProperty(category)) {
|
if (res.hasOwnProperty(category)) {
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
count.push(res[category]);
|
count.push(res[category]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < res.length; i++) {
|
for (var i = 0; i < res.length; i++) {
|
||||||
categories.push(res[i].category);
|
categories.push(res[i].category);
|
||||||
count.push(res[i].count);
|
count.push(res[i].count);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = [{
|
var data = [
|
||||||
values: count,
|
{
|
||||||
labels: categories,
|
values: count,
|
||||||
hole: .4,
|
labels: categories,
|
||||||
type: 'pie'
|
hole: 0.4,
|
||||||
}];
|
type: "pie"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Category Breakdown'
|
title: "Category Breakdown"
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#categories-pie-graph').empty();
|
$("#categories-pie-graph").empty();
|
||||||
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("categories-pie-graph").fn =
|
||||||
Plotly.newPlot('categories-pie-graph', data, layout);
|
"CTFd_categories_" + new Date().toISOString().slice(0, 19);
|
||||||
});
|
Plotly.newPlot("categories-pie-graph", data, layout);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function solve_percentages_graph() {
|
function solve_percentages_graph() {
|
||||||
$.get(script_root + '/api/v1/statistics/challenges/solves/percentages', function (response) {
|
$.get(
|
||||||
var res = response.data;
|
script_root + "/api/v1/statistics/challenges/solves/percentages",
|
||||||
|
function(response) {
|
||||||
|
var res = response.data;
|
||||||
|
|
||||||
var names = [];
|
var names = [];
|
||||||
var percents = [];
|
var percents = [];
|
||||||
var labels = [];
|
var labels = [];
|
||||||
|
|
||||||
var annotations = [];
|
var annotations = [];
|
||||||
|
|
||||||
for (var key in res) {
|
for (var key in res) {
|
||||||
names.push(res[key].name);
|
names.push(res[key].name);
|
||||||
percents.push((res[key].percentage * 100));
|
percents.push(res[key].percentage * 100);
|
||||||
|
|
||||||
var result = {
|
var result = {
|
||||||
x: res[key].name,
|
x: res[key].name,
|
||||||
y: (res[key].percentage * 100),
|
y: res[key].percentage * 100,
|
||||||
text: Math.round(res[key].percentage * 100) + '%',
|
text: Math.round(res[key].percentage * 100) + "%",
|
||||||
xanchor: 'center',
|
xanchor: "center",
|
||||||
yanchor: 'bottom',
|
yanchor: "bottom",
|
||||||
showarrow: false,
|
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 {0} (%)'.format(user_mode.charAt(0).toUpperCase() + user_mode.slice(1)),
|
|
||||||
range: [0, 100]
|
|
||||||
},
|
|
||||||
annotations: annotations
|
|
||||||
};
|
};
|
||||||
|
annotations.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
$('#solve-percentages-graph').empty();
|
var data = [
|
||||||
document.getElementById('solve-percentages-graph').fn = 'CTFd_challenge_percentages_' + (new Date).toISOString().slice(0, 19);
|
{
|
||||||
Plotly.newPlot('solve-percentages-graph', data, layout);
|
type: "bar",
|
||||||
});
|
x: names,
|
||||||
|
y: percents,
|
||||||
|
orientation: "v"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var layout = {
|
||||||
|
title: "Solve Percentages per Challenge",
|
||||||
|
xaxis: {
|
||||||
|
title: "Challenge Name"
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: "Percentage of {0} (%)".format(
|
||||||
|
user_mode.charAt(0).toUpperCase() + user_mode.slice(1)
|
||||||
|
),
|
||||||
|
range: [0, 100]
|
||||||
|
},
|
||||||
|
annotations: annotations
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#solve-percentages-graph").empty();
|
||||||
|
document.getElementById("solve-percentages-graph").fn =
|
||||||
|
"CTFd_challenge_percentages_" + new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("solve-percentages-graph", data, layout);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
solves_graph();
|
solves_graph();
|
||||||
keys_percentage_graph();
|
keys_percentage_graph();
|
||||||
category_breakdown_graph();
|
category_breakdown_graph();
|
||||||
solve_percentages_graph();
|
solve_percentages_graph();
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function () {
|
$(function() {
|
||||||
update();
|
update();
|
||||||
window.onresize = function () {
|
window.onresize = function() {
|
||||||
console.log('resizing');
|
console.log("resizing");
|
||||||
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
|
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
|
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('solves-graph'));
|
Plotly.Plots.resize(document.getElementById("solves-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('solve-percentages-graph'));
|
Plotly.Plots.resize(document.getElementById("solve-percentages-graph"));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(update, 300000);
|
setInterval(update, 300000);
|
||||||
|
|
|
@ -1,47 +1,46 @@
|
||||||
$(".form-control").bind({
|
$(".form-control").bind({
|
||||||
focus: function() {
|
focus: function() {
|
||||||
$(this).addClass('input-filled-valid' );
|
$(this).addClass("input-filled-valid");
|
||||||
},
|
},
|
||||||
blur: function() {
|
blur: function() {
|
||||||
if ($(this).val() === '') {
|
if ($(this).val() === "") {
|
||||||
$(this).removeClass('input-filled-valid' );
|
$(this).removeClass("input-filled-valid");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.modal').on('show.bs.modal', function (e) {
|
$(".modal").on("show.bs.modal", function(e) {
|
||||||
$('.form-control').each(function () {
|
$(".form-control").each(function() {
|
||||||
if ($(this).val()) {
|
if ($(this).val()) {
|
||||||
$(this).addClass("input-filled-valid");
|
$(this).addClass("input-filled-valid");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(function () {
|
$(function() {
|
||||||
$('.form-control').each(function () {
|
$(".form-control").each(function() {
|
||||||
if ($(this).val()) {
|
if ($(this).val()) {
|
||||||
$(this).addClass("input-filled-valid");
|
$(this).addClass("input-filled-valid");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("tr").click(function () {
|
$("tr").click(function() {
|
||||||
var sel = getSelection().toString();
|
var sel = getSelection().toString();
|
||||||
if (!sel) {
|
if (!sel) {
|
||||||
var href = $(this).attr('data-href');
|
var href = $(this).attr("data-href");
|
||||||
if (href) {
|
if (href) {
|
||||||
window.location = href;
|
window.location = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$("tr a, button").click(function (e) {
|
$("tr a, button").click(function(e) {
|
||||||
// TODO: This is a hack to allow modal close buttons to work
|
// TODO: This is a hack to allow modal close buttons to work
|
||||||
if (!$(this).attr('data-dismiss')) {
|
if (!$(this).attr("data-dismiss")) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,32 +1,44 @@
|
||||||
// TODO: Replace this with CTFd JS library
|
// TODO: Replace this with CTFd JS library
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('.delete-correct-submission').click(function () {
|
$(".delete-correct-submission").click(function() {
|
||||||
var elem = $(this).parent().parent();
|
var elem = $(this)
|
||||||
var chal = elem.find('.chal').attr('id');
|
.parent()
|
||||||
var chal_name = elem.find('.chal').text().trim();
|
.parent();
|
||||||
var team = elem.find('.team').attr('id');
|
var chal = elem.find(".chal").attr("id");
|
||||||
var team_name = elem.find('.team').text().trim();
|
var chal_name = elem
|
||||||
var key_id = elem.find('.flag').attr('id');
|
.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");
|
||||||
|
|
||||||
var td_row = $(this).parent().parent();
|
var td_row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
ezq({
|
ezq({
|
||||||
title: 'Delete Submission',
|
title: "Delete Submission",
|
||||||
body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format(
|
body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format(
|
||||||
"<strong>" + htmlentities(team_name) + "</strong>",
|
"<strong>" + htmlentities(team_name) + "</strong>",
|
||||||
"<strong>" + htmlentities(chal_name) + "</strong>"
|
"<strong>" + htmlentities(chal_name) + "</strong>"
|
||||||
),
|
),
|
||||||
success: function () {
|
success: function() {
|
||||||
CTFd.fetch('/api/v1/submissions/' + key_id, {
|
CTFd.fetch("/api/v1/submissions/" + key_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
td_row.remove();
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
});
|
td_row.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,97 +1,109 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('.edit-team').click(function (e) {
|
$(".edit-team").click(function(e) {
|
||||||
$('#team-info-modal').modal('toggle');
|
$("#team-info-modal").modal("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.edit-captain').click(function (e) {
|
$(".edit-captain").click(function(e) {
|
||||||
$('#team-captain-modal').modal('toggle');
|
$("#team-captain-modal").modal("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.delete-team').click(function (e) {
|
$(".delete-team").click(function(e) {
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete Team",
|
title: "Delete Team",
|
||||||
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(TEAM_NAME) + "</strong>"),
|
body: "Are you sure you want to delete {0}".format(
|
||||||
success: function () {
|
"<strong>" + htmlentities(TEAM_NAME) + "</strong>"
|
||||||
CTFd.fetch('/api/v1/teams/' + TEAM_ID, {
|
),
|
||||||
method: 'DELETE',
|
success: function() {
|
||||||
}).then(function (response) {
|
CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
|
||||||
return response.json();
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
})
|
||||||
if (response.success) {
|
.then(function(response) {
|
||||||
window.location = script_root + '/admin/teams';
|
return response.json();
|
||||||
}
|
})
|
||||||
});
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location = script_root + "/admin/teams";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.delete-submission').click(function (e) {
|
$(".delete-submission").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var submission_id = $(this).attr('submission-id');
|
var submission_id = $(this).attr("submission-id");
|
||||||
var submission_type = $(this).attr('submission-type');
|
var submission_type = $(this).attr("submission-type");
|
||||||
var submission_challenge = $(this).attr('submission-challenge');
|
var submission_challenge = $(this).attr("submission-challenge");
|
||||||
|
|
||||||
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
|
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
|
||||||
htmlentities(submission_type),
|
htmlentities(submission_type),
|
||||||
htmlentities(TEAM_NAME),
|
htmlentities(TEAM_NAME),
|
||||||
htmlentities(submission_challenge)
|
htmlentities(submission_challenge)
|
||||||
);
|
);
|
||||||
|
|
||||||
var row = $(this).parent().parent();
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete Submission",
|
title: "Delete Submission",
|
||||||
body: body,
|
body: body,
|
||||||
success: function () {
|
success: function() {
|
||||||
CTFd.fetch('/api/v1/submissions/' + submission_id, {
|
CTFd.fetch("/api/v1/submissions/" + submission_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
row.remove();
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
});
|
row.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('.delete-award').click(function (e) {
|
$(".delete-award").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var award_id = $(this).attr('award-id');
|
var award_id = $(this).attr("award-id");
|
||||||
var award_name = $(this).attr('award-name');
|
var award_name = $(this).attr("award-name");
|
||||||
|
|
||||||
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
|
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
|
||||||
htmlentities(award_name),
|
htmlentities(award_name),
|
||||||
htmlentities(TEAM_NAME)
|
htmlentities(TEAM_NAME)
|
||||||
);
|
);
|
||||||
|
|
||||||
var row = $(this).parent().parent();
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete Award",
|
title: "Delete Award",
|
||||||
body: body,
|
body: body,
|
||||||
success: function () {
|
success: function() {
|
||||||
CTFd.fetch('/api/v1/awards/' + award_id, {
|
CTFd.fetch("/api/v1/awards/" + award_id, {
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
row.remove();
|
.then(function(response) {
|
||||||
}
|
if (response.success) {
|
||||||
});
|
row.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,159 +1,177 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
function scoregraph() {
|
function scoregraph() {
|
||||||
var times = [];
|
var times = [];
|
||||||
var scores = [];
|
var scores = [];
|
||||||
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/solves', function (solve_data) {
|
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/solves", function(
|
||||||
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/awards', function (award_data) {
|
solve_data
|
||||||
var solves = solve_data.data;
|
) {
|
||||||
var awards = award_data.data;
|
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/awards", function(
|
||||||
|
award_data
|
||||||
|
) {
|
||||||
|
var solves = solve_data.data;
|
||||||
|
var awards = award_data.data;
|
||||||
|
|
||||||
var total = solves.concat(awards);
|
var total = solves.concat(awards);
|
||||||
|
|
||||||
total.sort(function (a, b) {
|
total.sort(function(a, b) {
|
||||||
return new Date(a.date) - new Date(b.date);
|
return new Date(a.date) - new Date(b.date);
|
||||||
});
|
|
||||||
|
|
||||||
for (var i = 0; i < total.length; i++) {
|
|
||||||
var date = moment(total[i].date);
|
|
||||||
times.push(date.toDate());
|
|
||||||
try {
|
|
||||||
scores.push(total[i].challenge.value);
|
|
||||||
} catch (e) {
|
|
||||||
scores.push(total[i].value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scores = cumulativesum(scores);
|
|
||||||
|
|
||||||
var data = [
|
|
||||||
{
|
|
||||||
x: times,
|
|
||||||
y: scores,
|
|
||||||
type: 'scatter',
|
|
||||||
marker: {
|
|
||||||
color: colorhash(TEAM_NAME + TEAM_ID),
|
|
||||||
},
|
|
||||||
line: {
|
|
||||||
color: colorhash(TEAM_NAME + TEAM_ID),
|
|
||||||
},
|
|
||||||
fill: 'tozeroy'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
var layout = {
|
|
||||||
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();
|
|
||||||
document.getElementById('score-graph').fn = 'CTFd_score_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
|
|
||||||
Plotly.newPlot('score-graph', data, layout);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function keys_percentage_graph() {
|
for (var i = 0; i < total.length; i++) {
|
||||||
var base_url = script_root + '/api/v1/teams/' + TEAM_ID;
|
var date = moment(total[i].date);
|
||||||
$.get(base_url + '/fails', function (fails) {
|
times.push(date.toDate());
|
||||||
$.get(base_url + '/solves', function (solves) {
|
try {
|
||||||
var solves_count = solves.data.length;
|
scores.push(total[i].challenge.value);
|
||||||
var fails_count = fails.data.length;
|
} catch (e) {
|
||||||
|
scores.push(total[i].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scores = cumulativesum(scores);
|
||||||
|
|
||||||
var graph_data = [{
|
var data = [
|
||||||
values: [solves_count, fails_count],
|
{
|
||||||
labels: ['Solves', 'Fails'],
|
x: times,
|
||||||
marker: {
|
y: scores,
|
||||||
colors: [
|
type: "scatter",
|
||||||
"rgb(0, 209, 64)",
|
marker: {
|
||||||
"rgb(207, 38, 0)"
|
color: colorhash(TEAM_NAME + TEAM_ID)
|
||||||
]
|
},
|
||||||
},
|
line: {
|
||||||
hole: .4,
|
color: colorhash(TEAM_NAME + TEAM_ID)
|
||||||
type: 'pie'
|
},
|
||||||
}];
|
fill: "tozeroy"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Solve Percentages',
|
title: "Score over Time",
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
legend: {
|
hovermode: "closest",
|
||||||
"orientation": "h"
|
xaxis: {
|
||||||
}
|
showgrid: false,
|
||||||
};
|
showspikes: true
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
showgrid: false,
|
||||||
|
showspikes: true
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orientation: "h"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$('#keys-pie-graph').empty();
|
$("#score-graph").empty();
|
||||||
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("score-graph").fn =
|
||||||
Plotly.newPlot('keys-pie-graph', graph_data, layout);
|
"CTFd_score_team_" +
|
||||||
});
|
TEAM_ID +
|
||||||
});
|
"_" +
|
||||||
}
|
new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("score-graph", data, layout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function category_breakdown_graph() {
|
function keys_percentage_graph() {
|
||||||
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/solves', function (response) {
|
var base_url = script_root + "/api/v1/teams/" + TEAM_ID;
|
||||||
var solves = response.data;
|
$.get(base_url + "/fails", function(fails) {
|
||||||
|
$.get(base_url + "/solves", function(solves) {
|
||||||
|
var solves_count = solves.data.length;
|
||||||
|
var fails_count = fails.data.length;
|
||||||
|
|
||||||
var categories = [];
|
var graph_data = [
|
||||||
for (var i = 0; i < solves.length; i++) {
|
{
|
||||||
categories.push(solves[i].challenge.category)
|
values: [solves_count, fails_count],
|
||||||
}
|
labels: ["Solves", "Fails"],
|
||||||
|
marker: {
|
||||||
|
colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
|
||||||
|
},
|
||||||
|
hole: 0.4,
|
||||||
|
type: "pie"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
var keys = categories.filter(function (elem, pos) {
|
var layout = {
|
||||||
return categories.indexOf(elem) == pos;
|
title: "Solve Percentages",
|
||||||
});
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
legend: {
|
||||||
|
orientation: "h"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var counts = [];
|
$("#keys-pie-graph").empty();
|
||||||
for (var i = 0; i < keys.length; i++) {
|
document.getElementById("keys-pie-graph").fn =
|
||||||
var count = 0;
|
"CTFd_submissions_team_" +
|
||||||
for (var x = 0; x < categories.length; x++) {
|
TEAM_ID +
|
||||||
if (categories[x] == keys[i]) {
|
"_" +
|
||||||
count++;
|
new Date().toISOString().slice(0, 19);
|
||||||
}
|
Plotly.newPlot("keys-pie-graph", graph_data, layout);
|
||||||
}
|
});
|
||||||
counts.push(count)
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = [{
|
function category_breakdown_graph() {
|
||||||
values: counts,
|
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/solves", function(
|
||||||
labels: keys,
|
response
|
||||||
hole: .4,
|
) {
|
||||||
type: 'pie'
|
var solves = response.data;
|
||||||
}];
|
|
||||||
|
|
||||||
var layout = {
|
var categories = [];
|
||||||
title: 'Category Breakdown',
|
for (var i = 0; i < solves.length; i++) {
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
categories.push(solves[i].challenge.category);
|
||||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
}
|
||||||
legend: {
|
|
||||||
"orientation": "v"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#categories-pie-graph').empty();
|
var keys = categories.filter(function(elem, pos) {
|
||||||
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
|
return categories.indexOf(elem) == pos;
|
||||||
Plotly.newPlot('categories-pie-graph', data, layout);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
category_breakdown_graph();
|
var counts = [];
|
||||||
keys_percentage_graph();
|
for (var i = 0; i < keys.length; i++) {
|
||||||
scoregraph();
|
var count = 0;
|
||||||
|
for (var x = 0; x < categories.length; x++) {
|
||||||
|
if (categories[x] == keys[i]) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counts.push(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = [
|
||||||
|
{
|
||||||
|
values: counts,
|
||||||
|
labels: keys,
|
||||||
|
hole: 0.4,
|
||||||
|
type: "pie"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
window.onresize = function () {
|
var layout = {
|
||||||
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
|
title: "Category Breakdown",
|
||||||
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
Plotly.Plots.resize(document.getElementById('score-graph'));
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
};
|
legend: {
|
||||||
});
|
orientation: "v"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#categories-pie-graph").empty();
|
||||||
|
document.getElementById("categories-pie-graph").fn =
|
||||||
|
"CTFd_categories_team_" +
|
||||||
|
TEAM_ID +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("categories-pie-graph", data, layout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
category_breakdown_graph();
|
||||||
|
keys_percentage_graph();
|
||||||
|
scoregraph();
|
||||||
|
|
||||||
|
window.onresize = function() {
|
||||||
|
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
|
||||||
|
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
|
||||||
|
Plotly.Plots.resize(document.getElementById("score-graph"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
|
@ -1,72 +1,77 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#team-info-form').submit(function (e) {
|
$("#team-info-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var params = $('#team-info-form').serializeJSON(true);
|
var params = $("#team-info-form").serializeJSON(true);
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/teams/' + TEAM_ID, {
|
CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
window.location.reload();
|
.then(function(response) {
|
||||||
} else {
|
if (response.success) {
|
||||||
$('#team-info-form > #results').empty();
|
window.location.reload();
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
} else {
|
||||||
$('#team-info-form > #results').append(
|
$("#team-info-form > #results").empty();
|
||||||
ezbadge({
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
type: 'error',
|
$("#team-info-form > #results").append(
|
||||||
body: response.errors[key]
|
ezbadge({
|
||||||
})
|
type: "error",
|
||||||
);
|
body: response.errors[key]
|
||||||
var i = $('#team-info-form').find('input[name={0}]'.format(key));
|
})
|
||||||
var input = $(i);
|
);
|
||||||
input.addClass('input-filled-invalid');
|
var i = $("#team-info-form").find("input[name={0}]".format(key));
|
||||||
input.removeClass('input-filled-valid');
|
var input = $(i);
|
||||||
});
|
input.addClass("input-filled-invalid");
|
||||||
}
|
input.removeClass("input-filled-valid");
|
||||||
})
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#team-captain-form").submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var params = $("#team-captain-form").serializeJSON(true);
|
||||||
|
|
||||||
$('#team-captain-form').submit(function (e) {
|
CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
|
||||||
e.preventDefault();
|
method: "PATCH",
|
||||||
var params = $('#team-captain-form').serializeJSON(true);
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
CTFd.fetch('/api/v1/teams/' + TEAM_ID, {
|
Accept: "application/json",
|
||||||
method: 'PATCH',
|
"Content-Type": "application/json"
|
||||||
credentials: 'same-origin',
|
},
|
||||||
headers: {
|
body: JSON.stringify(params)
|
||||||
'Accept': 'application/json',
|
})
|
||||||
'Content-Type': 'application/json'
|
.then(function(response) {
|
||||||
},
|
return response.json();
|
||||||
body: JSON.stringify(params)
|
})
|
||||||
}).then(function (response) {
|
.then(function(response) {
|
||||||
return response.json();
|
if (response.success) {
|
||||||
}).then(function (response) {
|
window.location.reload();
|
||||||
if (response.success) {
|
} else {
|
||||||
window.location.reload();
|
$("#team-captain-form > #results").empty();
|
||||||
} else {
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
$('#team-captain-form > #results').empty();
|
$("#team-captain-form > #results").append(
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
ezbadge({
|
||||||
$('#team-captain-form > #results').append(
|
type: "error",
|
||||||
ezbadge({
|
body: response.errors[key]
|
||||||
type: 'error',
|
})
|
||||||
body: response.errors[key]
|
);
|
||||||
})
|
var i = $("#team-captain-form").find(
|
||||||
);
|
"select[name={0}]".format(key)
|
||||||
var i = $('#team-captain-form').find('select[name={0}]'.format(key));
|
);
|
||||||
var input = $(i);
|
var input = $(i);
|
||||||
input.addClass('input-filled-invalid');
|
input.addClass("input-filled-invalid");
|
||||||
input.removeClass('input-filled-valid');
|
input.removeClass("input-filled-valid");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,37 +1,39 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#team-info-form').submit(function (e) {
|
$("#team-info-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var params = $('#team-info-form').serializeJSON(true);
|
var params = $("#team-info-form").serializeJSON(true);
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/teams', {
|
CTFd.fetch("/api/v1/teams", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
var team_id = response.data.id;
|
.then(function(response) {
|
||||||
window.location = script_root + '/admin/teams/' + team_id;
|
if (response.success) {
|
||||||
} else {
|
var team_id = response.data.id;
|
||||||
$('#team-info-form > #results').empty();
|
window.location = script_root + "/admin/teams/" + team_id;
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
} else {
|
||||||
$('#team-info-form > #results').append(
|
$("#team-info-form > #results").empty();
|
||||||
ezbadge({
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
type: 'error',
|
$("#team-info-form > #results").append(
|
||||||
body: response.errors[key]
|
ezbadge({
|
||||||
})
|
type: "error",
|
||||||
);
|
body: response.errors[key]
|
||||||
var i = $('#team-info-form').find('input[name={0}]'.format(key));
|
})
|
||||||
var input = $(i);
|
);
|
||||||
input.addClass('input-filled-invalid');
|
var i = $("#team-info-form").find("input[name={0}]".format(key));
|
||||||
input.removeClass('input-filled-valid');
|
var input = $(i);
|
||||||
});
|
input.addClass("input-filled-invalid");
|
||||||
}
|
input.removeClass("input-filled-valid");
|
||||||
})
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,219 +1,243 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('.delete-user').click(function(e){
|
$(".delete-user").click(function(e) {
|
||||||
ezq({
|
ezq({
|
||||||
title: "Delete User",
|
title: "Delete User",
|
||||||
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(USER_NAME) + "</strong>"),
|
body: "Are you sure you want to delete {0}".format(
|
||||||
success: function () {
|
"<strong>" + htmlentities(USER_NAME) + "</strong>"
|
||||||
CTFd.fetch('/api/v1/users/' + USER_ID, {
|
),
|
||||||
method: 'DELETE',
|
success: function() {
|
||||||
}).then(function (response) {
|
CTFd.fetch("/api/v1/users/" + USER_ID, {
|
||||||
return response.json();
|
method: "DELETE"
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
window.location = script_root + '/admin/users';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.edit-user').click(function (e) {
|
|
||||||
$('#user-info-modal').modal('toggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.award-user').click(function (e) {
|
|
||||||
$('#user-award-modal').modal('toggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.email-user').click(function (e) {
|
|
||||||
$('#user-email-modal').modal('toggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#user-award-form').submit(function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var params = $('#user-award-form').serializeJSON(true);
|
|
||||||
params['user_id'] = USER_ID;
|
|
||||||
CTFd.fetch('/api/v1/awards', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
window.location.reload()
|
|
||||||
} else {
|
|
||||||
$('#user-award-form > #results').empty();
|
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
|
||||||
$('#user-award-form > #results').append(
|
|
||||||
ezbadge({
|
|
||||||
type: 'error',
|
|
||||||
body: response.errors[key]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
var i = $('#user-award-form').find('input[name={0}]'.format(key));
|
|
||||||
var input = $(i);
|
|
||||||
input.addClass('input-filled-invalid');
|
|
||||||
input.removeClass('input-filled-valid');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#user-mail-form').submit(function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var params = $('#user-mail-form').serializeJSON(true);
|
|
||||||
CTFd.fetch('/api/v1/users/'+USER_ID+'/email', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
$('#user-mail-form > #results').append(
|
|
||||||
ezbadge({
|
|
||||||
type: 'success',
|
|
||||||
body: 'E-Mail sent successfully!'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
$('#user-mail-form').find("input[type=text], textarea").val("")
|
|
||||||
} else {
|
|
||||||
$('#user-mail-form > #results').empty();
|
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
|
||||||
$('#user-mail-form > #results').append(
|
|
||||||
ezbadge({
|
|
||||||
type: 'error',
|
|
||||||
body: response.errors[key]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
var i = $('#user-mail-form').find('input[name={0}], textarea[name={0}]'.format(key));
|
|
||||||
var input = $(i);
|
|
||||||
input.addClass('input-filled-invalid');
|
|
||||||
input.removeClass('input-filled-valid');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.delete-submission').click(function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var submission_id = $(this).attr('submission-id');
|
|
||||||
var submission_type = $(this).attr('submission-type');
|
|
||||||
var submission_challenge = $(this).attr('submission-challenge');
|
|
||||||
|
|
||||||
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
|
|
||||||
htmlentities(submission_type),
|
|
||||||
htmlentities(USER_NAME),
|
|
||||||
htmlentities(submission_challenge)
|
|
||||||
);
|
|
||||||
|
|
||||||
var row = $(this).parent().parent();
|
|
||||||
|
|
||||||
ezq({
|
|
||||||
title: "Delete Submission",
|
|
||||||
body: body,
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/submissions/' + submission_id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
row.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.delete-award').click(function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
var award_id = $(this).attr('award-id');
|
|
||||||
var award_name = $(this).attr('award-name');
|
|
||||||
|
|
||||||
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
|
|
||||||
htmlentities(award_name),
|
|
||||||
htmlentities(USER_NAME)
|
|
||||||
);
|
|
||||||
|
|
||||||
var row = $(this).parent().parent();
|
|
||||||
|
|
||||||
ezq({
|
|
||||||
title: "Delete Award",
|
|
||||||
body: body,
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/awards/' + award_id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
row.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.correct-submission').click(function(e) {
|
|
||||||
var challenge_id = $(this).attr('challenge-id');
|
|
||||||
var challenge_name = $(this).attr('challenge-name');
|
|
||||||
var row = $(this).parent().parent();
|
|
||||||
|
|
||||||
var body = "<span>Are you sure you want to mark <strong>{0}</strong> solved for from <strong>{1}</strong>?".format(
|
|
||||||
htmlentities(challenge_name),
|
|
||||||
htmlentities(USER_NAME)
|
|
||||||
);
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
provided: "MARKED AS SOLVED BY ADMIN",
|
|
||||||
user_id: USER_ID,
|
|
||||||
team_id: TEAM_ID,
|
|
||||||
challenge_id: challenge_id,
|
|
||||||
type: "correct"
|
|
||||||
};
|
|
||||||
|
|
||||||
ezq({
|
|
||||||
title: "Mark Correct",
|
|
||||||
body: body,
|
|
||||||
success: function () {
|
|
||||||
CTFd.fetch('/api/v1/submissions', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
// TODO: Refresh missing and solves instead of reloading
|
|
||||||
row.remove();
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location = script_root + "/admin/users";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".edit-user").click(function(e) {
|
||||||
|
$("#user-info-modal").modal("toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".award-user").click(function(e) {
|
||||||
|
$("#user-award-modal").modal("toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".email-user").click(function(e) {
|
||||||
|
$("#user-email-modal").modal("toggle");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#user-award-form").submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var params = $("#user-award-form").serializeJSON(true);
|
||||||
|
params["user_id"] = USER_ID;
|
||||||
|
CTFd.fetch("/api/v1/awards", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
$("#user-award-form > #results").empty();
|
||||||
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
|
$("#user-award-form > #results").append(
|
||||||
|
ezbadge({
|
||||||
|
type: "error",
|
||||||
|
body: response.errors[key]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
var i = $("#user-award-form").find("input[name={0}]".format(key));
|
||||||
|
var input = $(i);
|
||||||
|
input.addClass("input-filled-invalid");
|
||||||
|
input.removeClass("input-filled-valid");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#user-mail-form").submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var params = $("#user-mail-form").serializeJSON(true);
|
||||||
|
CTFd.fetch("/api/v1/users/" + USER_ID + "/email", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$("#user-mail-form > #results").append(
|
||||||
|
ezbadge({
|
||||||
|
type: "success",
|
||||||
|
body: "E-Mail sent successfully!"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
$("#user-mail-form")
|
||||||
|
.find("input[type=text], textarea")
|
||||||
|
.val("");
|
||||||
|
} else {
|
||||||
|
$("#user-mail-form > #results").empty();
|
||||||
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
|
$("#user-mail-form > #results").append(
|
||||||
|
ezbadge({
|
||||||
|
type: "error",
|
||||||
|
body: response.errors[key]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
var i = $("#user-mail-form").find(
|
||||||
|
"input[name={0}], textarea[name={0}]".format(key)
|
||||||
|
);
|
||||||
|
var input = $(i);
|
||||||
|
input.addClass("input-filled-invalid");
|
||||||
|
input.removeClass("input-filled-valid");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".delete-submission").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var submission_id = $(this).attr("submission-id");
|
||||||
|
var submission_type = $(this).attr("submission-type");
|
||||||
|
var submission_challenge = $(this).attr("submission-challenge");
|
||||||
|
|
||||||
|
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
|
||||||
|
htmlentities(submission_type),
|
||||||
|
htmlentities(USER_NAME),
|
||||||
|
htmlentities(submission_challenge)
|
||||||
|
);
|
||||||
|
|
||||||
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
|
ezq({
|
||||||
|
title: "Delete Submission",
|
||||||
|
body: body,
|
||||||
|
success: function() {
|
||||||
|
CTFd.fetch("/api/v1/submissions/" + submission_id, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".delete-award").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var award_id = $(this).attr("award-id");
|
||||||
|
var award_name = $(this).attr("award-name");
|
||||||
|
|
||||||
|
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
|
||||||
|
htmlentities(award_name),
|
||||||
|
htmlentities(USER_NAME)
|
||||||
|
);
|
||||||
|
|
||||||
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
|
ezq({
|
||||||
|
title: "Delete Award",
|
||||||
|
body: body,
|
||||||
|
success: function() {
|
||||||
|
CTFd.fetch("/api/v1/awards/" + award_id, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".correct-submission").click(function(e) {
|
||||||
|
var challenge_id = $(this).attr("challenge-id");
|
||||||
|
var challenge_name = $(this).attr("challenge-name");
|
||||||
|
var row = $(this)
|
||||||
|
.parent()
|
||||||
|
.parent();
|
||||||
|
|
||||||
|
var body = "<span>Are you sure you want to mark <strong>{0}</strong> solved for from <strong>{1}</strong>?".format(
|
||||||
|
htmlentities(challenge_name),
|
||||||
|
htmlentities(USER_NAME)
|
||||||
|
);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
provided: "MARKED AS SOLVED BY ADMIN",
|
||||||
|
user_id: USER_ID,
|
||||||
|
team_id: TEAM_ID,
|
||||||
|
challenge_id: challenge_id,
|
||||||
|
type: "correct"
|
||||||
|
};
|
||||||
|
|
||||||
|
ezq({
|
||||||
|
title: "Mark Correct",
|
||||||
|
body: body,
|
||||||
|
success: function() {
|
||||||
|
CTFd.fetch("/api/v1/submissions", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
// TODO: Refresh missing and solves instead of reloading
|
||||||
|
row.remove();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,160 +1,178 @@
|
||||||
function scoregraph() {
|
function scoregraph() {
|
||||||
var times = [];
|
var times = [];
|
||||||
var scores = [];
|
var scores = [];
|
||||||
$.get(script_root + '/api/v1/users/' + USER_ID + '/solves', function (solve_data) {
|
$.get(script_root + "/api/v1/users/" + USER_ID + "/solves", function(
|
||||||
$.get(script_root + '/api/v1/users/' + USER_ID + '/awards', function (award_data) {
|
solve_data
|
||||||
var solves = solve_data.data;
|
) {
|
||||||
var awards = award_data.data;
|
$.get(script_root + "/api/v1/users/" + USER_ID + "/awards", function(
|
||||||
|
award_data
|
||||||
|
) {
|
||||||
|
var solves = solve_data.data;
|
||||||
|
var awards = award_data.data;
|
||||||
|
|
||||||
var total = solves.concat(awards);
|
var total = solves.concat(awards);
|
||||||
|
|
||||||
total.sort(function (a, b) {
|
total.sort(function(a, b) {
|
||||||
return new Date(a.date) - new Date(b.date);
|
return new Date(a.date) - new Date(b.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (var i = 0; i < total.length; i++) {
|
for (var i = 0; i < total.length; i++) {
|
||||||
var date = moment(total[i].date);
|
var date = moment(total[i].date);
|
||||||
times.push(date.toDate());
|
times.push(date.toDate());
|
||||||
try {
|
try {
|
||||||
scores.push(total[i].challenge.value);
|
scores.push(total[i].challenge.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
scores.push(total[i].value);
|
scores.push(total[i].value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scores = cumulativesum(scores);
|
scores = cumulativesum(scores);
|
||||||
|
|
||||||
var data = [
|
var data = [
|
||||||
{
|
{
|
||||||
x: times,
|
x: times,
|
||||||
y: scores,
|
y: scores,
|
||||||
type: 'scatter',
|
type: "scatter",
|
||||||
marker: {
|
marker: {
|
||||||
color: colorhash(USER_NAME + USER_ID),
|
color: colorhash(USER_NAME + USER_ID)
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
color: colorhash(USER_NAME + USER_ID),
|
color: colorhash(USER_NAME + USER_ID)
|
||||||
},
|
},
|
||||||
fill: 'tozeroy'
|
fill: "tozeroy"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Score over Time',
|
title: "Score over Time",
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
hovermode: 'closest',
|
hovermode: "closest",
|
||||||
xaxis: {
|
xaxis: {
|
||||||
showgrid: false,
|
showgrid: false,
|
||||||
showspikes: true,
|
showspikes: true
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
showgrid: false,
|
showgrid: false,
|
||||||
showspikes: true,
|
showspikes: true
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
"orientation": "h"
|
orientation: "h"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#score-graph').empty();
|
$("#score-graph").empty();
|
||||||
document.getElementById('score-graph').fn = 'CTFd_score_user_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("score-graph").fn =
|
||||||
Plotly.newPlot('score-graph', data, layout);
|
"CTFd_score_user_" +
|
||||||
});
|
USER_ID +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("score-graph", data, layout);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function keys_percentage_graph() {
|
function keys_percentage_graph() {
|
||||||
// Solves and Fails pie chart
|
// Solves and Fails pie chart
|
||||||
var base_url = script_root + '/api/v1/users/' + USER_ID;
|
var base_url = script_root + "/api/v1/users/" + USER_ID;
|
||||||
$.get(base_url + '/fails', function (fails) {
|
$.get(base_url + "/fails", function(fails) {
|
||||||
$.get(base_url + '/solves', function (solves) {
|
$.get(base_url + "/solves", function(solves) {
|
||||||
var solves_count = solves.data.length;
|
var solves_count = solves.data.length;
|
||||||
var fails_count = fails.data.length;
|
var fails_count = fails.data.length;
|
||||||
|
|
||||||
var graph_data = [{
|
var graph_data = [
|
||||||
values: [solves_count, fails_count],
|
{
|
||||||
labels: ['Solves', 'Fails'],
|
values: [solves_count, fails_count],
|
||||||
marker: {
|
labels: ["Solves", "Fails"],
|
||||||
colors: [
|
marker: {
|
||||||
"rgb(0, 209, 64)",
|
colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
|
||||||
"rgb(207, 38, 0)"
|
},
|
||||||
]
|
hole: 0.4,
|
||||||
},
|
type: "pie"
|
||||||
hole: .4,
|
}
|
||||||
type: 'pie'
|
];
|
||||||
}];
|
|
||||||
|
|
||||||
var layout = {
|
var layout = {
|
||||||
title: 'Solve Percentages',
|
title: "Solve Percentages",
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
legend: {
|
legend: {
|
||||||
"orientation": "h"
|
orientation: "h"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#keys-pie-graph').empty();
|
$("#keys-pie-graph").empty();
|
||||||
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_user_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19);
|
document.getElementById("keys-pie-graph").fn =
|
||||||
Plotly.newPlot('keys-pie-graph', graph_data, layout);
|
"CTFd_submissions_user_" +
|
||||||
});
|
USER_ID +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("keys-pie-graph", graph_data, layout);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function category_breakdown_graph() {
|
function category_breakdown_graph() {
|
||||||
$.get(script_root + '/api/v1/users/' + USER_ID + '/solves', function (response) {
|
$.get(script_root + "/api/v1/users/" + USER_ID + "/solves", function(
|
||||||
var solves = response.data;
|
response
|
||||||
|
) {
|
||||||
|
var solves = response.data;
|
||||||
|
|
||||||
var categories = [];
|
var categories = [];
|
||||||
for (var i = 0; i < solves.length; i++) {
|
for (var i = 0; i < solves.length; i++) {
|
||||||
categories.push(solves[i].challenge.category)
|
categories.push(solves[i].challenge.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = categories.filter(function (elem, pos) {
|
var keys = categories.filter(function(elem, pos) {
|
||||||
return categories.indexOf(elem) == pos;
|
return categories.indexOf(elem) == pos;
|
||||||
});
|
|
||||||
|
|
||||||
var counts = [];
|
|
||||||
for (var i = 0; i < keys.length; i++) {
|
|
||||||
var count = 0;
|
|
||||||
for (var x = 0; x < categories.length; x++) {
|
|
||||||
if (categories[x] == keys[i]) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
counts.push(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = [{
|
|
||||||
values: counts,
|
|
||||||
labels: keys,
|
|
||||||
hole: .4,
|
|
||||||
type: 'pie'
|
|
||||||
}];
|
|
||||||
|
|
||||||
var layout = {
|
|
||||||
title: 'Category Breakdown',
|
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
|
||||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
|
||||||
legend: {
|
|
||||||
"orientation": "v"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#categories-pie-graph').empty();
|
|
||||||
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_team_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19);
|
|
||||||
Plotly.newPlot('categories-pie-graph', data, layout);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var counts = [];
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var count = 0;
|
||||||
|
for (var x = 0; x < categories.length; x++) {
|
||||||
|
if (categories[x] == keys[i]) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counts.push(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = [
|
||||||
|
{
|
||||||
|
values: counts,
|
||||||
|
labels: keys,
|
||||||
|
hole: 0.4,
|
||||||
|
type: "pie"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var layout = {
|
||||||
|
title: "Category Breakdown",
|
||||||
|
paper_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor: "rgba(0,0,0,0)",
|
||||||
|
legend: {
|
||||||
|
orientation: "v"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#categories-pie-graph").empty();
|
||||||
|
document.getElementById("categories-pie-graph").fn =
|
||||||
|
"CTFd_categories_team_" +
|
||||||
|
USER_ID +
|
||||||
|
"_" +
|
||||||
|
new Date().toISOString().slice(0, 19);
|
||||||
|
Plotly.newPlot("categories-pie-graph", data, layout);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
$(document).ready(function () {
|
category_breakdown_graph();
|
||||||
category_breakdown_graph();
|
keys_percentage_graph();
|
||||||
keys_percentage_graph();
|
scoregraph();
|
||||||
scoregraph();
|
window.onresize = function() {
|
||||||
window.onresize = function () {
|
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
|
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
|
Plotly.Plots.resize(document.getElementById("score-graph"));
|
||||||
Plotly.Plots.resize(document.getElementById('score-graph'));
|
};
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#user-info-form').submit(function(e){
|
$("#user-info-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var params = $('#user-info-form').serializeJSON(true);
|
var params = $("#user-info-form").serializeJSON(true);
|
||||||
|
|
||||||
CTFd.fetch('/api/v1/users/' + USER_ID, {
|
CTFd.fetch("/api/v1/users/" + USER_ID, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
|
||||||
return response.json();
|
|
||||||
}).then(function (response) {
|
|
||||||
if (response.success) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
$('#user-info-form > #results').empty();
|
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
|
||||||
$('#user-info-form > #results').append(
|
|
||||||
ezbadge({
|
|
||||||
type: 'error',
|
|
||||||
body: response.errors[key]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
var i = $('#user-info-form').find('input[name={0}]'.format(key));
|
|
||||||
var input = $(i);
|
|
||||||
input.addClass('input-filled-invalid');
|
|
||||||
input.removeClass('input-filled-valid');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
});
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
$("#user-info-form > #results").empty();
|
||||||
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
|
$("#user-info-form > #results").append(
|
||||||
|
ezbadge({
|
||||||
|
type: "error",
|
||||||
|
body: response.errors[key]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
var i = $("#user-info-form").find("input[name={0}]".format(key));
|
||||||
|
var input = $(i);
|
||||||
|
input.addClass("input-filled-invalid");
|
||||||
|
input.removeClass("input-filled-valid");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,42 +1,44 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {
|
||||||
$('#user-info-form').submit(function (e) {
|
$("#user-info-form").submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var params = $('#user-info-form').serializeJSON(true);
|
var params = $("#user-info-form").serializeJSON(true);
|
||||||
var url = '/api/v1/users';
|
var url = "/api/v1/users";
|
||||||
if (params.notify) {
|
if (params.notify) {
|
||||||
url += '?notify=true'
|
url += "?notify=true";
|
||||||
}
|
}
|
||||||
delete params.notify;
|
delete params.notify;
|
||||||
|
|
||||||
CTFd.fetch(url, {
|
CTFd.fetch(url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
credentials: 'same-origin',
|
credentials: "same-origin",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(params)
|
body: JSON.stringify(params)
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.json();
|
.then(function(response) {
|
||||||
}).then(function (response) {
|
return response.json();
|
||||||
if (response.success) {
|
})
|
||||||
var user_id = response.data.id;
|
.then(function(response) {
|
||||||
window.location = script_root + '/admin/users/' + user_id;
|
if (response.success) {
|
||||||
} else {
|
var user_id = response.data.id;
|
||||||
$('#user-info-form > #results').empty();
|
window.location = script_root + "/admin/users/" + user_id;
|
||||||
Object.keys(response.errors).forEach(function (key, index) {
|
} else {
|
||||||
$('#user-info-form > #results').append(
|
$("#user-info-form > #results").empty();
|
||||||
ezbadge({
|
Object.keys(response.errors).forEach(function(key, index) {
|
||||||
type: 'error',
|
$("#user-info-form > #results").append(
|
||||||
body: response.errors[key]
|
ezbadge({
|
||||||
})
|
type: "error",
|
||||||
);
|
body: response.errors[key]
|
||||||
var i = $('#user-info-form').find('input[name={0}]'.format(key));
|
})
|
||||||
var input = $(i);
|
);
|
||||||
input.addClass('input-filled-invalid');
|
var i = $("#user-info-form").find("input[name={0}]".format(key));
|
||||||
input.removeClass('input-filled-valid');
|
var input = $(i);
|
||||||
});
|
input.addClass("input-filled-invalid");
|
||||||
}
|
input.removeClass("input-filled-valid");
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
$(document).ready(function () {
|
$(document).ready(function() {});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,157 +1,160 @@
|
||||||
html, body, .container {
|
html,
|
||||||
font-family: 'Lato', 'LatoOffline', sans-serif;
|
body,
|
||||||
|
.container {
|
||||||
|
font-family: "Lato", "LatoOffline", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1,
|
||||||
font-family: 'Raleway', 'RalewayOffline', sans-serif;
|
h2 {
|
||||||
font-weight: 500;
|
font-family: "Raleway", "RalewayOffline", sans-serif;
|
||||||
letter-spacing: 2px;
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table > thead > tr > td {
|
table > thead > tr > td {
|
||||||
/* Remove border line from thead of all tables */
|
/* Remove border line from thead of all tables */
|
||||||
/* It can overlap with other element styles */
|
/* It can overlap with other element styles */
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#score-graph {
|
#score-graph {
|
||||||
height: 450px;
|
height: 450px;
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-spin.spinner {
|
.fa-spin.spinner {
|
||||||
margin-top: 225px;
|
margin-top: 225px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-error {
|
.spinner-error {
|
||||||
padding-top: 20vh;
|
padding-top: 20vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron {
|
.jumbotron {
|
||||||
background-color: #343a40;
|
background-color: #343a40;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #a3d39c;
|
border-color: #a3d39c;
|
||||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-filled-valid {
|
.input-filled-valid {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #a3d39c;
|
border-color: #a3d39c;
|
||||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-filled-invalid {
|
.input-filled-invalid {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #d46767;
|
border-color: #d46767;
|
||||||
box-shadow: 0 0 0 0.2rem #d46767;
|
box-shadow: 0 0 0 0.2rem #d46767;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined.btn-theme {
|
.btn-outlined.btn-theme {
|
||||||
background: none;
|
background: none;
|
||||||
color: #545454;
|
color: #545454;
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined {
|
.btn-outlined {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
border-color: #5B7290 !important;
|
border-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-fa {
|
.btn-fa {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-help {
|
.cursor-help {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.form-control {
|
select.form-control {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
-webkit-border-radius: 0 !important;
|
-webkit-border-radius: 0 !important;
|
||||||
-moz-border-radius: 0 !important;
|
-moz-border-radius: 0 !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-disabled {
|
.fa-disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,46 @@
|
||||||
.chal-desc {
|
.chal-desc {
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chal-desc img {
|
.chal-desc img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
background-color: #5B7290 !important;
|
background-color: #5b7290 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.challenge-button {
|
.challenge-button {
|
||||||
box-shadow: 3px 3px 3px grey;
|
box-shadow: 3px 3px 3px grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solved-challenge {
|
.solved-challenge {
|
||||||
background-color: #37d63e !important;
|
background-color: #37d63e !important;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner-button-check {
|
.corner-button-check {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-submit .btn {
|
.key-submit .btn {
|
||||||
height: 51px;
|
height: 51px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
||||||
body {
|
body {
|
||||||
padding-top: 3.5rem;
|
padding-top: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
/* Sticky footer styles
|
/* Sticky footer styles
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
html {
|
html {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
|
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px; /* Set the fixed height of the footer here */
|
height: 60px; /* Set the fixed height of the footer here */
|
||||||
line-height: 60px; /* Vertically center the text there */
|
line-height: 60px; /* Vertically center the text there */
|
||||||
/*background-color: #f5f5f5;*/
|
/*background-color: #f5f5f5;*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,402 +1,403 @@
|
||||||
|
html,
|
||||||
html, body, .container {
|
body,
|
||||||
height: 100% !important;
|
.container {
|
||||||
font-family: 'Lato', 'LatoOffline', sans-serif;
|
height: 100% !important;
|
||||||
|
font-family: "Lato", "LatoOffline", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1,
|
||||||
font-family: 'Raleway', 'RalewayOffline', sans-serif;
|
h2 {
|
||||||
font-weight: 500;
|
font-family: "Raleway", "RalewayOffline", sans-serif;
|
||||||
letter-spacing: 2px;
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 15px !important;
|
padding: 15px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-container {
|
.body-container {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-margin-right {
|
.no-margin-right {
|
||||||
margin-right: 0px !important;
|
margin-right: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-margin-left {
|
.no-margin-left {
|
||||||
margin-left: -3px !important;
|
margin-left: -3px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar > .container {
|
.navbar > .container {
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-brand {
|
.navbar .navbar-brand {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
letter-spacing: -0.04rem;
|
letter-spacing: -0.04rem;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse .navbar-nav > li > a {
|
.navbar-inverse .navbar-nav > li > a {
|
||||||
color: #FFF !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar li > a {
|
.navbar li > a {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.08s ease-in 0s;
|
transition: opacity 0.08s ease-in 0s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar li > a:hover {
|
.navbar li > a:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.align-text-to-button {
|
.align-text-to-button {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.padded-container {
|
.padded-container {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
padding-left: 50px;
|
padding-left: 50px;
|
||||||
padding-right: 50px;
|
padding-right: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
margin-top: 35px;
|
margin-top: 35px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-container {
|
#login-container {
|
||||||
padding-left: 70px;
|
padding-left: 70px;
|
||||||
padding-right: 70px;
|
padding-right: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chal > form{
|
#chal > form {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reveal-modal{
|
.reveal-modal {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chal-desc{
|
.chal-desc {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
table{
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#challenges button{
|
#challenges button {
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row > h1{
|
.row > h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#challenges{
|
#challenges {
|
||||||
line-height: 66px;
|
line-height: 66px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#score-graph{
|
#score-graph {
|
||||||
height: 450px;
|
height: 450px;
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa.spinner {
|
.fa.spinner {
|
||||||
margin-top: 225px;
|
margin-top: 225px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-error {
|
.spinner-error {
|
||||||
padding-top: 20vh;
|
padding-top: 20vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#keys-pie-graph{
|
#keys-pie-graph {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#categories-pie-graph{
|
#categories-pie-graph {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo{
|
.logo {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 40.063em) {
|
||||||
|
.top-bar .dropdown {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
padding: 0 15px 5px;
|
||||||
|
width: 200% !important;
|
||||||
@media only screen and (min-width: 40.063em){
|
}
|
||||||
.top-bar .dropdown{
|
|
||||||
display: block;
|
|
||||||
padding: 0 15px 5px;
|
|
||||||
width: 200% !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size:14px;
|
font-size: 14px;
|
||||||
line-height:20px;
|
line-height: 20px;
|
||||||
font-weight:700;
|
font-weight: 700;
|
||||||
text-transform:uppercase;
|
text-transform: uppercase;
|
||||||
padding:8px 20px;
|
padding: 8px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined {
|
.btn-outlined {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
-webkit-transition: all 0.3s;
|
-webkit-transition: all 0.3s;
|
||||||
-moz-transition: all 0.3s;
|
-moz-transition: all 0.3s;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-wrapper {
|
.file-wrapper {
|
||||||
background-color: #5B7290;
|
background-color: #5b7290;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-wrapper:hover {
|
.file-wrapper:hover {
|
||||||
background-color: #747474;
|
background-color: #747474;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-background {
|
.theme-background {
|
||||||
background-color: #545454 !important;
|
background-color: #545454 !important;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.solved-challenge {
|
.solved-challenge {
|
||||||
background-color: #8EDC9D !important;
|
background-color: #8edc9d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-theme {
|
.panel-theme {
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-theme > .panel-heading {
|
.panel-theme > .panel-heading {
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
background-color: #545454;
|
background-color: #545454;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined.btn-theme {
|
.btn-outlined.btn-theme {
|
||||||
background: none;
|
background: none;
|
||||||
color: #545454;
|
color: #545454;
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outlined.btn-theme:hover,
|
.btn-outlined.btn-theme:hover,
|
||||||
.btn-outlined.btn-theme:active {
|
.btn-outlined.btn-theme:active {
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
background: #545454;
|
background: #545454;
|
||||||
border-color: #545454;
|
border-color: #545454;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-inverse {
|
.navbar-inverse {
|
||||||
background: none repeat scroll 0% 0% #545454;
|
background: none repeat scroll 0% 0% #545454;
|
||||||
border: medium none;
|
border: medium none;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron {
|
.jumbotron {
|
||||||
background-color: #545454;
|
background-color: #545454;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
padding: 0px 0px 25px;
|
padding: 0px 0px 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron.home {
|
.jumbotron.home {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home h1 {
|
.home h1 {
|
||||||
font-size: 48px !important;
|
font-size: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chal-tag {
|
.chal-tag {
|
||||||
margin: 0 5px 0 5px;
|
margin: 0 5px 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* INPUT CSS */
|
/* INPUT CSS */
|
||||||
|
|
||||||
.submit-row {
|
.submit-row {
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
float: right;
|
float: right;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
-webkit-appearance: none; /* for box shadows to show on iOS */
|
-webkit-appearance: none; /* for box shadows to show on iOS */
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field:focus {
|
.input-field:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-label {
|
.input-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: right;
|
float: right;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
color: #6a7989;
|
color: #6a7989;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-khtml-user-select: none;
|
-khtml-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: 0 0 0 0 !important;
|
margin: 0 0 0 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-content {
|
.label-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1.6em 0;
|
padding: 1.6em 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graphic {
|
.graphic {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
-webkit-transition: background-color 0.3s, border-color 0.3s;
|
-webkit-transition: background-color 0.3s, border-color 0.3s;
|
||||||
transition: background-color 0.3s, border-color 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-label {
|
.input-label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 1.25em;
|
padding: 0 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-content {
|
.label-content {
|
||||||
color: #8B8C8B;
|
color: #8b8c8b;
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
-webkit-transition: -webkit-transform 0.3s;
|
-webkit-transition: -webkit-transform 0.3s;
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-content::after {
|
.label-content::after {
|
||||||
content: attr(data-content);
|
content: attr(data-content);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: #a3d39c;
|
color: #a3d39c;
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field:focus + .input-field,
|
.input-field:focus + .input-field,
|
||||||
.input--filled .input-field {
|
.input--filled .input-field {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: #a3d39c;
|
border-color: #a3d39c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input--hide {
|
.input--hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#submit {
|
#submit {
|
||||||
position: relative;
|
position: relative;
|
||||||
right: -15px;
|
right: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.done-row {
|
.done-row {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* END OF INPUT CSS */
|
/* END OF INPUT CSS */
|
||||||
|
|
|
@ -1,48 +1,46 @@
|
||||||
var CTFd = (function () {
|
var CTFd = (function() {
|
||||||
|
var options = {
|
||||||
|
urlRoot: "",
|
||||||
|
csrfNonce: "",
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
};
|
||||||
|
|
||||||
var options = {
|
var challenges = {};
|
||||||
urlRoot: '',
|
|
||||||
csrfNonce: '',
|
|
||||||
start: null,
|
|
||||||
end: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
var challenges = {};
|
var scoreboard = function() {};
|
||||||
|
|
||||||
var scoreboard = function() {};
|
var teams = {};
|
||||||
|
|
||||||
var teams = {};
|
var users = {};
|
||||||
|
|
||||||
var users = {};
|
var fetch = function(url, options) {
|
||||||
|
if (options === undefined) {
|
||||||
|
options = {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
url = this.options.urlRoot + url;
|
||||||
|
|
||||||
var fetch = function(url, options) {
|
if (options.headers === undefined) {
|
||||||
if (options === undefined) {
|
options.headers = {};
|
||||||
options = {
|
}
|
||||||
method: "GET",
|
options.credentials = "same-origin";
|
||||||
credentials: "same-origin",
|
options.headers["Accept"] = "application/json";
|
||||||
headers: {},
|
options.headers["Content-Type"] = "application/json";
|
||||||
};
|
options.headers["CSRF-Token"] = this.options.csrfNonce;
|
||||||
}
|
|
||||||
url = this.options.urlRoot + url;
|
|
||||||
|
|
||||||
|
return window.fetch(url, options);
|
||||||
|
};
|
||||||
|
|
||||||
if (options.headers === undefined) {
|
return {
|
||||||
options.headers = {};
|
challenges: challenges,
|
||||||
}
|
scoreboard: scoreboard,
|
||||||
options.credentials = 'same-origin';
|
teams: teams,
|
||||||
options.headers['Accept'] = 'application/json';
|
users: users,
|
||||||
options.headers['Content-Type'] = 'application/json';
|
fetch: fetch,
|
||||||
options.headers['CSRF-Token'] = this.options.csrfNonce;
|
options: options
|
||||||
|
};
|
||||||
return window.fetch(url, options);
|
})();
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
challenges: challenges,
|
|
||||||
scoreboard: scoreboard,
|
|
||||||
teams: teams,
|
|
||||||
users: users,
|
|
||||||
fetch: fetch,
|
|
||||||
options: options
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
|
@ -5,342 +5,404 @@ var templates = {};
|
||||||
window.challenge = new Object();
|
window.challenge = new Object();
|
||||||
|
|
||||||
function loadchal(id) {
|
function loadchal(id) {
|
||||||
var obj = $.grep(challenges, function (e) {
|
var obj = $.grep(challenges, function(e) {
|
||||||
return e.id == id;
|
return e.id == id;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
if (obj.type === 'hidden') {
|
if (obj.type === "hidden") {
|
||||||
ezal({
|
ezal({
|
||||||
title: "Challenge Hidden!",
|
title: "Challenge Hidden!",
|
||||||
body: "You haven't unlocked this challenge yet!",
|
body: "You haven't unlocked this challenge yet!",
|
||||||
button: "Got it!"
|
button: "Got it!"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChalWindow(obj);
|
updateChalWindow(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadchalbyname(chalname) {
|
function loadchalbyname(chalname) {
|
||||||
var obj = $.grep(challenges, function (e) {
|
var obj = $.grep(challenges, function(e) {
|
||||||
return e.name == chalname;
|
return e.name == chalname;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
updateChalWindow(obj);
|
updateChalWindow(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChalWindow(obj) {
|
function updateChalWindow(obj) {
|
||||||
$.get(script_root + "/api/v1/challenges/" + obj.id, function (response) {
|
$.get(script_root + "/api/v1/challenges/" + obj.id, function(response) {
|
||||||
var challenge_data = response.data;
|
var challenge_data = response.data;
|
||||||
|
|
||||||
$.getScript(script_root + obj.script, function () {
|
$.getScript(script_root + obj.script, function() {
|
||||||
$.get(script_root + obj.template, function (template_data) {
|
$.get(script_root + obj.template, function(template_data) {
|
||||||
$('#challenge-window').empty();
|
$("#challenge-window").empty();
|
||||||
var template = nunjucks.compile(template_data);
|
var template = nunjucks.compile(template_data);
|
||||||
window.challenge.data = challenge_data;
|
window.challenge.data = challenge_data;
|
||||||
window.challenge.preRender();
|
window.challenge.preRender();
|
||||||
|
|
||||||
challenge_data['description'] = window.challenge.render(challenge_data['description']);
|
challenge_data["description"] = window.challenge.render(
|
||||||
challenge_data['script_root'] = script_root;
|
challenge_data["description"]
|
||||||
|
);
|
||||||
|
challenge_data["script_root"] = script_root;
|
||||||
|
|
||||||
$('#challenge-window').append(template.render(challenge_data));
|
$("#challenge-window").append(template.render(challenge_data));
|
||||||
|
|
||||||
$('.challenge-solves').click(function (e) {
|
$(".challenge-solves").click(function(e) {
|
||||||
getsolves($('#challenge-id').val())
|
getsolves($("#challenge-id").val());
|
||||||
});
|
|
||||||
$('.nav-tabs a').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).tab('show')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle modal toggling
|
|
||||||
$('#challenge-window').on('hide.bs.modal', function (event) {
|
|
||||||
$("#submission-input").removeClass("wrong");
|
|
||||||
$("#submission-input").removeClass("correct");
|
|
||||||
$("#incorrect-key").slideUp();
|
|
||||||
$("#correct-key").slideUp();
|
|
||||||
$("#already-solved").slideUp();
|
|
||||||
$("#too-fast").slideUp();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#submit-key').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('#submit-key').addClass("disabled-button");
|
|
||||||
$('#submit-key').prop('disabled', true);
|
|
||||||
window.challenge.submit(function (data) {
|
|
||||||
renderSubmissionResponse(data);
|
|
||||||
loadchals(function () {
|
|
||||||
marksolves();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#submission-input").keyup(function (event) {
|
|
||||||
if (event.keyCode == 13) {
|
|
||||||
$("#submit-key").click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".input-field").bind({
|
|
||||||
focus: function () {
|
|
||||||
$(this).parent().addClass('input--filled');
|
|
||||||
$label = $(this).siblings(".input-label");
|
|
||||||
},
|
|
||||||
blur: function () {
|
|
||||||
if ($(this).val() === '') {
|
|
||||||
$(this).parent().removeClass('input--filled');
|
|
||||||
$label = $(this).siblings(".input-label");
|
|
||||||
$label.removeClass('input--hide');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.challenge.postRender();
|
|
||||||
|
|
||||||
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
|
|
||||||
$('#challenge-window').modal();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
$(".nav-tabs a").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).tab("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal toggling
|
||||||
|
$("#challenge-window").on("hide.bs.modal", function(event) {
|
||||||
|
$("#submission-input").removeClass("wrong");
|
||||||
|
$("#submission-input").removeClass("correct");
|
||||||
|
$("#incorrect-key").slideUp();
|
||||||
|
$("#correct-key").slideUp();
|
||||||
|
$("#already-solved").slideUp();
|
||||||
|
$("#too-fast").slideUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#submit-key").click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$("#submit-key").addClass("disabled-button");
|
||||||
|
$("#submit-key").prop("disabled", true);
|
||||||
|
window.challenge.submit(function(data) {
|
||||||
|
renderSubmissionResponse(data);
|
||||||
|
loadchals(function() {
|
||||||
|
marksolves();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#submission-input").keyup(function(event) {
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
$("#submit-key").click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".input-field").bind({
|
||||||
|
focus: function() {
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.addClass("input--filled");
|
||||||
|
$label = $(this).siblings(".input-label");
|
||||||
|
},
|
||||||
|
blur: function() {
|
||||||
|
if ($(this).val() === "") {
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.removeClass("input--filled");
|
||||||
|
$label = $(this).siblings(".input-label");
|
||||||
|
$label.removeClass("input--hide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.challenge.postRender();
|
||||||
|
|
||||||
|
window.location.replace(
|
||||||
|
window.location.href.split("#")[0] + "#" + obj.name
|
||||||
|
);
|
||||||
|
$("#challenge-window").modal();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#submission-input").keyup(function (event) {
|
$("#submission-input").keyup(function(event) {
|
||||||
if (event.keyCode == 13) {
|
if (event.keyCode == 13) {
|
||||||
$("#submit-key").click();
|
$("#submit-key").click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function renderSubmissionResponse(response, cb) {
|
function renderSubmissionResponse(response, cb) {
|
||||||
var result = response.data;
|
var result = response.data;
|
||||||
|
|
||||||
var result_message = $('#result-message');
|
var result_message = $("#result-message");
|
||||||
var result_notification = $('#result-notification');
|
var result_notification = $("#result-notification");
|
||||||
var answer_input = $("#submission-input");
|
var answer_input = $("#submission-input");
|
||||||
result_notification.removeClass();
|
result_notification.removeClass();
|
||||||
result_message.text(result.message);
|
result_message.text(result.message);
|
||||||
|
|
||||||
if (result.status === "authentication_required") {
|
if (result.status === "authentication_required") {
|
||||||
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash;
|
window.location =
|
||||||
return
|
script_root +
|
||||||
}
|
"/login?next=" +
|
||||||
else if (result.status === "incorrect") { // Incorrect key
|
script_root +
|
||||||
result_notification.addClass('alert alert-danger alert-dismissable text-center');
|
window.location.pathname +
|
||||||
result_notification.slideDown();
|
window.location.hash;
|
||||||
|
return;
|
||||||
|
} else if (result.status === "incorrect") {
|
||||||
|
// Incorrect key
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-danger alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
answer_input.removeClass("correct");
|
answer_input.removeClass("correct");
|
||||||
answer_input.addClass("wrong");
|
answer_input.addClass("wrong");
|
||||||
setTimeout(function () {
|
setTimeout(function() {
|
||||||
answer_input.removeClass("wrong");
|
answer_input.removeClass("wrong");
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
else if (result.status === "correct") { // Challenge Solved
|
|
||||||
result_notification.addClass('alert alert-success alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
|
|
||||||
$('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves"));
|
|
||||||
|
|
||||||
answer_input.val("");
|
|
||||||
answer_input.removeClass("wrong");
|
|
||||||
answer_input.addClass("correct");
|
|
||||||
}
|
|
||||||
else if (result.status === "already_solved") { // Challenge already solved
|
|
||||||
result_notification.addClass('alert alert-info alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
|
|
||||||
answer_input.addClass("correct");
|
|
||||||
}
|
|
||||||
else if (result.status === "paused") { // CTF is paused
|
|
||||||
result_notification.addClass('alert alert-warning alert-dismissable text-center');
|
|
||||||
result_notification.slideDown();
|
|
||||||
}
|
|
||||||
else if (result.status === "ratelimited") { // 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);
|
}, 3000);
|
||||||
|
} else if (result.status === "correct") {
|
||||||
|
// Challenge Solved
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-success alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
if (cb) {
|
$(".challenge-solves").text(
|
||||||
cb(result);
|
parseInt(
|
||||||
}
|
$(".challenge-solves")
|
||||||
|
.text()
|
||||||
|
.split(" ")[0]
|
||||||
|
) +
|
||||||
|
1 +
|
||||||
|
" Solves"
|
||||||
|
);
|
||||||
|
|
||||||
|
answer_input.val("");
|
||||||
|
answer_input.removeClass("wrong");
|
||||||
|
answer_input.addClass("correct");
|
||||||
|
} else if (result.status === "already_solved") {
|
||||||
|
// Challenge already solved
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-info alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
|
||||||
|
answer_input.addClass("correct");
|
||||||
|
} else if (result.status === "paused") {
|
||||||
|
// CTF is paused
|
||||||
|
result_notification.addClass(
|
||||||
|
"alert alert-warning alert-dismissable text-center"
|
||||||
|
);
|
||||||
|
result_notification.slideDown();
|
||||||
|
} else if (result.status === "ratelimited") {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function marksolves(cb) {
|
function marksolves(cb) {
|
||||||
$.get(script_root + '/api/v1/' + user_mode + '/me/solves', function (response) {
|
$.get(script_root + "/api/v1/" + user_mode + "/me/solves", function(
|
||||||
var solves = response.data;
|
response
|
||||||
for (var i = solves.length - 1; i >= 0; i--) {
|
) {
|
||||||
var id = solves[i].challenge_id;
|
var solves = response.data;
|
||||||
var btn = $('button[value="' + id + '"]');
|
for (var i = solves.length - 1; i >= 0; i--) {
|
||||||
btn.addClass('solved-challenge');
|
var id = solves[i].challenge_id;
|
||||||
btn.prepend("<i class='fas fa-check corner-button-check'></i>")
|
var btn = $('button[value="' + id + '"]');
|
||||||
}
|
btn.addClass("solved-challenge");
|
||||||
if (cb) {
|
btn.prepend("<i class='fas fa-check corner-button-check'></i>");
|
||||||
cb();
|
}
|
||||||
}
|
if (cb) {
|
||||||
});
|
cb();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_user_solves(cb) {
|
function load_user_solves(cb) {
|
||||||
if (authed) {
|
if (authed) {
|
||||||
$.get(script_root + '/api/v1/' + user_mode + '/me/solves', function (response) {
|
$.get(script_root + "/api/v1/" + user_mode + "/me/solves", function(
|
||||||
var solves = response.data;
|
response
|
||||||
|
) {
|
||||||
|
var solves = response.data;
|
||||||
|
|
||||||
for (var i = solves.length - 1; i >= 0; i--) {
|
for (var i = solves.length - 1; i >= 0; i--) {
|
||||||
var chal_id = solves[i].challenge_id;
|
var chal_id = solves[i].challenge_id;
|
||||||
user_solves.push(chal_id);
|
user_solves.push(chal_id);
|
||||||
|
}
|
||||||
}
|
if (cb) {
|
||||||
if (cb) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getsolves(id) {
|
function getsolves(id) {
|
||||||
$.get(script_root + '/api/v1/challenges/' + id + '/solves', function (response) {
|
$.get(script_root + "/api/v1/challenges/" + id + "/solves", function(
|
||||||
var data = response.data;
|
response
|
||||||
$('.challenge-solves').text(
|
) {
|
||||||
(parseInt(data.length) + " Solves")
|
var data = response.data;
|
||||||
);
|
$(".challenge-solves").text(parseInt(data.length) + " Solves");
|
||||||
var box = $('#challenge-solves-names');
|
var box = $("#challenge-solves-names");
|
||||||
box.empty();
|
box.empty();
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var id = data[i].account_id;
|
var id = data[i].account_id;
|
||||||
var name = data[i].name;
|
var name = data[i].name;
|
||||||
var date = moment(data[i].date).local().fromNow();
|
var date = moment(data[i].date)
|
||||||
var account_url = data[i].account_url
|
.local()
|
||||||
box.append('<tr><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(account_url, id, htmlentities(name), date));
|
.fromNow();
|
||||||
}
|
var account_url = data[i].account_url;
|
||||||
});
|
box.append(
|
||||||
|
'<tr><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(
|
||||||
|
account_url,
|
||||||
|
id,
|
||||||
|
htmlentities(name),
|
||||||
|
date
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadchals(cb) {
|
function loadchals(cb) {
|
||||||
$.get(script_root + "/api/v1/challenges", function (response) {
|
$.get(script_root + "/api/v1/challenges", function(response) {
|
||||||
var categories = [];
|
var categories = [];
|
||||||
challenges = response.data;
|
challenges = response.data;
|
||||||
|
|
||||||
$('#challenges-board').empty();
|
$("#challenges-board").empty();
|
||||||
|
|
||||||
for (var i = challenges.length - 1; i >= 0; i--) {
|
for (var i = challenges.length - 1; i >= 0; i--) {
|
||||||
challenges[i].solves = 0;
|
challenges[i].solves = 0;
|
||||||
if ($.inArray(challenges[i].category, categories) == -1) {
|
if ($.inArray(challenges[i].category, categories) == -1) {
|
||||||
var category = challenges[i].category;
|
var category = challenges[i].category;
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
|
|
||||||
var categoryid = category.replace(/ /g, "-").hashCode();
|
var categoryid = category.replace(/ /g, "-").hashCode();
|
||||||
var categoryrow = $('' +
|
var categoryrow = $(
|
||||||
'<div id="{0}-row" class="pt-5">'.format(categoryid) +
|
"" +
|
||||||
'<div class="category-header col-md-12 mb-3">' +
|
'<div id="{0}-row" class="pt-5">'.format(categoryid) +
|
||||||
'</div>' +
|
'<div class="category-header col-md-12 mb-3">' +
|
||||||
'<div class="category-challenges col-md-12">' +
|
"</div>" +
|
||||||
'<div class="challenges-row col-md-12"></div>' +
|
'<div class="category-challenges col-md-12">' +
|
||||||
'</div>' +
|
'<div class="challenges-row col-md-12"></div>' +
|
||||||
'</div>');
|
"</div>" +
|
||||||
categoryrow.find(".category-header").append($("<h3>" + category + "</h3>"));
|
"</div>"
|
||||||
|
);
|
||||||
|
categoryrow
|
||||||
|
.find(".category-header")
|
||||||
|
.append($("<h3>" + category + "</h3>"));
|
||||||
|
|
||||||
$('#challenges-board').append(categoryrow);
|
$("#challenges-board").append(categoryrow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i <= challenges.length - 1; i++) {
|
for (var i = 0; i <= challenges.length - 1; i++) {
|
||||||
var chalinfo = challenges[i];
|
var chalinfo = challenges[i];
|
||||||
var challenge = chalinfo.category.replace(/ /g, "-").hashCode();
|
var challenge = chalinfo.category.replace(/ /g, "-").hashCode();
|
||||||
var chalid = chalinfo.name.replace(/ /g, "-").hashCode();
|
var chalid = chalinfo.name.replace(/ /g, "-").hashCode();
|
||||||
var catid = chalinfo.category.replace(/ /g, "-").hashCode();
|
var catid = chalinfo.category.replace(/ /g, "-").hashCode();
|
||||||
var chalwrap = $("<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid));
|
var chalwrap = $(
|
||||||
|
"<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid)
|
||||||
|
);
|
||||||
|
|
||||||
if (user_solves.indexOf(chalinfo.id) == -1) {
|
if (user_solves.indexOf(chalinfo.id) == -1) {
|
||||||
var chalbutton = $("<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(chalinfo.id));
|
var chalbutton = $(
|
||||||
} else {
|
"<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(
|
||||||
var chalbutton = $("<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(chalinfo.id));
|
chalinfo.id
|
||||||
}
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var chalbutton = $(
|
||||||
|
"<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(
|
||||||
|
chalinfo.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var chalheader = $("<p>{0}</p>".format(chalinfo.name));
|
var chalheader = $("<p>{0}</p>".format(chalinfo.name));
|
||||||
var chalscore = $("<span>{0}</span>".format(chalinfo.value));
|
var chalscore = $("<span>{0}</span>".format(chalinfo.value));
|
||||||
for (var j = 0; j < chalinfo.tags.length; j++) {
|
for (var j = 0; j < chalinfo.tags.length; j++) {
|
||||||
var tag = 'tag-' + chalinfo.tags[j].value.replace(/ /g, '-');
|
var tag = "tag-" + chalinfo.tags[j].value.replace(/ /g, "-");
|
||||||
chalwrap.addClass(tag);
|
chalwrap.addClass(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
chalbutton.append(chalheader);
|
chalbutton.append(chalheader);
|
||||||
chalbutton.append(chalscore);
|
chalbutton.append(chalscore);
|
||||||
chalwrap.append(chalbutton);
|
chalwrap.append(chalbutton);
|
||||||
|
|
||||||
$("#" + catid + "-row").find(".category-challenges > .challenges-row").append(chalwrap);
|
$("#" + catid + "-row")
|
||||||
}
|
.find(".category-challenges > .challenges-row")
|
||||||
|
.append(chalwrap);
|
||||||
|
}
|
||||||
|
|
||||||
$('.challenge-button').click(function (e) {
|
$(".challenge-button").click(function(e) {
|
||||||
loadchal(this.value);
|
loadchal(this.value);
|
||||||
getsolves(this.value);
|
getsolves(this.value);
|
||||||
});
|
|
||||||
|
|
||||||
if (cb) {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$("#submit-key").click(function(e) {
|
||||||
|
submitkey(
|
||||||
$('#submit-key').click(function (e) {
|
$("#challenge-id").val(),
|
||||||
submitkey($('#challenge-id').val(), $('#submission-input').val(), $('#nonce').val())
|
$("#submission-input").val(),
|
||||||
|
$("#nonce").val()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.challenge-solves').click(function (e) {
|
$(".challenge-solves").click(function(e) {
|
||||||
getsolves($('#challenge-id').val())
|
getsolves($("#challenge-id").val());
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#challenge-window').on('hide.bs.modal', function (event) {
|
$("#challenge-window").on("hide.bs.modal", function(event) {
|
||||||
$("#submission-input").removeClass("wrong");
|
$("#submission-input").removeClass("wrong");
|
||||||
$("#submission-input").removeClass("correct");
|
$("#submission-input").removeClass("correct");
|
||||||
$("#incorrect-key").slideUp();
|
$("#incorrect-key").slideUp();
|
||||||
$("#correct-key").slideUp();
|
$("#correct-key").slideUp();
|
||||||
$("#already-solved").slideUp();
|
$("#already-solved").slideUp();
|
||||||
$("#too-fast").slideUp();
|
$("#too-fast").slideUp();
|
||||||
});
|
});
|
||||||
|
|
||||||
var load_location_hash = function () {
|
var load_location_hash = function() {
|
||||||
if (window.location.hash.length > 0) {
|
if (window.location.hash.length > 0) {
|
||||||
loadchalbyname(decodeURIComponent(window.location.hash.substring(1)));
|
loadchalbyname(decodeURIComponent(window.location.hash.substring(1)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function update(cb) {
|
function update(cb) {
|
||||||
load_user_solves(function () { // Load the user's solved challenge ids
|
load_user_solves(function() {
|
||||||
loadchals(function () { // Load the full list of challenges
|
// Load the user's solved challenge ids
|
||||||
if (cb) {
|
loadchals(function() {
|
||||||
cb();
|
// Load the full list of challenges
|
||||||
}
|
if (cb) {
|
||||||
});
|
cb();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function () {
|
$(function() {
|
||||||
update(function () {
|
update(function() {
|
||||||
load_location_hash();
|
load_location_hash();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.nav-tabs a').click(function (e) {
|
$(".nav-tabs a").click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(this).tab('show')
|
$(this).tab("show");
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#challenge-window').on('hidden.bs.modal', function () {
|
$("#challenge-window").on("hidden.bs.modal", function() {
|
||||||
$('.nav-tabs a:first').tab('show');
|
$(".nav-tabs a:first").tab("show");
|
||||||
history.replaceState('', document.title, window.location.pathname);
|
history.replaceState("", document.title, window.location.pathname);
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(update, 300000);
|
setInterval(update, 300000);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue