Merge pull request #1361 from CTFd/2.4.0-dev

2.4.0 / 2020-05-04
==================

**General**
* Cache user and team attributes and use those perform certain page operations intead of going to the database for data
    * After modifying the user/team attributes you should call the appropriate cache clearing function (clear_user_session/clear_team_session)
* Cache user IPs for the last hour to avoid hitting the database on every authenticated page view
    * Update the user IP's last seen value at least every hour or on every non-GET request
* Replace `flask_restplus` with `flask_restx`
* Remove `datafreeze`, `normality`, and `banal` dependencies in favor of in-repo solutions to exporting database

**Admin Panel**
* Add bulk selection and deletion for Users, Teams, Scoreboard, Challenges, Submissions
* Make some Admin tables sortable by table headers
* Create a score distribution graph in the statistics page
* Make instance reset more granular to allow for choosing to reset Accounts, Submissions, Challenges, Pages, and/or Notificatoins
* Properly update challenge visibility after updating challenge
* Show total possible points in Statistics page
* Add searching for Users, Teams, Challenges, Submissions
* Move User IP addresses into a modal
* Move Team IP addresses into a modal
* Show User website in a user page button
* Show Team website in a team page button
* Make the Pages editor use proper HTML syntax highlighting
* Theme header and footer editors now use CodeMirror
* Make default CodeMirror font-size 12px
* Stop storing last action via location hash and switch to using sessionStorage

**Themes**
* Make page selection a select and option instead of having a lot of page links
* Add the JSEnum class to create constants that can be accessed from webpack. Generate constants with `python manage.py build jsenums`
* Add the JinjaEnum class to inject constants into the Jinja environment to access from themes
* Update jQuery to 3.5.0 to resolve potential security issue
* Add some new CSS utilities (`.min-vh-*` and `.opacity-*`)
* Change some rows to have a minimum height so they don't render oddly without data
* Deprecate `.spinner-error` CSS class
* Deprecate accessing the type variable to check user role. Instead you should use `is_admin()`

**Miscellaneous**
* Enable foreign key enforcement for SQLite. Only really matters for the debug server.
* Remove the duplicated `get_config` from `CTFd.models`
* Fix possible email sending issues in Python 3 by using `EmailMessage`
* Dont set User type in the user side session. Instead it should be set in the new user attributes
* Fix flask-profiler and bump dependency to 1.8.1
* Switch to using the `Faker` library for `populate.py` instead of hardcoded data
* Add a `yarn lint` command to run eslint on JS files
* Always insert the current CTFd version at the end of the import process
* Fix issue where files could not be downloaded on Windows
bulk-clear-sessions
Kevin Chung 2020-05-04 02:25:26 -04:00 committed by GitHub
commit bd4900b896
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
172 changed files with 3454 additions and 2444 deletions

17
.eslintrc.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
}
};

3
.gitignore vendored
View File

@ -73,3 +73,6 @@ CTFd/uploads
# JS
node_modules/
# Flask Profiler files
flask_profiler.sql

View File

@ -1,3 +1,53 @@
2.4.0 / 2020-05-04
==================
**General**
* Cache user and team attributes and use those perform certain page operations intead of going to the database for data
* After modifying the user/team attributes you should call the appropriate cache clearing function (clear_user_session/clear_team_session)
* Cache user IPs for the last hour to avoid hitting the database on every authenticated page view
* Update the user IP's last seen value at least every hour or on every non-GET request
* Replace `flask_restplus` with `flask_restx`
* Remove `datafreeze`, `normality`, and `banal` dependencies in favor of in-repo solutions to exporting database
**Admin Panel**
* Add bulk selection and deletion for Users, Teams, Scoreboard, Challenges, Submissions
* Make some Admin tables sortable by table headers
* Create a score distribution graph in the statistics page
* Make instance reset more granular to allow for choosing to reset Accounts, Submissions, Challenges, Pages, and/or Notificatoins
* Properly update challenge visibility after updating challenge
* Show total possible points in Statistics page
* Add searching for Users, Teams, Challenges, Submissions
* Move User IP addresses into a modal
* Move Team IP addresses into a modal
* Show User website in a user page button
* Show Team website in a team page button
* Make the Pages editor use proper HTML syntax highlighting
* Theme header and footer editors now use CodeMirror
* Make default CodeMirror font-size 12px
* Stop storing last action via location hash and switch to using sessionStorage
**Themes**
* Make page selection a select and option instead of having a lot of page links
* Add the JSEnum class to create constants that can be accessed from webpack. Generate constants with `python manage.py build jsenums`
* Add the JinjaEnum class to inject constants into the Jinja environment to access from themes
* Update jQuery to 3.5.0 to resolve potential security issue
* Add some new CSS utilities (`.min-vh-*` and `.opacity-*`)
* Change some rows to have a minimum height so they don't render oddly without data
* Deprecate `.spinner-error` CSS class
* Deprecate accessing the type variable to check user role. Instead you should use `is_admin()`
**Miscellaneous**
* Enable foreign key enforcement for SQLite. Only really matters for the debug server.
* Remove the duplicated `get_config` from `CTFd.models`
* Fix possible email sending issues in Python 3 by using `EmailMessage`
* Dont set User type in the user side session. Instead it should be set in the new user attributes
* Fix flask-profiler and bump dependency to 1.8.1
* Switch to using the `Faker` library for `populate.py` instead of hardcoded data
* Add a `yarn lint` command to run eslint on JS files
* Always insert the current CTFd version at the end of the import process
* Fix issue where files could not be downloaded on Windows
2.3.3 / 2020-04-12
==================

View File

@ -31,7 +31,7 @@ if sys.version_info[0] < 3:
reload(sys) # noqa: F821
sys.setdefaultencoding("utf-8")
__version__ = "2.3.3"
__version__ = "2.4.0"
class CTFdRequest(Request):
@ -179,6 +179,19 @@ def create_app(config="CTFd.config.Config"):
# Alembic sqlite support is lacking so we should just create_all anyway
if url.drivername.startswith("sqlite"):
# Enable foreign keys for SQLite. This must be before the
# db.create_all call because tests use the in-memory SQLite
# database (each connection, including db creation, is a new db).
# https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#foreign-key-support
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
db.create_all()
stamp_latest_revision()
else:

View File

@ -14,10 +14,13 @@ from flask import (
url_for,
)
from CTFd.cache import cache, clear_config
from CTFd.cache import cache, clear_config, clear_standings, clear_pages
from CTFd.models import (
Awards,
Challenges,
Configs,
Notifications,
Pages,
Solves,
Submissions,
Teams,
@ -34,6 +37,7 @@ from CTFd.utils.exports import export_ctf as export_ctf_util
from CTFd.utils.exports import import_ctf as import_ctf_util
from CTFd.utils.helpers import get_errors
from CTFd.utils.security.auth import logout_user
from CTFd.utils.uploads import delete_file
from CTFd.utils.user import is_admin
admin = Blueprint("admin", __name__)
@ -176,19 +180,60 @@ def config():
@admins_only
def reset():
if request.method == "POST":
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
Tracking.query.delete()
Solves.query.delete()
Submissions.query.delete()
Awards.query.delete()
Unlocks.query.delete()
Users.query.delete()
Teams.query.delete()
set_config("setup", False)
require_setup = False
logout = False
next_url = url_for("admin.statistics")
data = request.form
if data.get("pages"):
_pages = Pages.query.all()
for p in _pages:
for f in p.files:
delete_file(file_id=f.id)
Pages.query.delete()
if data.get("notifications"):
Notifications.query.delete()
if data.get("challenges"):
_challenges = Challenges.query.all()
for c in _challenges:
for f in c.files:
delete_file(file_id=f.id)
Challenges.query.delete()
if data.get("accounts"):
Users.query.delete()
Teams.query.delete()
require_setup = True
logout = True
if data.get("submissions"):
Solves.query.delete()
Submissions.query.delete()
Awards.query.delete()
Unlocks.query.delete()
Tracking.query.delete()
if require_setup:
set_config("setup", False)
cache.clear()
logout_user()
next_url = url_for("views.setup")
db.session.commit()
cache.clear()
logout_user()
clear_pages()
clear_standings()
clear_config()
if logout is True:
cache.clear()
logout_user()
db.session.close()
return redirect(url_for("views.setup"))
return redirect(next_url)
return render_template("admin/reset.html")

View File

@ -2,7 +2,7 @@ import os
import six
from flask import current_app as app
from flask import render_template, render_template_string, url_for
from flask import render_template, render_template_string, request, url_for
from CTFd.admin import admin
from CTFd.models import Challenges, Flags, Solves
@ -14,8 +14,26 @@ from CTFd.utils.decorators import admins_only
@admin.route("/admin/challenges")
@admins_only
def challenges_listing():
challenges = Challenges.query.all()
return render_template("admin/challenges/challenges.html", challenges=challenges)
q = request.args.get("q")
field = request.args.get("field")
filters = []
if q:
# The field exists as an exposed column
if Challenges.__mapper__.has_property(field):
filters.append(getattr(Challenges, field).like("%{}%".format(q)))
query = Challenges.query.filter(*filters).order_by(Challenges.id.asc())
challenges = query.all()
total = query.count()
return render_template(
"admin/challenges/challenges.html",
challenges=challenges,
total=total,
q=q,
field=field,
)
@admin.route("/admin/challenges/<int:challenge_id>")

View File

@ -31,6 +31,13 @@ def statistics():
challenge_count = Challenges.query.count()
total_points = (
Challenges.query.with_entities(db.func.sum(Challenges.value).label("sum"))
.filter_by(state="visible")
.first()
.sum
) or 0
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
solves_sub = (
@ -73,6 +80,7 @@ def statistics():
wrong_count=wrong_count,
solve_count=solve_count,
challenge_count=challenge_count,
total_points=total_points,
solve_data=solve_data,
most_solved=most_solved,
least_solved=least_solved,

View File

@ -1,4 +1,4 @@
from flask import render_template, request
from flask import render_template, request, url_for
from CTFd.admin import admin
from CTFd.models import Challenges, Submissions
@ -10,16 +10,21 @@ from CTFd.utils.modes import get_model
@admin.route("/admin/submissions/<submission_type>")
@admins_only
def submissions_listing(submission_type):
filters = {}
filters_by = {}
if submission_type:
filters["type"] = submission_type
filters_by["type"] = submission_type
filters = []
curr_page = abs(int(request.args.get("page", 1, type=int)))
results_per_page = 50
page_start = results_per_page * (curr_page - 1)
page_end = results_per_page * (curr_page - 1) + results_per_page
sub_count = Submissions.query.filter_by(**filters).count()
page_count = int(sub_count / results_per_page) + (sub_count % results_per_page > 0)
q = request.args.get("q")
field = request.args.get("field")
page = abs(request.args.get("page", 1, type=int))
if q:
submissions = []
if Submissions.__mapper__.has_property(
field
): # The field exists as an exposed column
filters.append(getattr(Submissions, field).like("%{}%".format(q)))
Model = get_model()
@ -34,18 +39,27 @@ def submissions_listing(submission_type):
Challenges.name.label("challenge_name"),
Model.name.label("team_name"),
)
.filter_by(**filters)
.filter_by(**filters_by)
.filter(*filters)
.join(Challenges)
.join(Model)
.order_by(Submissions.date.desc())
.slice(page_start, page_end)
.all()
.paginate(page=page, per_page=50)
)
args = dict(request.args)
args.pop("page", 1)
return render_template(
"admin/submissions.html",
submissions=submissions,
page_count=page_count,
curr_page=curr_page,
prev_page=url_for(
request.endpoint, type=submission_type, page=submissions.prev_num, **args
),
next_page=url_for(
request.endpoint, type=submission_type, page=submissions.next_num, **args
),
type=submission_type,
q=q,
field=field,
)

