mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
# 3.1.0 / 2020-09-08 **General** - Loosen team password confirmation in team settings to also accept the team captain's password to make it easier to change the team password - Adds the ability to add custom user and team fields for registration/profile settings. - Improve Notifications pubsub events system to use a subscriber per server instead of a subscriber per browser. This should improve the reliability of CTFd at higher load and make it easier to deploy the Notifications system **Admin Panel** - Add a comments functionality for admins to discuss challenges, users, teams, pages - Adds a legal section in Configs where users can add a terms of service and privacy policy - Add a Custom Fields section in Configs where admins can add/edit custom user/team fields - Move user graphs into a modal for Admin Panel **API** - Add `/api/v1/comments` to manipulate and create comments **Themes** - Make scoreboard caching only cache the score table instead of the entire page. This is done by caching the specific template section. Refer to #1586, specifically the changes in `scoreboard.html`. - Add rel=noopener to external links to prevent tab napping attacks - Change the registration page to reference links to Terms of Service and Privacy Policy if specified in configuration **Miscellaneous** - Make team settings modal larger in the core theme - Update tests in Github Actions to properly test under MySQL and Postgres - Make gevent default in serve.py and add a `--disable-gevent` switch in serve.py - Add `tenacity` library for retrying logic - Add `pytest-sugar` for slightly prettier pytest output - Add a `listen()` method to `CTFd.utils.events.EventManager` and `CTFd.utils.events.RedisEventManager`. - This method should implement subscription for a CTFd worker to whatever underlying notification system there is. This should be implemented with gevent or a background thread. - The `subscribe()` method (which used to implement the functionality of the new `listen()` function) now only handles passing notifications from CTFd to the browser. This should also be implemented with gevent or a background thread.
499 lines
13 KiB
JavaScript
499 lines
13 KiB
JavaScript
import "./main";
|
|
import $ from "jquery";
|
|
import CTFd from "core/CTFd";
|
|
import { htmlEntities } from "core/utils";
|
|
import { ezQuery, ezBadge } from "core/ezq";
|
|
import { createGraph, updateGraph } from "core/graphs";
|
|
import Vue from "vue/dist/vue.esm.browser";
|
|
import CommentBox from "../components/comments/CommentBox.vue";
|
|
|
|
function createUser(event) {
|
|
event.preventDefault();
|
|
const params = $("#user-info-create-form").serializeJSON(true);
|
|
|
|
params.fields = [];
|
|
|
|
for (const property in params) {
|
|
if (property.match(/fields\[\d+\]/)) {
|
|
let field = {};
|
|
let id = parseInt(property.slice(7, -1));
|
|
field["field_id"] = id;
|
|
field["value"] = params[property];
|
|
params.fields.push(field);
|
|
delete params[property];
|
|
}
|
|
}
|
|
|
|
// Move the notify value into a GET param
|
|
let url = "/api/v1/users";
|
|
let notify = params.notify;
|
|
if (notify === true) {
|
|
url = `${url}?notify=true`;
|
|
}
|
|
delete params.notify;
|
|
|
|
CTFd.fetch(url, {
|
|
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) {
|
|
const user_id = response.data.id;
|
|
window.location = CTFd.config.urlRoot + "/admin/users/" + user_id;
|
|
} else {
|
|
$("#user-info-create-form > #results").empty();
|
|
Object.keys(response.errors).forEach(function(key, _index) {
|
|
$("#user-info-create-form > #results").append(
|
|
ezBadge({
|
|
type: "error",
|
|
body: response.errors[key]
|
|
})
|
|
);
|
|
const i = $("#user-info-form").find("input[name={0}]".format(key));
|
|
const input = $(i);
|
|
input.addClass("input-filled-invalid");
|
|
input.removeClass("input-filled-valid");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateUser(event) {
|
|
event.preventDefault();
|
|
const params = $("#user-info-edit-form").serializeJSON(true);
|
|
|
|
params.fields = [];
|
|
|
|
for (const property in params) {
|
|
if (property.match(/fields\[\d+\]/)) {
|
|
let field = {};
|
|
let id = parseInt(property.slice(7, -1));
|
|
field["field_id"] = id;
|
|
field["value"] = params[property];
|
|
params.fields.push(field);
|
|
delete params[property];
|
|
}
|
|
}
|
|
|
|
CTFd.fetch("/api/v1/users/" + window.USER_ID, {
|
|
method: "PATCH",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(params)
|
|
})
|
|
.then(function(response) {
|
|
return response.json();
|
|
})
|
|
.then(function(response) {
|
|
if (response.success) {
|
|
window.location.reload();
|
|
} else {
|
|
$("#user-info-edit-form > #results").empty();
|
|
Object.keys(response.errors).forEach(function(key, _index) {
|
|
$("#user-info-edit-form > #results").append(
|
|
ezBadge({
|
|
type: "error",
|
|
body: response.errors[key]
|
|
})
|
|
);
|
|
const i = $("#user-info-edit-form").find(
|
|
"input[name={0}]".format(key)
|
|
);
|
|
const input = $(i);
|
|
input.addClass("input-filled-invalid");
|
|
input.removeClass("input-filled-valid");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteUser(event) {
|
|
event.preventDefault();
|
|
ezQuery({
|
|
title: "Delete User",
|
|
body: "Are you sure you want to delete {0}".format(
|
|
"<strong>" + htmlEntities(window.USER_NAME) + "</strong>"
|
|
),
|
|
success: function() {
|
|
CTFd.fetch("/api/v1/users/" + window.USER_ID, {
|
|
method: "DELETE"
|
|
})
|
|
.then(function(response) {
|
|
return response.json();
|
|
})
|
|
.then(function(response) {
|
|
if (response.success) {
|
|
window.location = CTFd.config.urlRoot + "/admin/users";
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function awardUser(event) {
|
|
event.preventDefault();
|
|
const params = $("#user-award-form").serializeJSON(true);
|
|
params["user_id"] = window.USER_ID;
|
|
|
|
CTFd.fetch("/api/v1/awards", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(params)
|
|
})
|
|
.then(function(response) {
|
|
return response.json();
|
|
})
|
|
.then(function(response) {
|
|
if (response.success) {
|
|
window.location.reload();
|
|
} else {
|
|
$("#user-award-form > #results").empty();
|
|
Object.keys(response.errors).forEach(function(key, _index) {
|
|
$("#user-award-form > #results").append(
|
|
ezBadge({
|
|
type: "error",
|
|
body: response.errors[key]
|
|
})
|
|
);
|
|
const i = $("#user-award-form").find("input[name={0}]".format(key));
|
|
const input = $(i);
|
|
input.addClass("input-filled-invalid");
|
|
input.removeClass("input-filled-valid");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function emailUser(event) {
|
|
event.preventDefault();
|
|
var params = $("#user-mail-form").serializeJSON(true);
|
|
CTFd.fetch("/api/v1/users/" + window.USER_ID + "/email", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(params)
|
|
})
|
|
.then(function(response) {
|
|
return response.json();
|
|
})
|
|
.then(function(response) {
|
|
if (response.success) {
|
|
$("#user-mail-form > #results").append(
|
|
ezBadge({
|
|
type: "success",
|
|
body: "E-Mail sent successfully!"
|
|
})
|
|
);
|
|
$("#user-mail-form")
|
|
.find("input[type=text], textarea")
|
|
.val("");
|
|
} else {
|
|
$("#user-mail-form > #results").empty();
|
|
Object.keys(response.errors).forEach(function(key, _index) {
|
|
$("#user-mail-form > #results").append(
|
|
ezBadge({
|
|
type: "error",
|
|
body: response.errors[key]
|
|
})
|
|
);
|
|
var i = $("#user-mail-form").find(
|
|
"input[name={0}], textarea[name={0}]".format(key)
|
|
);
|
|
var input = $(i);
|
|
input.addClass("input-filled-invalid");
|
|
input.removeClass("input-filled-valid");
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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
|
|
} ${target} correct for ${htmlEntities(window.USER_NAME)}?`,
|
|
success: function() {
|
|
const reqs = [];
|
|
for (var challengeID of challengeIDs) {
|
|
let params = {
|
|
provided: "MARKED AS SOLVED BY ADMIN",
|
|
user_id: window.USER_ID,
|
|
team_id: window.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 }),
|
|
x => CTFd.api.get_team_fails({ teamId: x }),
|
|
x => CTFd.api.get_team_awards({ teamId: x })
|
|
],
|
|
user: [
|
|
x => CTFd.api.get_user_solves({ userId: x }),
|
|
x => CTFd.api.get_user_fails({ userId: x }),
|
|
x => CTFd.api.get_user_awards({ userId: x })
|
|
]
|
|
};
|
|
|
|
const createGraphs = (type, id, name, account_id) => {
|
|
let [solves_func, fails_func, awards_func] = api_funcs[type];
|
|
|
|
Promise.all([
|
|
solves_func(account_id),
|
|
fails_func(account_id),
|
|
awards_func(account_id)
|
|
]).then(responses => {
|
|
createGraph(
|
|
"score_graph",
|
|
"#score-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
createGraph(
|
|
"category_breakdown",
|
|
"#categories-pie-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
createGraph(
|
|
"solve_percentages",
|
|
"#keys-pie-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
});
|
|
};
|
|
|
|
const updateGraphs = (type, id, name, account_id) => {
|
|
let [solves_func, fails_func, awards_func] = api_funcs[type];
|
|
|
|
Promise.all([
|
|
solves_func(account_id),
|
|
fails_func(account_id),
|
|
awards_func(account_id)
|
|
]).then(responses => {
|
|
updateGraph(
|
|
"score_graph",
|
|
"#score-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
updateGraph(
|
|
"category_breakdown",
|
|
"#categories-pie-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
updateGraph(
|
|
"solve_percentages",
|
|
"#keys-pie-graph",
|
|
responses,
|
|
type,
|
|
id,
|
|
name,
|
|
account_id
|
|
);
|
|
});
|
|
};
|
|
|
|
$(() => {
|
|
$(".delete-user").click(deleteUser);
|
|
|
|
$(".edit-user").click(function(_event) {
|
|
$("#user-info-modal").modal("toggle");
|
|
});
|
|
|
|
$(".award-user").click(function(_event) {
|
|
$("#user-award-modal").modal("toggle");
|
|
});
|
|
|
|
$(".email-user").click(function(_event) {
|
|
$("#user-email-modal").modal("toggle");
|
|
});
|
|
|
|
$(".addresses-user").click(function(_event) {
|
|
$("#user-addresses-modal").modal("toggle");
|
|
});
|
|
|
|
$("#user-mail-form").submit(emailUser);
|
|
|
|
$("#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);
|
|
|
|
$("#user-info-edit-form").submit(updateUser);
|
|
$("#user-award-form").submit(awardUser);
|
|
|
|
// Insert CommentBox element
|
|
const commentBox = Vue.extend(CommentBox);
|
|
let vueContainer = document.createElement("div");
|
|
document.querySelector("#comment-box").appendChild(vueContainer);
|
|
new commentBox({
|
|
propsData: { type: "user", id: window.USER_ID }
|
|
}).$mount(vueContainer);
|
|
|
|
let type, id, name, account_id;
|
|
({ type, id, name, account_id } = window.stats_data);
|
|
|
|
let intervalId;
|
|
$("#user-statistics-modal").on("shown.bs.modal", function(_e) {
|
|
createGraphs(type, id, name, account_id);
|
|
intervalId = setInterval(() => {
|
|
updateGraphs(type, id, name, account_id);
|
|
}, 300000);
|
|
});
|
|
|
|
$("#user-statistics-modal").on("hidden.bs.modal", function(_e) {
|
|
clearInterval(intervalId);
|
|
});
|
|
|
|
$(".statistics-user").click(function(_event) {
|
|
$("#user-statistics-modal").modal("toggle");
|
|
});
|
|
});
|