View File

@ -1,64 +1,40 @@
from flask import render_template, request
from flask import render_template, request, url_for
from sqlalchemy.sql import not_
from CTFd.admin import admin
from CTFd.models import Challenges, Teams, Tracking, db
from CTFd.models import Challenges, Teams, Tracking
from CTFd.utils.decorators import admins_only
from CTFd.utils.helpers import get_errors
@admin.route("/admin/teams")
@admins_only
def teams_listing():
page = abs(request.args.get("page", 1, type=int))
q = request.args.get("q")
field = request.args.get("field")
page = abs(request.args.get("page", 1, type=int))
filters = []
if q:
field = request.args.get("field")
teams = []
errors = get_errors()
if field == "id":
if q.isnumeric():
teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all()
else:
teams = []
errors.append("Your ID search term is not numeric")
elif field == "name":
teams = (
Teams.query.filter(Teams.name.like("%{}%".format(q)))
.order_by(Teams.id.asc())
.all()
)
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=0,
curr_page=None,
q=q,
field=field,
)
# The field exists as an exposed column
if Teams.__mapper__.has_property(field):
filters.append(getattr(Teams, field).like("%{}%".format(q)))
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
teams = (
Teams.query.filter(*filters)
.order_by(Teams.id.asc())
.paginate(page=page, per_page=50)
)
args = dict(request.args)
args.pop("page", 1)
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]
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
"admin/teams/teams.html",
teams=teams,
prev_page=url_for(request.endpoint, page=teams.prev_num, **args),
next_page=url_for(request.endpoint, page=teams.next_num, **args),
q=q,
field=field,
)

View File

@ -1,75 +1,51 @@
from flask import render_template, request
from flask import render_template, request, url_for
from sqlalchemy.sql import not_
from CTFd.admin import admin
from CTFd.models import Challenges, Tracking, Users, db
from CTFd.models import Challenges, Tracking, Users
from CTFd.utils import get_config
from CTFd.utils.decorators import admins_only
from CTFd.utils.helpers import get_errors
from CTFd.utils.modes import TEAMS_MODE
@admin.route("/admin/users")
@admins_only
def users_listing():
page = abs(request.args.get("page", 1, type=int))
q = request.args.get("q")
if q:
field = request.args.get("field")
users = []
errors = get_errors()
if field == "id":
if q.isnumeric():
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
else:
users = []
errors.append("Your ID search term is not numeric")
elif field == "name":
users = (
Users.query.filter(Users.name.like("%{}%".format(q)))
.order_by(Users.id.asc())
.all()
)
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()
)
elif field == "ip":
users = (
Users.query.join(Tracking, Users.id == Tracking.user_id)
.filter(Tracking.ip.like("%{}%".format(q)))
.order_by(Users.id.asc())
.all()
)
field = request.args.get("field")
page = abs(request.args.get("page", 1, type=int))
filters = []
users = []
return render_template(
"admin/users/users.html",
users=users,
pages=0,
curr_page=None,
q=q,
field=field,
if q:
# The field exists as an exposed column
if Users.__mapper__.has_property(field):
filters.append(getattr(Users, field).like("%{}%".format(q)))
if q and field == "ip":
users = (
Users.query.join(Tracking, Users.id == Tracking.user_id)
.filter(Tracking.ip.like("%{}%".format(q)))
.order_by(Users.id.asc())
.paginate(page=page, per_page=50)
)
else:
users = (
Users.query.filter(*filters)
.order_by(Users.id.asc())
.paginate(page=page, per_page=50)
)
page = abs(int(page))
results_per_page = 50
page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page
users = Users.query.order_by(Users.id.asc()).slice(page_start, page_end).all()
count = db.session.query(db.func.count(Users.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 0)
args = dict(request.args)
args.pop("page", 1)
return render_template(
"admin/users/users.html", users=users, pages=pages, curr_page=page
"admin/users/users.html",
users=users,
prev_page=url_for(request.endpoint, page=users.prev_num, **args),
next_page=url_for(request.endpoint, page=users.next_num, **args),
q=q,
field=field,
)

View File

@ -1,5 +1,5 @@
from flask import Blueprint, current_app
from flask_restplus import Api
from flask_restx import Api
from CTFd.api.v1.awards import awards_namespace
from CTFd.api.v1.challenges import challenges_namespace

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings
from CTFd.utils.config import is_teams_mode

View File

@ -1,7 +1,7 @@
import datetime
from flask import abort, request, url_for
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from sqlalchemy.sql import and_
from CTFd.cache import clear_standings
@ -378,7 +378,7 @@ class ChallengeAttempt(Resource):
log(
"submissions",
"[{date}] {name} submitted {submission} on {challenge_id} with kpm {kpm} [TOO FAST]",
submission=request_data["submission"].encode("utf-8"),
submission=request_data.get("submission", "").encode("utf-8"),
challenge_id=challenge_id,
kpm=kpm,
)
@ -425,7 +425,7 @@ class ChallengeAttempt(Resource):
log(
"submissions",
"[{date}] {name} submitted {submission} on {challenge_id} with kpm {kpm} [CORRECT]",
submission=request_data["submission"].encode("utf-8"),
submission=request_data.get("submission", "").encode("utf-8"),
challenge_id=challenge_id,
kpm=kpm,
)
@ -443,7 +443,7 @@ class ChallengeAttempt(Resource):
log(
"submissions",
"[{date}] {name} submitted {submission} on {challenge_id} with kpm {kpm} [WRONG]",
submission=request_data["submission"].encode("utf-8"),
submission=request_data.get("submission", "").encode("utf-8"),
challenge_id=challenge_id,
kpm=kpm,
)
@ -477,7 +477,7 @@ class ChallengeAttempt(Resource):
log(
"submissions",
"[{date}] {name} submitted {submission} on {challenge_id} with kpm {kpm} [ALREADY SOLVED]",
submission=request_data["submission"].encode("utf-8"),
submission=request_data.get("submission", "").encode("utf-8"),
challenge_id=challenge_id,
kpm=kpm,
)

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_config, clear_standings
from CTFd.models import Configs, db

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Files, db
from CTFd.schemas.files import FileSchema

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Flags, db
from CTFd.plugins.flags import FLAG_CLASSES, get_flag_class

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Hints, HintUnlocks, db
from CTFd.schemas.hints import HintSchema

View File

@ -1,5 +1,5 @@
from flask import current_app, request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Notifications, db
from CTFd.schemas.notifications import NotificationSchema

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_pages
from CTFd.models import Pages, db

View File

@ -1,4 +1,4 @@
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import cache, make_cache_key
from CTFd.models import Awards, Solves, Teams

View File

@ -1,4 +1,4 @@
from flask_restplus import Namespace
from flask_restx import Namespace
statistics_namespace = Namespace(
"statistics", description="Endpoint to retrieve Statistics"
@ -8,3 +8,4 @@ from CTFd.api.v1.statistics import challenges # noqa: F401
from CTFd.api.v1.statistics import submissions # noqa: F401
from CTFd.api.v1.statistics import teams # noqa: F401
from CTFd.api.v1.statistics import users # noqa: F401
from CTFd.api.v1.statistics import scores # noqa: F401

View File

@ -1,4 +1,4 @@
from flask_restplus import Resource
from flask_restx import Resource
from sqlalchemy import func
from sqlalchemy.sql import or_

View File

@ -0,0 +1,43 @@
from collections import defaultdict
from flask_restx import Resource
from CTFd.api.v1.statistics import statistics_namespace
from CTFd.models import db, Challenges
from CTFd.utils.decorators import admins_only
from CTFd.utils.scores import get_standings
@statistics_namespace.route("/scores/distribution")
class ScoresDistribution(Resource):
@admins_only
def get(self):
challenge_count = Challenges.query.count() or 1
total_points = (
Challenges.query.with_entities(db.func.sum(Challenges.value).label("sum"))
.filter_by(state="visible")
.first()
.sum
) or 0
# Convert Decimal() to int in some database backends for Python 2
total_points = int(total_points)
# Divide score by challenges to get brackets with explicit floor division
bracket_size = total_points // challenge_count
# Get standings
standings = get_standings(admin=True)
# Iterate over standings and increment the count for each bracket for each standing within that bracket
bottom, top = 0, bracket_size
count = 1
brackets = defaultdict(lambda: 0)
for t in reversed(standings):
if ((t.score >= bottom) and (t.score <= top)) or t.score <= 0:
brackets[top] += 1
else:
count += 1
bottom, top = (bracket_size, (bracket_size * count))
brackets[top] += 1
return {"success": True, "data": {"brackets": brackets}}

View File

@ -1,4 +1,4 @@
from flask_restplus import Resource
from flask_restx import Resource
from sqlalchemy import func
from CTFd.api.v1.statistics import statistics_namespace

View File

@ -1,4 +1,4 @@
from flask_restplus import Resource
from flask_restx import Resource
from CTFd.api.v1.statistics import statistics_namespace
from CTFd.models import Teams

View File

@ -1,4 +1,4 @@
from flask_restplus import Resource
from flask_restx import Resource
from sqlalchemy import func
from CTFd.api.v1.statistics import statistics_namespace

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings
from CTFd.models import Submissions, db

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Tags, db
from CTFd.schemas.tags import TagSchema

View File

@ -1,9 +1,9 @@
import copy
from flask import abort, request, session
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings
from CTFd.cache import clear_standings, clear_team_session, clear_user_session
from CTFd.models import Awards, Submissions, Teams, Unlocks, Users, db
from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.submissions import SubmissionSchema
@ -13,7 +13,7 @@ from CTFd.utils.decorators.visibility import (
check_account_visibility,
check_score_visibility,
)
from CTFd.utils.user import get_current_team, is_admin
from CTFd.utils.user import get_current_team, get_current_user_type, is_admin
teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams")
@ -23,7 +23,8 @@ class TeamList(Resource):
@check_account_visibility
def get(self):
teams = Teams.query.filter_by(hidden=False, banned=False)
view = copy.deepcopy(TeamSchema.views.get(session.get("type", "user")))
user_type = get_current_user_type(fallback="user")
view = copy.deepcopy(TeamSchema.views.get(user_type))
view.remove("members")
response = TeamSchema(view=view, many=True).dump(teams)
@ -35,7 +36,8 @@ class TeamList(Resource):
@admins_only
def post(self):
req = request.get_json()
view = TeamSchema.views.get(session.get("type", "self"))
user_type = get_current_user_type()
view = TeamSchema.views.get(user_type)
schema = TeamSchema(view=view)
response = schema.load(req)
@ -63,7 +65,8 @@ class TeamPublic(Resource):
if (team.banned or team.hidden) and is_admin() is False:
abort(404)
view = TeamSchema.views.get(session.get("type", "user"))
user_type = get_current_user_type(fallback="user")
view = TeamSchema.views.get(user_type)
schema = TeamSchema(view=view)
response = schema.dump(team)
@ -88,25 +91,31 @@ class TeamPublic(Resource):
response = schema.dump(response.data)
db.session.commit()
db.session.close()
clear_team_session(team_id=team.id)
clear_standings()
db.session.close()
return {"success": True, "data": response.data}
@admins_only
def delete(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404()
team_id = team.id
for member in team.members:
member.team_id = None
clear_user_session(user_id=member.id)
db.session.delete(team)
db.session.commit()
db.session.close()
clear_team_session(team_id=team_id)
clear_standings()
db.session.close()
return {"success": True}
@ -147,7 +156,7 @@ class TeamPrivate(Resource):
return {"success": False, "errors": response.errors}, 400
db.session.commit()
clear_team_session(team_id=team.id)
response = TeamSchema("self").dump(response.data)
db.session.close()

View File

@ -1,13 +1,13 @@
import datetime
from flask import request, session
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.models import Tokens, db
from CTFd.schemas.tokens import TokenSchema
from CTFd.utils.decorators import authed_only, require_verified_emails
from CTFd.utils.security.auth import generate_user_token
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.user import get_current_user, get_current_user_type, is_admin
tokens_namespace = Namespace("tokens", description="Endpoint to retrieve Tokens")
@ -62,7 +62,8 @@ class TokenDetail(Resource):
id=token_id, user_id=session["id"]
).first_or_404()
schema = TokenSchema(view=session.get("type", "user"))
user_type = get_current_user_type(fallback="user")
schema = TokenSchema(view=user_type)
response = schema.dump(token)
if response.errors:

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restplus import Namespace, Resource
from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings
from CTFd.models import Unlocks, db, get_class_by_tablename

View File

@ -1,7 +1,7 @@
from flask import abort, request, session
from flask_restplus import Namespace, Resource
from flask import abort, request
from flask_restx import Namespace, Resource
from CTFd.cache import clear_standings
from CTFd.cache import clear_standings, clear_user_session
from CTFd.models import (
Awards,
Notifications,
@ -22,7 +22,7 @@ from CTFd.utils.decorators.visibility import (
check_score_visibility,
)
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, get_current_user_type, is_admin
users_namespace = Namespace("users", description="Endpoint to retrieve Users")
@ -80,7 +80,8 @@ class UserPublic(Resource):
if (user.banned or user.hidden) and is_admin() is False:
abort(404)
response = UserSchema(view=session.get("type", "user")).dump(user)
user_type = get_current_user_type(fallback="user")
response = UserSchema(view=user_type).dump(user)
if response.errors:
return {"success": False, "errors": response.errors}, 400
@ -106,6 +107,7 @@ class UserPublic(Resource):
db.session.close()
clear_user_session(user_id=user_id)
clear_standings()
return {"success": True, "data": response}
@ -122,6 +124,7 @@ class UserPublic(Resource):
db.session.commit()
db.session.close()
clear_user_session(user_id=user_id)
clear_standings()
return {"success": True}
@ -148,6 +151,7 @@ class UserPrivate(Resource):
db.session.commit()
clear_user_session(user_id=user.id)
response = schema.dump(response.data)
db.session.close()

View File

@ -9,6 +9,7 @@ from itsdangerous.exc import BadSignature, BadTimeSignature, SignatureExpired
from CTFd.models import Teams, Users, db
from CTFd.utils import config, email, get_app_config, get_config
from CTFd.utils import user as current_user
from CTFd.cache import clear_user_session, clear_team_session
from CTFd.utils import validators
from CTFd.utils.config import is_teams_mode
from CTFd.utils.config.integrations import mlc_registration
@ -57,6 +58,7 @@ def confirm(data=None):
name=user.name,
)
db.session.commit()
clear_user_session(user_id=user.id)
email.successful_registration_notification(user.email)
db.session.close()
if current_user.authed():
@ -126,6 +128,7 @@ def reset_password(data=None):
user.password = password
db.session.commit()
clear_user_session(user_id=user.id)
log(
"logins",
format="[{date}] {ip} - successful password reset for {name}",
@ -411,6 +414,7 @@ def oauth_redirect():
team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id)
db.session.add(team)
db.session.commit()
clear_team_session(team_id=team.id)
team_size_limit = get_config("team_size", default=0)
if team_size_limit and len(team.members) >= team_size_limit:
@ -428,6 +432,7 @@ def oauth_redirect():
user.oauth_id = user_id
user.verified = True
db.session.commit()
clear_user_session(user_id=user.id)
login_user(user)

View File

@ -44,3 +44,21 @@ def clear_pages():
cache.delete_memoized(get_pages)
cache.delete_memoized(get_page)
def clear_user_recent_ips(user_id):
from CTFd.utils.user import get_user_recent_ips
cache.delete_memoized(get_user_recent_ips, user_id=user_id)
def clear_user_session(user_id):
from CTFd.utils.user import get_user_attrs
cache.delete_memoized(get_user_attrs, user_id=user_id)
def clear_team_session(team_id):
from CTFd.utils.user import get_team_attrs
cache.delete_memoized(get_team_attrs, team_id=team_id)

View File

@ -0,0 +1,63 @@
from enum import Enum
from flask import current_app
JS_ENUMS = {}
class RawEnum(Enum):
"""
This is a customized enum class which should be used with a mixin.
The mixin should define the types of each member.
For example:
class Colors(str, RawEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
"""
def __str__(self):
return str(self._value_)
@classmethod
def keys(cls):
return list(cls.__members__.keys())
@classmethod
def values(cls):
return list(cls.__members__.values())
@classmethod
def test(cls, value):
try:
return bool(cls(value))
except ValueError:
return False
def JSEnum(cls):
"""
This is a decorator used to gather all Enums which should be shared with
the CTFd front end. The JS_Enums dictionary can be taken be a script and
compiled into a JavaScript file for use by frontend assets. JS_Enums
should not be passed directly into Jinja. A JinjaEnum is better for that.
"""
if cls.__name__ not in JS_ENUMS:
JS_ENUMS[cls.__name__] = dict(cls.__members__)
else:
raise KeyError("{} was already defined as a JSEnum".format(cls.__name__))
return cls
def JinjaEnum(cls):
"""
This is a decorator used to inject the decorated Enum into Jinja globals
which allows you to access it from the front end. If you need to access
an Enum from JS, a better tool to use is the JSEnum decorator.
"""
if cls.__name__ not in current_app.jinja_env.globals:
current_app.jinja_env.globals[cls.__name__] = cls
else:
raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__))
return cls

20
CTFd/constants/teams.py Normal file
View File

@ -0,0 +1,20 @@
from collections import namedtuple
TeamAttrs = namedtuple(
"TeamAttrs",
[
"id",
"oauth_id",
"name",
"email",
"secret",
"website",
"affiliation",
"country",
"bracket",
"hidden",
"banned",
"captain_id",
"created",
],
)

22
CTFd/constants/users.py Normal file
View File

@ -0,0 +1,22 @@
from collections import namedtuple
UserAttrs = namedtuple(
"UserAttrs",
[
"id",
"oauth_id",
"name",
"email",
"type",
"secret",
"website",
"affiliation",
"country",
"bracket",
"hidden",
"banned",
"verified",
"team_id",
"created",
],
)

View File

@ -1,12 +1,10 @@
import datetime
import six
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import column_property, validates
from CTFd.cache import cache
from CTFd.utils.crypto import hash_password
from CTFd.utils.humanize.numbers import ordinalize
@ -142,6 +140,8 @@ class Awards(db.Model):
@hybrid_property
def account_id(self):
from CTFd.utils import get_config
user_mode = get_config("user_mode")
if user_mode == "teams":
return self.team_id
@ -259,6 +259,8 @@ class Users(db.Model):
@hybrid_property
def account_id(self):
from CTFd.utils import get_config
user_mode = get_config("user_mode")
if user_mode == "teams":
return self.team_id
@ -291,6 +293,8 @@ class Users(db.Model):
return None
def get_solves(self, admin=False):
from CTFd.utils import get_config
solves = Solves.query.filter_by(user_id=self.id)
freeze = get_config("freeze")
if freeze and admin is False:
@ -299,6 +303,8 @@ class Users(db.Model):
return solves.all()
def get_fails(self, admin=False):
from CTFd.utils import get_config
fails = Fails.query.filter_by(user_id=self.id)
freeze = get_config("freeze")
if freeze and admin is False:
@ -307,6 +313,8 @@ class Users(db.Model):
return fails.all()
def get_awards(self, admin=False):
from CTFd.utils import get_config
awards = Awards.query.filter_by(user_id=self.id)
freeze = get_config("freeze")
if freeze and admin is False:
@ -432,6 +440,8 @@ class Teams(db.Model):
return None
def get_solves(self, admin=False):
from CTFd.utils import get_config
member_ids = [member.id for member in self.members]
solves = Solves.query.filter(Solves.user_id.in_(member_ids)).order_by(
@ -446,6 +456,8 @@ class Teams(db.Model):
return solves.all()
def get_fails(self, admin=False):
from CTFd.utils import get_config
member_ids = [member.id for member in self.members]
fails = Fails.query.filter(Fails.user_id.in_(member_ids)).order_by(
@ -460,6 +472,8 @@ class Teams(db.Model):
return fails.all()
def get_awards(self, admin=False):
from CTFd.utils import get_config
member_ids = [member.id for member in self.members]
awards = Awards.query.filter(Awards.user_id.in_(member_ids)).order_by(
@ -523,6 +537,8 @@ class Submissions(db.Model):
@hybrid_property
def account_id(self):
from CTFd.utils import get_config
user_mode = get_config("user_mode")
if user_mode == "teams":
return self.team_id
@ -531,6 +547,8 @@ class Submissions(db.Model):
@hybrid_property
def account(self):
from CTFd.utils import get_config
user_mode = get_config("user_mode")
if user_mode == "teams":
return self.team
@ -600,6 +618,8 @@ class Unlocks(db.Model):
@hybrid_property
def account_id(self):
from CTFd.utils import get_config
user_mode = get_config("user_mode")
if user_mode == "teams":
return self.team_id
@ -668,22 +688,3 @@ class Tokens(db.Model):
class UserTokens(Tokens):
__mapper_args__ = {"polymorphic_identity": "user"}
@cache.memoize()
def get_config(key):
"""
This should be a direct clone of its implementation in utils. It is used to avoid a circular import.
"""
config = Configs.query.filter_by(key=key).first()
if config and config.value:
value = config.value
if value and value.isdigit():
return int(value)
elif value and isinstance(value, six.string_types):
if value.lower() == "true":
return True
elif value.lower() == "false":
return False
else:
return value

View File

@ -1,5 +1,6 @@
from flask import Blueprint, redirect, render_template, request, url_for
from CTFd.cache import clear_user_session, clear_team_session
from CTFd.models import Teams, db
from CTFd.utils import config, get_config
from CTFd.utils.crypto import verify_password
@ -63,7 +64,6 @@ def join():
passphrase = request.form.get("password", "").strip()
team = Teams.query.filter_by(name=teamname).first()
user = get_current_user()
if team and verify_password(passphrase, team.password):
team_size_limit = get_config("team_size", default=0)
@ -77,6 +77,7 @@ def join():
"teams/join_team.html", infos=infos, errors=errors
)
user = get_current_user()
user.team_id = team.id
db.session.commit()
@ -84,6 +85,9 @@ def join():
team.captain_id = user.id
db.session.commit()
clear_user_session(user_id=user.id)
clear_team_session(team_id=team.id)
return redirect(url_for("challenges.listing"))
else:
errors.append("That information is incorrect")
@ -130,6 +134,10 @@ def new():
user.team_id = team.id
db.session.commit()
clear_user_session(user_id=user.id)
clear_team_session(team_id=team.id)
return redirect(url_for("challenges.listing"))

View File

@ -66,3 +66,11 @@ tbody tr:hover {
tr[data-href] {
cursor: pointer;
}
.sort-col {
cursor: pointer;
}
input[type="checkbox"] {
cursor: pointer;
}

View File

@ -1 +1,4 @@
@import "~codemirror/lib/codemirror.css";
.CodeMirror {
font-size: 12px;
}

View File

@ -220,14 +220,4 @@ $(() => {
}
});
});
if (window.location.hash) {
let hash = window.location.hash.replace("<>[]'\"", "");
$('nav a[href="' + hash + '"]').tab("show");
}
$(".nav-tabs a").click(function(event) {
$(this).tab("show");
window.location.hash = this.hash;
});
});

View File

@ -407,8 +407,23 @@ $(() => {
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.success) {
.then(function(response) {
if (response.success) {
$(".challenge-state").text(response.data.state);
switch (response.data.state) {
case "visible":
$(".challenge-state")
.removeClass("badge-danger")
.addClass("badge-success");
break;
case "hidden":
$(".challenge-state")
.removeClass("badge-success")
.addClass("badge-danger");
break;
default:
break;
}
ezToast({
title: "Success",
body: "Your challenge has been updated!"
@ -432,16 +447,6 @@ $(() => {
$("#challenge-create-options form").submit(handleChallengeOptions);
$(".nav-tabs a").click(function(e) {
$(this).tab("show");
window.location.hash = this.hash;
});
if (window.location.hash) {
let hash = window.location.hash.replace("<>[]'\"", "");
$('nav a[href="' + hash + '"]').tab("show");
}
$("#tags-add-input").keyup(addTag);
$(".delete-tag").click(deleteTag);

View File

@ -0,0 +1,80 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { ezAlert, ezQuery } from "core/ezq";
function deleteSelectedChallenges(event) {
let challengeIDs = $("input[data-challenge-id]:checked").map(function() {
return $(this).data("challenge-id");
});
let target = challengeIDs.length === 1 ? "challenge" : "challenges";
ezQuery({
title: "Delete Challenges",
body: `Are you sure you want to delete ${challengeIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var chalID of challengeIDs) {
reqs.push(
CTFd.fetch(`/api/v1/challenges/${chalID}`, {
method: "DELETE"
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function bulkEditChallenges(event) {
let challengeIDs = $("input[data-challenge-id]:checked").map(function() {
return $(this).data("challenge-id");
});
ezAlert({
title: "Edit Challenges",
body: $(`
<form id="challenges-bulk-edit">
<div class="form-group">
<label>Category</label>
<input type="text" name="category" data-initial="" value="">
</div>
<div class="form-group">
<label>Value</label>
<input type="number" name="value" data-initial="" value="">
</div>
<div class="form-group">
<label>State</label>
<select name="state" data-initial="">
<option value="">--</option>
<option value="visible">Visible</option>
<option value="hidden">Hidden</option>
</select>
</div>
</form>
`),
button: "Submit",
success: function() {
let data = $("#challenges-bulk-edit").serializeJSON(true);
const reqs = [];
for (var chalID of challengeIDs) {
reqs.push(
CTFd.fetch(`/api/v1/challenges/${chalID}`, {
method: "PATCH",
body: JSON.stringify(data)
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
$(() => {
$("#challenges-delete-button").click(deleteSelectedChallenges);
$("#challenges-edit-button").click(bulkEditChallenges);
});

View File

@ -7,6 +7,8 @@ import CTFd from "core/CTFd";
import { default as helpers } from "core/helpers";
import $ from "jquery";
import { ezQuery, ezProgressBar } from "core/ezq";
import CodeMirror from "codemirror";
import "codemirror/mode/htmlmixed/htmlmixed.js";
function loadTimestamp(place, timestamp) {
if (typeof timestamp == "string") {
@ -218,10 +220,6 @@ function exportConfig(event) {
window.location.href = $(this).attr("href");
}
function showTab(event) {
window.location.hash = this.hash;
}
function insertTimezones(target) {
let current = $("<option>").text(moment.tz.guess());
$(target).append(current);
@ -233,6 +231,20 @@ function insertTimezones(target) {
}
$(() => {
CodeMirror.fromTextArea(document.getElementById("theme-header"), {
lineNumbers: true,
lineWrapping: true,
mode: "htmlmixed",
htmlMode: true
});
CodeMirror.fromTextArea(document.getElementById("theme-footer"), {
lineNumbers: true,
lineWrapping: true,
mode: "htmlmixed",
htmlMode: true
});
insertTimezones($("#start-timezone"));
insertTimezones($("#end-timezone"));
insertTimezones($("#freeze-timezone"));
@ -242,7 +254,6 @@ $(() => {
$("#remove-logo").click(removeLogo);
$("#export-button").click(exportConfig);
$("#import-button").click(importConfig);
$(".nav-pills a").click(showTab);
$("#config-color-update").click(function() {
const hex_code = $("#config-color-picker").val();
const user_css = $("#theme-header").val();
@ -271,12 +282,6 @@ $(() => {
loadDateValues("freeze");
});
let hash = window.location.hash;
if (hash) {
hash = hash.replace("<>[]'\"", "");
$('ul.nav a[href="' + hash + '"]').tab("show");
}
const start = $("#start").val();
const end = $("#end").val();
const freeze = $("#freeze").val();

View File

@ -4,6 +4,7 @@ import $ from "jquery";
import CTFd from "core/CTFd";
import { default as helpers } from "core/helpers";
import CodeMirror from "codemirror";
import "codemirror/mode/htmlmixed/htmlmixed.js";
import { ezQuery, ezToast } from "core/ezq";
function get_filetype_icon_class(filename) {
@ -206,7 +207,7 @@ $(() => {
{
lineNumbers: true,
lineWrapping: true,
mode: "xml",
mode: "htmlmixed",
htmlMode: true
}
);

View File

@ -1,37 +1,33 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { htmlEntities } from "core/utils";
import { ezQuery } from "core/ezq";
function deletePage(event) {
const elem = $(this);
const name = elem.attr("page-route");
const page_id = elem.attr("page-id");
function deleteSelectedUsers(event) {
let pageIDs = $("input[data-page-id]:checked").map(function() {
return $(this).data("page-id");
});
let target = pageIDs.length === 1 ? "page" : "pages";
ezQuery({
title: "Delete " + htmlEntities(name),
body: "Are you sure you want to delete {0}?".format(
"<strong>" + htmlEntities(name) + "</strong>"
),
title: "Delete Pages",
body: `Are you sure you want to delete ${pageIDs.length} ${target}?`,
success: function() {
CTFd.fetch("/api/v1/pages/" + page_id, {
method: "DELETE"
})
.then(function(response) {
return response.json();
})
.then(function(response) {
if (response.success) {
elem
.parent()
.parent()
.remove();
}
});
const reqs = [];
for (var pageID of pageIDs) {
reqs.push(
CTFd.fetch(`/api/v1/pages/${pageID}`, {
method: "DELETE"
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
$(() => {
$(".delete-page").click(deletePage);
$("#pages-delete-button").click(deleteSelectedUsers);
});

View File

@ -1,6 +1,7 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { ezAlert, ezQuery } from "core/ezq";
const api_func = {
users: (x, y) => CTFd.api.patch_user_public({ userId: x }, y),
@ -37,6 +38,48 @@ function toggleAccount() {
});
}
function toggleSelectedAccounts(accountIDs, action) {
const params = {
hidden: action === "hidden" ? true : false
};
const reqs = [];
for (var accId of accountIDs) {
reqs.push(api_func[CTFd.config.userMode](accId, params));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
function bulkToggleAccounts(event) {
let accountIDs = $("input[data-account-id]:checked").map(function() {
return $(this).data("account-id");
});
ezAlert({
title: "Toggle Visibility",
body: $(`
<form id="scoreboard-bulk-edit">
<div class="form-group">
<label>Visibility</label>
<select name="visibility" data-initial="">
<option value="">--</option>
<option value="visible">Visible</option>
<option value="hidden">Hidden</option>
</select>
</div>
</form>
`),
button: "Submit",
success: function() {
let data = $("#scoreboard-bulk-edit").serializeJSON(true);
let state = data.visibility;
toggleSelectedAccounts(accountIDs, state);
}
});
}
$(() => {
$(".scoreboard-toggle").click(toggleAccount);
$("#scoreboard-edit-button").click(bulkToggleAccounts);
});

View File

@ -177,6 +177,59 @@ const graph_configs = {
annotations
];
}
},
"#score-distribution-graph": {
layout: annotations => ({
title: "Score Distribution",
xaxis: {
title: "Score Bracket",
showticklabels: true,
type: "category"
},
yaxis: {
title: "Number of {0}".format(
CTFd.config.userMode.charAt(0).toUpperCase() +
CTFd.config.userMode.slice(1)
)
},
annotations: annotations
}),
data: () =>
CTFd.fetch("/api/v1/statistics/scores/distribution").then(function(
response
) {
return response.json();
}),
fn: () =>
"CTFd_score_distribution_" + new Date().toISOString().slice(0, 19),
format: response => {
const data = response.data.brackets;
const keys = [];
const brackets = [];
const sizes = [];
for (let key in data) {
keys.push(parseInt(key));
}
keys.sort((a, b) => a - b);
let start = "<0";
keys.map(key => {
brackets.push("{0} - {1}".format(start, key));
sizes.push(data[key]);
start = key;
});
return [
{
type: "bar",
x: brackets,
y: sizes,
orientation: "v"
}
];
}
}
};

View File

@ -40,6 +40,28 @@ function deleteCorrectSubmission(event) {
});
}
function deleteSelectedSubmissions(event) {
let submissionIDs = $("input[data-submission-id]:checked").map(function() {
return $(this).data("submission-id");
});
let target = submissionIDs.length === 1 ? "submission" : "submissions";
ezQuery({
title: "Delete Submissions",
body: `Are you sure you want to delete ${submissionIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var subId of submissionIDs) {
reqs.push(CTFd.api.delete_submission({ submissionId: subId }));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
$(() => {
$(".delete-correct-submission").click(deleteCorrectSubmission);
$("#submission-delete-button").click(deleteSelectedSubmissions);
});

View File

@ -2,7 +2,7 @@ import "./main";
import $ from "jquery";
import CTFd from "core/CTFd";
import { htmlEntities } from "core/utils";
import { ezQuery, ezBadge } from "core/ezq";
import { ezAlert, ezQuery, ezBadge } from "core/ezq";
import { createGraph, updateGraph } from "core/graphs";
function createTeam(event) {
@ -80,6 +80,132 @@ function updateTeam(event) {
});
}
function deleteSelectedSubmissions(event, target) {
let submissions;
let type;
let title;
switch (target) {
case "solves":
submissions = $("input[data-submission-type=correct]:checked");
type = "solve";
title = "Solves";
break;
case "fails":
submissions = $("input[data-submission-type=incorrect]:checked");
type = "fail";
title = "Fails";
break;
default:
break;
}
let submissionIDs = submissions.map(function() {
return $(this).data("submission-id");
});
let target_string = submissionIDs.length === 1 ? type : type + "s";
ezQuery({
title: `Delete ${title}`,
body: `Are you sure you want to delete ${
submissionIDs.length
} ${target_string}?`,
success: function() {
const reqs = [];
for (var subId of submissionIDs) {
reqs.push(CTFd.api.delete_submission({ submissionId: subId }));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function deleteSelectedAwards(event) {
let awardIDs = $("input[data-award-id]:checked").map(function() {
return $(this).data("award-id");
});
let target = awardIDs.length === 1 ? "award" : "awards";
ezQuery({
title: `Delete Awards`,
body: `Are you sure you want to delete ${awardIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var awardID of awardIDs) {
let req = CTFd.fetch("/api/v1/awards/" + awardID, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function solveSelectedMissingChallenges(event) {
event.preventDefault();
let challengeIDs = $("input[data-missing-challenge-id]:checked").map(
function() {
return $(this).data("missing-challenge-id");
}
);
let target = challengeIDs.length === 1 ? "challenge" : "challenges";
ezQuery({
title: `Mark Correct`,
body: `Are you sure you want to mark ${
challengeIDs.length
} challenges correct for ${htmlEntities(TEAM_NAME)}?`,
success: function() {
ezAlert({
title: `User Attribution`,
body: `
Which user on ${htmlEntities(TEAM_NAME)} solved these challenges?
<div class="pb-3" id="query-team-member-solve">
${$("#team-member-select").html()}
</div>
`,
button: "Mark Correct",
success: function() {
const USER_ID = $("#query-team-member-solve > select").val();
const reqs = [];
for (var challengeID of challengeIDs) {
let params = {
provided: "MARKED AS SOLVED BY ADMIN",
user_id: USER_ID,
team_id: TEAM_ID,
challenge_id: challengeID,
type: "correct"
};
let req = CTFd.fetch("/api/v1/submissions", {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
});
}
const api_funcs = {
team: [
x => CTFd.api.get_team_solves({ teamId: x }),
@ -221,6 +347,10 @@ $(() => {
$("#team-award-modal").modal("toggle");
});
$(".addresses-team").click(function(event) {
$("#team-addresses-modal").modal("toggle");
});
$("#user-award-form").submit(function(e) {
e.preventDefault();
const params = $("#user-award-form").serializeJSON(true);
@ -331,82 +461,20 @@ $(() => {
});
});
$(".delete-submission").click(function(e) {
e.preventDefault();
const submission_id = $(this).attr("submission-id");
const submission_type = $(this).attr("submission-type");
const submission_challenge = $(this).attr("submission-challenge");
const 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(TEAM_NAME),
htmlEntities(submission_challenge)
);
const row = $(this)
.parent()
.parent();
ezQuery({
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();
}
});
}
});
$("#solves-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "solves");
});
$(".delete-award").click(function(e) {
e.preventDefault();
const award_id = $(this).attr("award-id");
const award_name = $(this).attr("award-name");
$("#fails-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "fails");
});
const body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
htmlEntities(award_name),
htmlEntities(TEAM_NAME)
);
$("#awards-delete-button").click(function(e) {
deleteSelectedAwards(e);
});
const row = $(this)
.parent()
.parent();
ezQuery({
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();
}
});
}
});
$("#missing-solve-button").click(function(e) {
solveSelectedMissingChallenges(e);
});
$("#team-info-create-form").submit(createTeam);

View File

@ -0,0 +1,80 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { ezAlert, ezQuery } from "core/ezq";
function deleteSelectedTeams(event) {
let teamIDs = $("input[data-team-id]:checked").map(function() {
return $(this).data("team-id");
});
let target = teamIDs.length === 1 ? "team" : "teams";
ezQuery({
title: "Delete Teams",
body: `Are you sure you want to delete ${teamIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var teamID of teamIDs) {
reqs.push(
CTFd.fetch(`/api/v1/teams/${teamID}`, {
method: "DELETE"
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function bulkEditTeams(event) {
let teamIDs = $("input[data-team-id]:checked").map(function() {
return $(this).data("team-id");
});
ezAlert({
title: "Edit Teams",
body: $(`
<form id="teams-bulk-edit">
<div class="form-group">
<label>Banned</label>
<select name="banned" data-initial="">
<option value="">--</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
<div class="form-group">
<label>Hidden</label>
<select name="hidden" data-initial="">
<option value="">--</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</form>
`),
button: "Submit",
success: function() {
let data = $("#teams-bulk-edit").serializeJSON(true);
const reqs = [];
for (var teamID of teamIDs) {
reqs.push(
CTFd.fetch(`/api/v1/teams/${teamID}`, {
method: "PATCH",
body: JSON.stringify(data)
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
$(() => {
$("#teams-delete-button").click(deleteSelectedTeams);
$("#teams-edit-button").click(bulkEditTeams);
});

View File

@ -189,128 +189,115 @@ function emailUser(event) {
});
}
function deleteUserSubmission(event) {
event.preventDefault();
const submission_id = $(this).attr("submission-id");
const submission_type = $(this).attr("submission-type");
const submission_challenge = $(this).attr("submission-challenge");
function deleteSelectedSubmissions(event, target) {
let submissions;
let type;
let title;
switch (target) {
case "solves":
submissions = $("input[data-submission-type=correct]:checked");
type = "solve";
title = "Solves";
break;
case "fails":
submissions = $("input[data-submission-type=incorrect]:checked");
type = "fail";
title = "Fails";
break;
default:
break;
}
const 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)
);
const row = $(this)
.parent()
.parent();
let submissionIDs = submissions.map(function() {
return $(this).data("submission-id");
});
let target_string = submissionIDs.length === 1 ? type : type + "s";
ezQuery({
title: "Delete Submission",
body: body,
title: `Delete ${title}`,
body: `Are you sure you want to delete ${
submissionIDs.length
} ${target_string}?`,
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();
}
});
const reqs = [];
for (var subId of submissionIDs) {
reqs.push(CTFd.api.delete_submission({ submissionId: subId }));
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function deleteUserAward(event) {
event.preventDefault();
const award_id = $(this).attr("award-id");
const award_name = $(this).attr("award-name");
const 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)
);
const row = $(this)
.parent()
.parent();
function deleteSelectedAwards(event) {
let awardIDs = $("input[data-award-id]:checked").map(function() {
return $(this).data("award-id");
});
let target = awardIDs.length === 1 ? "award" : "awards";
ezQuery({
title: "Delete Award",
body: body,
title: `Delete Awards`,
body: `Are you sure you want to delete ${awardIDs.length} ${target}?`,
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();
const reqs = [];
for (var awardID of awardIDs) {
let req = CTFd.fetch("/api/v1/awards/" + awardID, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
}
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function correctUserSubmission(event) {
function solveSelectedMissingChallenges(event) {
event.preventDefault();
const challenge_id = $(this).attr("challenge-id");
const challenge_name = $(this).attr("challenge-name");
const row = $(this)
.parent()
.parent();
const 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)
let challengeIDs = $("input[data-missing-challenge-id]:checked").map(
function() {
return $(this).data("missing-challenge-id");
}
);
const params = {
provided: "MARKED AS SOLVED BY ADMIN",
user_id: USER_ID,
team_id: TEAM_ID,
challenge_id: challenge_id,
type: "correct"
};
let target = challengeIDs.length === 1 ? "challenge" : "challenges";
ezQuery({
title: "Mark Correct",
body: body,
title: `Mark Correct`,
body: `Are you sure you want to mark ${
challengeIDs.length
} correct for ${htmlEntities(USER_NAME)}?`,
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();
}
const reqs = [];
for (var challengeID of challengeIDs) {
let params = {
provided: "MARKED AS SOLVED BY ADMIN",
user_id: USER_ID,
team_id: TEAM_ID,
challenge_id: challengeID,
type: "correct"
};
let req = CTFd.fetch("/api/v1/submissions", {
method: "POST",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
});
reqs.push(req);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
@ -419,11 +406,27 @@ $(() => {
$("#user-email-modal").modal("toggle");
});
$(".addresses-user").click(function(event) {
$("#user-addresses-modal").modal("toggle");
});
$("#user-mail-form").submit(emailUser);
$(".delete-submission").click(deleteUserSubmission);
$(".delete-award").click(deleteUserAward);
$(".correct-submission").click(correctUserSubmission);
$("#solves-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "solves");
});
$("#fails-delete-button").click(function(e) {
deleteSelectedSubmissions(e, "fails");
});
$("#awards-delete-button").click(function(e) {
deleteSelectedAwards(e);
});
$("#missing-solve-button").click(function(e) {
solveSelectedMissingChallenges(e);
});
$("#user-info-create-form").submit(createUser);

View File

@ -1 +1,88 @@
import "./main";
import CTFd from "core/CTFd";
import $ from "jquery";
import { ezAlert, ezQuery } from "core/ezq";
function deleteSelectedUsers(event) {
let userIDs = $("input[data-user-id]:checked").map(function() {
return $(this).data("user-id");
});
let target = userIDs.length === 1 ? "user" : "users";
ezQuery({
title: "Delete Users",
body: `Are you sure you want to delete ${userIDs.length} ${target}?`,
success: function() {
const reqs = [];
for (var userID of userIDs) {
reqs.push(
CTFd.fetch(`/api/v1/users/${userID}`, {
method: "DELETE"
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
function bulkEditUsers(event) {
let userIDs = $("input[data-user-id]:checked").map(function() {
return $(this).data("user-id");
});
ezAlert({
title: "Edit Users",
body: $(`
<form id="users-bulk-edit">
<div class="form-group">
<label>Verified</label>
<select name="verified" data-initial="">
<option value="">--</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
<div class="form-group">
<label>Banned</label>
<select name="banned" data-initial="">
<option value="">--</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
<div class="form-group">
<label>Hidden</label>
<select name="hidden" data-initial="">
<option value="">--</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
</div>
</form>
`),
button: "Submit",
success: function() {
let data = $("#users-bulk-edit").serializeJSON(true);
const reqs = [];
for (var userID of userIDs) {
reqs.push(
CTFd.fetch(`/api/v1/users/${userID}`, {
method: "PATCH",
body: JSON.stringify(data)
})
);
}
Promise.all(reqs).then(responses => {
window.location.reload();
});
}
});
}
$(() => {
$("#users-delete-button").click(deleteSelectedUsers);
$("#users-edit-button").click(bulkEditUsers);
});

View File

@ -1,4 +1,5 @@
import "bootstrap/dist/js/bootstrap.bundle";
import { makeSortableTables } from "core/utils";
import $ from "jquery";
export default () => {
@ -45,6 +46,27 @@ export default () => {
return false;
});
$("[data-checkbox]").click(function(e) {
if ($(e.target).is("input[type=checkbox]")) {
e.stopImmediatePropagation();
return;
}
let checkbox = $(this).find("input[type=checkbox]");
// Doing it this way with an event allows data-checkbox-all to work
checkbox.click();
e.stopImmediatePropagation();
});
$("[data-checkbox-all]").on("click change", function(e) {
const checked = $(this).prop("checked");
const idx = $(this).index() + 1;
$(this)
.closest("table")
.find(`tr td:nth-child(${idx}) input[type=checkbox]`)
.prop("checked", checked);
e.stopImmediatePropagation();
});
$("tr[data-href] a, tr[data-href] button").click(function(e) {
// TODO: This is a hack to allow modal close buttons to work
if (!$(this).attr("data-dismiss")) {
@ -52,6 +74,29 @@ export default () => {
}
});
$(".page-select").change(function() {
let url = new URL(window.location);
url.searchParams.set("page", this.value);
window.location.href = url.toString();
});
$('a[data-toggle="tab"]').on("shown.bs.tab", function(e) {
sessionStorage.setItem("activeTab", $(e.target).attr("href"));
});
let activeTab = sessionStorage.getItem("activeTab");
if (activeTab) {
let target = $(
`.nav-tabs a[href="${activeTab}"], .nav-pills a[href="${activeTab}"]`
);
if (target.length) {
target.tab("show");
} else {
sessionStorage.removeItem("activeTab");
}
}
makeSortableTables();
$('[data-toggle="tooltip"]').tooltip();
});
};

View File

@ -1,4 +1,4 @@
html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal !important;z-index:-20}
#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{height:400px;display:block}#categories-pie-graph{height:400px;display:block}#solve-percentages-graph{height:400px;display:block}.no-decoration{color:inherit !important;text-decoration:none !important}.no-decoration:hover{color:inherit !important;text-decoration:none !important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,0.1) !important}tr[data-href]{cursor:pointer}
#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{height:400px;display:block}#categories-pie-graph{height:400px;display:block}#solve-percentages-graph{height:400px;display:block}.no-decoration{color:inherit !important;text-decoration:none !important}.no-decoration:hover{color:inherit !important;text-decoration:none !important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:"Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,0.1) !important}tr[data-href]{cursor:pointer}.sort-col{cursor:pointer}input[type="checkbox"]{cursor:pointer}

View File

@ -1 +1 @@
html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal!important;z-index:-20}#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#categories-pie-graph,#keys-pie-graph,#solve-percentages-graph{height:400px;display:block}.no-decoration,.no-decoration:hover{color:inherit!important;text-decoration:none!important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:Avenir Next,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,.1)!important}tr[data-href]{cursor:pointer}
html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:1px;width:100%;height:60px;line-height:normal!important;z-index:-20}#score-graph{height:450px;display:block;clear:both}#solves-graph{display:block;height:350px}#categories-pie-graph,#keys-pie-graph,#solve-percentages-graph{height:400px;display:block}.no-decoration,.no-decoration:hover{color:inherit!important;text-decoration:none!important}.table td,.table th{vertical-align:inherit}pre{white-space:pre-wrap;margin:0;padding:0}.form-control{position:relative;display:block;border-radius:0;font-weight:400;font-family:Avenir Next,Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-appearance:none}tbody tr:hover{background-color:rgba(0,0,0,.1)!important}.sort-col,input[type=checkbox],tr[data-href]{cursor:pointer}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,169 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var executeModules = data[2];
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
/******/ var depId = deferredModule[j];
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/ return result;
/******/ }
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "pages/challenges": 0
/******/ };
/******/
/******/ var deferredModules = [];
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/themes/admin/static/js";
/******/
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/challenges.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ({
/***/ "./CTFd/themes/admin/assets/js/pages/challenges.js":
/*!*********************************************************!*\
!*** ./CTFd/themes/admin/assets/js/pages/challenges.js ***!
\*********************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteSelectedChallenges(event) {\n var challengeIDs = (0, _jquery.default)(\"input[data-challenge-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"challenge-id\");\n });\n var target = challengeIDs.length === 1 ? \"challenge\" : \"challenges\";\n (0, _ezq.ezQuery)({\n title: \"Delete Challenges\",\n body: \"Are you sure you want to delete \".concat(challengeIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = challengeIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var chalID = _step.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/challenges/\".concat(chalID), {\n method: \"DELETE\"\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction bulkEditChallenges(event) {\n var challengeIDs = (0, _jquery.default)(\"input[data-challenge-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"challenge-id\");\n });\n (0, _ezq.ezAlert)({\n title: \"Edit Challenges\",\n body: (0, _jquery.default)(\"\\n <form id=\\\"challenges-bulk-edit\\\">\\n <div class=\\\"form-group\\\">\\n <label>Category</label>\\n <input type=\\\"text\\\" name=\\\"category\\\" data-initial=\\\"\\\" value=\\\"\\\">\\n </div>\\n <div class=\\\"form-group\\\">\\n <label>Value</label>\\n <input type=\\\"number\\\" name=\\\"value\\\" data-initial=\\\"\\\" value=\\\"\\\">\\n </div>\\n <div class=\\\"form-group\\\">\\n <label>State</label>\\n <select name=\\\"state\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"visible\\\">Visible</option>\\n <option value=\\\"hidden\\\">Hidden</option>\\n </select>\\n </div>\\n </form>\\n \"),\n button: \"Submit\",\n success: function success() {\n var data = (0, _jquery.default)(\"#challenges-bulk-edit\").serializeJSON(true);\n var reqs = [];\n var _iteratorNormalCompletion2 = true;\n var _didIteratorError2 = false;\n var _iteratorError2 = undefined;\n\n try {\n for (var _iterator2 = challengeIDs[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {\n var chalID = _step2.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/challenges/\".concat(chalID), {\n method: \"PATCH\",\n body: JSON.stringify(data)\n }));\n }\n } catch (err) {\n _didIteratorError2 = true;\n _iteratorError2 = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion2 && _iterator2.return != null) {\n _iterator2.return();\n }\n } finally {\n if (_didIteratorError2) {\n throw _iteratorError2;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#challenges-delete-button\").click(deleteSelectedChallenges);\n (0, _jquery.default)(\"#challenges-edit-button\").click(bulkEditChallenges);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/challenges.js?");
/***/ })
/******/ });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/main.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/main.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/notifications.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/notifications.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/pages.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/pages.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deletePage(event) {\n var elem = (0, _jquery.default)(this);\n var name = elem.attr(\"page-route\");\n var page_id = elem.attr(\"page-id\");\n (0, _ezq.ezQuery)({\n title: \"Delete \" + (0, _utils.htmlEntities)(name),\n body: \"Are you sure you want to delete {0}?\".format(\"<strong>\" + (0, _utils.htmlEntities)(name) + \"</strong>\"),\n success: function success() {\n _CTFd.default.fetch(\"/api/v1/pages/\" + page_id, {\n method: \"DELETE\"\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n elem.parent().parent().remove();\n }\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".delete-page\").click(deletePage);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/pages.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteSelectedUsers(event) {\n var pageIDs = (0, _jquery.default)(\"input[data-page-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"page-id\");\n });\n var target = pageIDs.length === 1 ? \"page\" : \"pages\";\n (0, _ezq.ezQuery)({\n title: \"Delete Pages\",\n body: \"Are you sure you want to delete \".concat(pageIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = pageIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var pageID = _step.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/pages/\".concat(pageID), {\n method: \"DELETE\"\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#pages-delete-button\").click(deleteSelectedUsers);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/pages.js?");
/***/ })

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/reset.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/reset.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/scoreboard.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/scoreboard.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar api_func = {\n users: function users(x, y) {\n return _CTFd.default.api.patch_user_public({\n userId: x\n }, y);\n },\n teams: function teams(x, y) {\n return _CTFd.default.api.patch_team_public({\n teamId: x\n }, y);\n }\n};\n\nfunction toggleAccount() {\n var $btn = (0, _jquery.default)(this);\n var id = $btn.data(\"account-id\");\n var state = $btn.data(\"state\");\n var hidden = undefined;\n\n if (state === \"visible\") {\n hidden = true;\n } else if (state === \"hidden\") {\n hidden = false;\n }\n\n var params = {\n hidden: hidden\n };\n\n api_func[_CTFd.default.config.userMode](id, params).then(function (response) {\n if (response.success) {\n if (hidden) {\n $btn.data(\"state\", \"hidden\");\n $btn.addClass(\"btn-danger\").removeClass(\"btn-success\");\n $btn.text(\"Hidden\");\n } else {\n $btn.data(\"state\", \"visible\");\n $btn.addClass(\"btn-success\").removeClass(\"btn-danger\");\n $btn.text(\"Visible\");\n }\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".scoreboard-toggle\").click(toggleAccount);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/scoreboard.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar api_func = {\n users: function users(x, y) {\n return _CTFd.default.api.patch_user_public({\n userId: x\n }, y);\n },\n teams: function teams(x, y) {\n return _CTFd.default.api.patch_team_public({\n teamId: x\n }, y);\n }\n};\n\nfunction toggleAccount() {\n var $btn = (0, _jquery.default)(this);\n var id = $btn.data(\"account-id\");\n var state = $btn.data(\"state\");\n var hidden = undefined;\n\n if (state === \"visible\") {\n hidden = true;\n } else if (state === \"hidden\") {\n hidden = false;\n }\n\n var params = {\n hidden: hidden\n };\n\n api_func[_CTFd.default.config.userMode](id, params).then(function (response) {\n if (response.success) {\n if (hidden) {\n $btn.data(\"state\", \"hidden\");\n $btn.addClass(\"btn-danger\").removeClass(\"btn-success\");\n $btn.text(\"Hidden\");\n } else {\n $btn.data(\"state\", \"visible\");\n $btn.addClass(\"btn-success\").removeClass(\"btn-danger\");\n $btn.text(\"Visible\");\n }\n }\n });\n}\n\nfunction toggleSelectedAccounts(accountIDs, action) {\n var params = {\n hidden: action === \"hidden\" ? true : false\n };\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = accountIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var accId = _step.value;\n reqs.push(api_func[_CTFd.default.config.userMode](accId, params));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n}\n\nfunction bulkToggleAccounts(event) {\n var accountIDs = (0, _jquery.default)(\"input[data-account-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"account-id\");\n });\n (0, _ezq.ezAlert)({\n title: \"Toggle Visibility\",\n body: (0, _jquery.default)(\"\\n <form id=\\\"scoreboard-bulk-edit\\\">\\n <div class=\\\"form-group\\\">\\n <label>Visibility</label>\\n <select name=\\\"visibility\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"visible\\\">Visible</option>\\n <option value=\\\"hidden\\\">Hidden</option>\\n </select>\\n </div>\\n </form>\\n \"),\n button: \"Submit\",\n success: function success() {\n var data = (0, _jquery.default)(\"#scoreboard-bulk-edit\").serializeJSON(true);\n var state = data.visibility;\n toggleSelectedAccounts(accountIDs, state);\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".scoreboard-toggle\").click(toggleAccount);\n (0, _jquery.default)(\"#scoreboard-edit-button\").click(bulkToggleAccounts);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/scoreboard.js?");
/***/ })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/submissions.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/submissions.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteCorrectSubmission(event) {\n var key_id = (0, _jquery.default)(this).data(\"submission-id\");\n var $elem = (0, _jquery.default)(this).parent().parent();\n var chal_name = $elem.find(\".chal\").text().trim();\n var team_name = $elem.find(\".team\").text().trim();\n var row = (0, _jquery.default)(this).parent().parent();\n (0, _ezq.ezQuery)({\n title: \"Delete Submission\",\n body: \"Are you sure you want to delete correct submission from {0} for challenge {1}\".format(\"<strong>\" + (0, _utils.htmlEntities)(team_name) + \"</strong>\", \"<strong>\" + (0, _utils.htmlEntities)(chal_name) + \"</strong>\"),\n success: function success() {\n _CTFd.default.api.delete_submission({\n submissionId: key_id\n }).then(function (response) {\n if (response.success) {\n row.remove();\n }\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".delete-correct-submission\").click(deleteCorrectSubmission);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/submissions.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _utils = __webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteCorrectSubmission(event) {\n var key_id = (0, _jquery.default)(this).data(\"submission-id\");\n var $elem = (0, _jquery.default)(this).parent().parent();\n var chal_name = $elem.find(\".chal\").text().trim();\n var team_name = $elem.find(\".team\").text().trim();\n var row = (0, _jquery.default)(this).parent().parent();\n (0, _ezq.ezQuery)({\n title: \"Delete Submission\",\n body: \"Are you sure you want to delete correct submission from {0} for challenge {1}\".format(\"<strong>\" + (0, _utils.htmlEntities)(team_name) + \"</strong>\", \"<strong>\" + (0, _utils.htmlEntities)(chal_name) + \"</strong>\"),\n success: function success() {\n _CTFd.default.api.delete_submission({\n submissionId: key_id\n }).then(function (response) {\n if (response.success) {\n row.remove();\n }\n });\n }\n });\n}\n\nfunction deleteSelectedSubmissions(event) {\n var submissionIDs = (0, _jquery.default)(\"input[data-submission-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"submission-id\");\n });\n var target = submissionIDs.length === 1 ? \"submission\" : \"submissions\";\n (0, _ezq.ezQuery)({\n title: \"Delete Submissions\",\n body: \"Are you sure you want to delete \".concat(submissionIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = submissionIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var subId = _step.value;\n reqs.push(_CTFd.default.api.delete_submission({\n submissionId: subId\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\".delete-correct-submission\").click(deleteCorrectSubmission);\n (0, _jquery.default)(\"#submission-delete-button\").click(deleteSelectedSubmissions);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/submissions.js?");
/***/ })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,66 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/ var executeModules = data[2];
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/
/******/ // add entry modules from loaded chunk to deferred list
/******/ deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ // run deferred modules when all chunks ready
/******/ return checkDeferredModules();
/******/ };
/******/ function checkDeferredModules() {
/******/ var result;
/******/ for(var i = 0; i < deferredModules.length; i++) {
/******/ var deferredModule = deferredModules[i];
/******/ var fulfilled = true;
/******/ for(var j = 1; j < deferredModule.length; j++) {
/******/ var depId = deferredModule[j];
/******/ if(installedChunks[depId] !== 0) fulfilled = false;
/******/ }
/******/ if(fulfilled) {
/******/ deferredModules.splice(i--, 1);
/******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ }
/******/ }
/******/ return result;
/******/ }
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // Promise = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "pages/teams": 0
/******/ };
/******/
/******/ var deferredModules = [];
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
@ -79,9 +138,18 @@
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/themes/admin/static/js";
/******/
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
/******/ jsonpArray = jsonpArray.slice();
/******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/ var parentJsonpFunction = oldJsonpFunction;
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./CTFd/themes/admin/assets/js/pages/teams.js");
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/teams.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ({
@ -94,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/teams.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteSelectedTeams(event) {\n var teamIDs = (0, _jquery.default)(\"input[data-team-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"team-id\");\n });\n var target = teamIDs.length === 1 ? \"team\" : \"teams\";\n (0, _ezq.ezQuery)({\n title: \"Delete Teams\",\n body: \"Are you sure you want to delete \".concat(teamIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = teamIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var teamID = _step.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/teams/\".concat(teamID), {\n method: \"DELETE\"\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction bulkEditTeams(event) {\n var teamIDs = (0, _jquery.default)(\"input[data-team-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"team-id\");\n });\n (0, _ezq.ezAlert)({\n title: \"Edit Teams\",\n body: (0, _jquery.default)(\"\\n <form id=\\\"teams-bulk-edit\\\">\\n <div class=\\\"form-group\\\">\\n <label>Banned</label>\\n <select name=\\\"banned\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"true\\\">True</option>\\n <option value=\\\"false\\\">False</option>\\n </select>\\n </div>\\n <div class=\\\"form-group\\\">\\n <label>Hidden</label>\\n <select name=\\\"hidden\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"true\\\">True</option>\\n <option value=\\\"false\\\">False</option>\\n </select>\\n </div>\\n </form>\\n \"),\n button: \"Submit\",\n success: function success() {\n var data = (0, _jquery.default)(\"#teams-bulk-edit\").serializeJSON(true);\n var reqs = [];\n var _iteratorNormalCompletion2 = true;\n var _didIteratorError2 = false;\n var _iteratorError2 = undefined;\n\n try {\n for (var _iterator2 = teamIDs[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {\n var teamID = _step2.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/teams/\".concat(teamID), {\n method: \"PATCH\",\n body: JSON.stringify(data)\n }));\n }\n } catch (err) {\n _didIteratorError2 = true;\n _iteratorError2 = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion2 && _iterator2.return != null) {\n _iterator2.return();\n }\n } finally {\n if (_didIteratorError2) {\n throw _iteratorError2;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#teams-delete-button\").click(deleteSelectedTeams);\n (0, _jquery.default)(\"#teams-edit-button\").click(bulkEditTeams);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/teams.js?");
/***/ })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -147,7 +147,7 @@
/******/
/******/
/******/ // add entry module to deferred list
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/users.js","helpers","vendor","default~pages/challenge~pages/configs~pages/editor~pages/main~pages/notifications~pages/pages~pages/~0fc9fcae"]);
/******/ deferredModules.push(["./CTFd/themes/admin/assets/js/pages/users.js","helpers","vendor","default~pages/challenge~pages/challenges~pages/configs~pages/editor~pages/main~pages/notifications~p~d5a3cc0a"]);
/******/ // run deferred modules when ready
/******/ return checkDeferredModules();
/******/ })
@ -162,7 +162,7 @@
/***/ (function(module, exports, __webpack_require__) {
;
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/users.js?");
eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction deleteSelectedUsers(event) {\n var userIDs = (0, _jquery.default)(\"input[data-user-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"user-id\");\n });\n var target = userIDs.length === 1 ? \"user\" : \"users\";\n (0, _ezq.ezQuery)({\n title: \"Delete Users\",\n body: \"Are you sure you want to delete \".concat(userIDs.length, \" \").concat(target, \"?\"),\n success: function success() {\n var reqs = [];\n var _iteratorNormalCompletion = true;\n var _didIteratorError = false;\n var _iteratorError = undefined;\n\n try {\n for (var _iterator = userIDs[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {\n var userID = _step.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/users/\".concat(userID), {\n method: \"DELETE\"\n }));\n }\n } catch (err) {\n _didIteratorError = true;\n _iteratorError = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n } finally {\n if (_didIteratorError) {\n throw _iteratorError;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction bulkEditUsers(event) {\n var userIDs = (0, _jquery.default)(\"input[data-user-id]:checked\").map(function () {\n return (0, _jquery.default)(this).data(\"user-id\");\n });\n (0, _ezq.ezAlert)({\n title: \"Edit Users\",\n body: (0, _jquery.default)(\"\\n <form id=\\\"users-bulk-edit\\\">\\n <div class=\\\"form-group\\\">\\n <label>Verified</label>\\n <select name=\\\"verified\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"true\\\">True</option>\\n <option value=\\\"false\\\">False</option>\\n </select>\\n </div>\\n <div class=\\\"form-group\\\">\\n <label>Banned</label>\\n <select name=\\\"banned\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"true\\\">True</option>\\n <option value=\\\"false\\\">False</option>\\n </select>\\n </div>\\n <div class=\\\"form-group\\\">\\n <label>Hidden</label>\\n <select name=\\\"hidden\\\" data-initial=\\\"\\\">\\n <option value=\\\"\\\">--</option>\\n <option value=\\\"true\\\">True</option>\\n <option value=\\\"false\\\">False</option>\\n </select>\\n </div>\\n </form>\\n \"),\n button: \"Submit\",\n success: function success() {\n var data = (0, _jquery.default)(\"#users-bulk-edit\").serializeJSON(true);\n var reqs = [];\n var _iteratorNormalCompletion2 = true;\n var _didIteratorError2 = false;\n var _iteratorError2 = undefined;\n\n try {\n for (var _iterator2 = userIDs[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {\n var userID = _step2.value;\n reqs.push(_CTFd.default.fetch(\"/api/v1/users/\".concat(userID), {\n method: \"PATCH\",\n body: JSON.stringify(data)\n }));\n }\n } catch (err) {\n _didIteratorError2 = true;\n _iteratorError2 = err;\n } finally {\n try {\n if (!_iteratorNormalCompletion2 && _iterator2.return != null) {\n _iterator2.return();\n }\n } finally {\n if (_didIteratorError2) {\n throw _iteratorError2;\n }\n }\n }\n\n Promise.all(reqs).then(function (responses) {\n window.location.reload();\n });\n }\n });\n}\n\n(0, _jquery.default)(function () {\n (0, _jquery.default)(\"#users-delete-button\").click(deleteSelectedUsers);\n (0, _jquery.default)(\"#users-edit-button\").click(bulkEditUsers);\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/users.js?");
/***/ })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,7 @@
<h2 class="text-center">{{ challenge.type }}</h2>
{% set badge_state = 'badge-danger' if challenge.state == 'hidden' else 'badge-success' %}
<h5>
<span class="badge {{ badge_state }}">
<span class="badge {{ badge_state }} challenge-state">
{{ challenge.state }}
</span>
</h5>

View File

@ -18,28 +18,84 @@
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
{% if q and field %}
<h5 class="text-muted text-center">Searching for challenges with <strong>{{ field }}</strong> matching <strong>{{ q }}</strong></h5>
<h6 class="text-muted text-center pb-3">{{ total }} results</h6>
{% endif %}
<form method="GET" class="form-inline">
<div class="form-group col-md-2">
<label for="sel1" class="sr-only" >Search Field</label>
<select class="form-control custom-select w-100" id="sel1" name="field">
<option value="name" {% if field == 'name' %}selected{% endif %}>Name</option>
<option value="id" {% if field == 'id' %}selected{% endif %}>ID</option>
<option value="category" {% if field == 'category' %}selected{% endif %}>Category</option>
<option value="type" {% if field == 'type' %}selected{% endif %}>Type</option>
</select>
</div>
<div class="form-group col-md-8">
<label for="challenges-search" class="sr-only">Parameter</label>
<input type="text" class="form-control w-100" id="challenges-search" name="q" placeholder="Search for matching challenge" {% if q %}value="{{q}}"{% endif %}>
</div>
<div class="form-group col-md-2">
<label class="sr-only">Search</label>
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-search" aria-hidden="true"></i></button>
</div>
</form>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-12">
<div class="float-right pb-3">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" data-toggle="tooltip" title="Edit Challenges" id="challenges-edit-button">
<i class="btn-fa fas fa-pencil-alt"></i>
</button>
<button type="button" class="btn btn-outline-danger" data-toggle="tooltip" title="Delete Challenges" id="challenges-delete-button">
<i class="btn-fa fas fa-trash-alt"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div>
<table id="challenges" class="table table-striped">
<table id="challenges" class="table table-striped border">
<thead>
<tr>
<td><b>ID</b></td>
<td><b>Name</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Category</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Value</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Type</b></td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>State</b></td>
<td class="d-block border-right border-bottom text-center" data-checkbox>
<div class="form-check">
<input type="checkbox" class="form-check-input" data-checkbox-all>&nbsp;
</div>
</td>
<th class="sort-col text-center"><b>ID</b></th>
<th class="sort-col"><b>Name</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col"><b>Category</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>Value</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>Type</b></th>
<th class="d-none d-md-table-cell d-lg-table-cell sort-col text-center"><b>State</b></th>
</tr>
</thead>
<tbody>
{% for challenge in challenges %}
<tr data-href="{{ url_for('admin.challenges_detail', challenge_id=challenge.id) }}">
<td>{{ challenge.id }}</td>
<td class="d-block border-right text-center" data-checkbox>
<div class="form-check">
<input type="checkbox" class="form-check-input" value="{{ challenge.id }}" data-challenge-id="{{ challenge.id }}">&nbsp;
</div>
</td>
<td class="text-center">{{ challenge.id }}</td>
<td><a href="{{ url_for('admin.challenges_detail', challenge_id=challenge.id) }}">{{ challenge.name }}</a></td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.category }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.value }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.type }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">{{ challenge.value }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">{{ challenge.type }}</td>
<td class="d-none d-md-table-cell d-lg-table-cell text-center">
{% set badge_state = 'badge-danger' if challenge.state == 'hidden' else 'badge-success' %}
<span class="badge {{ badge_state }}">{{ challenge.state }}</span>
@ -56,3 +112,7 @@
{% block scripts %}
{% endblock %}
{% block entrypoint %}
<script defer src="{{ url_for('views.themes', theme='admin', path='js/pages/challenges.js') }}"></script>
{% endblock %}

View File

@ -50,7 +50,7 @@
<small class="form-text text-muted">Files distributed along with your challenge</small>
</label>
<input class="form-control-file" type="file" name="file" multiple="multiple">
<sub>Attach multiple files using Control+Click or Cmd+Click</sub>
<sub class="text-muted">Attach multiple files using Control+Click or Cmd+Click</sub>
</div>
</div>
<div class="form-group">

View File

@ -1,5 +1,9 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<link rel="stylesheet" type="text/css" href="{{ url_for('views.themes', theme='admin', path='css/codemirror.css') }}">
{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="container">

View File

@ -64,7 +64,7 @@
or choose to form teams.
</small>
</label>
<div data-toggle="tooltip" data-placement="bottom" title="In order to change User Mode you must reset your CTF">
<div data-toggle="tooltip" data-placement="bottom" title="In order to change User Mode you must reset your CTF and delete all accounts">
<select class="form-control custom-select" id="user_mode" name="user_mode" disabled="true" style="z-index: -1;">
<option value="teams" {% if user_mode == 'teams' %}selected{% endif %}>
Teams

View File

@ -24,7 +24,7 @@
<div class="col-md-12">
<p>This is the time when the competition will begin. Challenges will automatically
unlock and users will be able to submit answers.</p>
<sub class="text-right mb-3">* All time fields required</sub>
<sub class="text-muted text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">
@ -87,7 +87,7 @@
<div class="col-md-12">
<p>This is the time when the competition will end. Challenges will automatically
close and users won't be able to submit answers.</p>
<sub class="text-right mb-3">* All time fields required</sub>
<sub class="text-muted text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">
@ -159,7 +159,7 @@
<p>Freeze time specifies the timestamp that the competition will be frozen to.
All solves before the freeze time will be shown, but new solves won't be shown to
users. </p>
<sub class="text-right mb-3">* All time fields required</sub>
<sub class="text-muted text-right mb-3">* All time fields required</sub>
</div>
<div class="form-group col-md-2">

View File

@ -24,7 +24,7 @@
<form id="file-add-form" method="POST">
<div class="form-group">
<input class="form-control-file" type="file" name="file" multiple="multiple">
<sub>Attach multiple files using Control+Click or Cmd+Click</sub>
<sub class="text-muted">Attach multiple files using Control+Click or Cmd+Click</sub>
</div>
<div class="form-group">
<button class="btn btn-success float-right" id="submit-files">Upload</button>

View File

@ -11,7 +11,7 @@
<div class="form-group">
<label>Tag
<br>
<small>Type tag and press Enter</small>
<small class="text-muted">Type tag and press Enter</small>
</label>
<input id="tags-add-input" maxlength="80" type="text" class="form-control">
</div>

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