Skip to content

Commit

Permalink
functioning challenge creation
Browse files Browse the repository at this point in the history
  • Loading branch information
Kathode-Negative committed Dec 6, 2024
1 parent e57d815 commit c0db4c0
Show file tree
Hide file tree
Showing 52 changed files with 2,699 additions and 109 deletions.
354 changes: 314 additions & 40 deletions CTFd/plugins/userchallenge/__init__.py

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions CTFd/plugins/userchallenge/apiModding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from CTFd.api.v1.unlocks import unlocks_namespace
from CTFd.api.v1.users import users_namespace

api = Blueprint("userChallengeApi", __name__, url_prefix="/userChallenge/apiModding")
CTFd_API_v1 = Api(
api = Blueprint("userChallengeApi", __name__, url_prefix="/apiModding")
User_API_v1 = Api(
api,
version="v1",
doc=current_app.config.get("SWAGGER_UI_ENDPOINT"),
Expand All @@ -44,11 +44,11 @@
security=["AccessToken"],
)

CTFd_API_v1.schema_model("APISimpleErrorResponse", APISimpleErrorResponse.schema())
CTFd_API_v1.schema_model(
User_API_v1.schema_model("APISimpleErrorResponse", APISimpleErrorResponse.schema())
User_API_v1.schema_model(
"APIDetailedSuccessResponse", APIDetailedSuccessResponse.schema()
)
CTFd_API_v1.schema_model("APISimpleSuccessResponse", APISimpleSuccessResponse.schema())
User_API_v1.schema_model("APISimpleSuccessResponse", APISimpleSuccessResponse.schema())

CTFd_API_v1.add_namespace(challenges_namespace, "/challenges")
CTFd_API_v1.add_namespace(flags_namespace, "/flags")
User_API_v1.add_namespace(challenges_namespace, "/challenges")
User_API_v1.add_namespace(flags_namespace, "/flags")
5 changes: 3 additions & 2 deletions CTFd/plugins/userchallenge/apiModding/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
)

challenges_namespace = Namespace(
"challenges", description="Endpoint to retrieve Challenges"
"userchallenge/challenges", description="Endpoint to retrieve Challenges"
)

ChallengeModel = sqlalchemy_to_pydantic(
Expand Down Expand Up @@ -161,7 +161,7 @@ def get(self, query_args):
# `None` for the solve count if visiblity checks fail
solve_count_dfl = None

chal_q = get_all_challenges(admin=admin_view, field=field, q=q, **query_args)
chal_q = get_all_challenges(admin=True, field=field, q=q, **query_args)

# Iterate through the list of challenges, adding to the object which
# will be JSONified back to the client
Expand Down Expand Up @@ -272,6 +272,7 @@ def get(self):
challenge_class.templates["create"].lstrip("/")
),
}

return {"success": True, "data": response}


Expand Down
Empty file.
12 changes: 6 additions & 6 deletions CTFd/plugins/userchallenge/assets/js/main.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import CTFd from "../compat/CTFd";
import CTFd from "./compat/CTFd";
import Alpine from "alpinejs";
import $ from "jquery";
import dayjs from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat";
import nunjucks from "nunjucks";
import { Howl } from "howler";
import events from "../compat/events";
import times from "../compat/times";
import "../compat/json";
import styles from "../styles";
import { default as helpers } from "../compat/helpers";
import events from "./compat/events";
import times from "./compat/times";
import "./compat/json";
import styles from "./styles";
import { default as helpers } from "./compat/helpers";

dayjs.extend(advancedFormat);

Expand Down
192 changes: 192 additions & 0 deletions CTFd/plugins/userchallenge/assets/js/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import "bootstrap/dist/js/bootstrap.bundle";
import $ from "jquery";
import EasyMDE from "easymde";
import Vue from "vue";
import MediaLibrary from "./components/files/MediaLibrary.vue";
import hljs from "highlight.js";

export function showMediaLibrary(editor) {
const mediaModal = Vue.extend(MediaLibrary);

// Create an empty div and append it to our <main>
let vueContainer = document.createElement("div");
document.querySelector("main").appendChild(vueContainer);

// Create MediaLibrary component and pass it our editor
let m = new mediaModal({
propsData: {
editor: editor,
},
// Mount to the empty div
}).$mount(vueContainer);

// Destroy the Vue instance and the media modal when closed
$("#media-modal").on("hidden.bs.modal", function (_e) {
m.$destroy();
$("#media-modal").remove();
});

// Pop the Component modal
$("#media-modal").modal();
}

export function bindMarkdownEditor(elem) {
if (Object.hasOwn(elem, "mde") === false) {
let mde = new EasyMDE({
autoDownloadFontAwesome: false,
toolbar: [
"bold",
"italic",
"heading",
"|",
"quote",
"unordered-list",
"ordered-list",
"|",
"link",
"image",
{
name: "media",
action: (editor) => {
showMediaLibrary(editor);
},
className: "fas fa-file-upload",
title: "Media Library",
},
"|",
"preview",
"guide",
],
element: elem,
initialValue: $(elem).val(),
forceSync: true,
minHeight: "200px",
renderingConfig: {
codeSyntaxHighlighting: true,
hljs: hljs,
},
});
elem.mde = mde;
elem.codemirror = mde.codemirror;
$(elem).on("change keyup paste", function () {
mde.codemirror.getDoc().setValue($(elem).val());
mde.codemirror.refresh();
});
}
}

export function bindMarkdownEditors() {
$("textarea.markdown").each(function (_i, e) {
bindMarkdownEditor(e);
});
}

export function makeSortableTables() {
$("th.sort-col").append(` <i class="fas fa-sort"></i>`);
$("th.sort-col").click(function () {
var table = $(this).parents("table").eq(0);
var rows = table
.find("tr:gt(0)")
.toArray()
.sort(comparer($(this).index()));
this.asc = !this.asc;
if (!this.asc) {
rows = rows.reverse();
}
for (var i = 0; i < rows.length; i++) {
table.append(rows[i]);
}
});
function comparer(index) {
return function (a, b) {
var valA = getCellValue(a, index),
valB = getCellValue(b, index);
return $.isNumeric(valA) && $.isNumeric(valB)
? valA - valB
: valA.toString().localeCompare(valB);
};
}
function getCellValue(row, index) {
return $(row).children("td").eq(index).text();
}
}

export default () => {
// TODO: This is kind of a hack to mimic a React-like state construct.
// It should be removed once we have a real front-end framework in place.
$(":input").each(function () {
$(this).data("initial", $(this).val());
});

$(function () {
$("tr[data-href], td[data-href]").click(function () {
var sel = getSelection().toString();
if (!sel) {
var href = $(this).attr("data-href");
if (href) {
window.location = href;
}
}
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")) {
e.stopPropagation();
}
});

$(".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");
}
}

bindMarkdownEditors();
makeSortableTables();
$('[data-toggle="tooltip"]').tooltip();

// Syntax highlighting
document.querySelectorAll("pre code").forEach((block) => {
hljs.highlightBlock(block);
});
});
};
65 changes: 22 additions & 43 deletions CTFd/plugins/userchallenge/assets/js/userChallenge.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import "./main";
import $ from "jquery";
import "../compat/json";
import "./compat/json";
import "bootstrap/js/dist/tab";
import CTFd from "../compat/CTFd";
import CTFd from "./compat/CTFd";
import { htmlEntities } from "@ctfdio/ctfd-js/utils/html";
import { ezQuery, ezAlert, ezToast } from "../compat/ezq";
import { default as helpers } from "../compat/helpers";
import { bindMarkdownEditors } from "../styles";
import { ezQuery, ezAlert, ezToast } from "./compat/ezq";
import { default as helpers } from "./compat/helpers";
import { bindMarkdownEditors } from "./styles";
import Vue from "vue";
import CommentBox from "../components/comments/CommentBox.vue";
import FlagList from "../components/flags/FlagList.vue";
import Requirements from "../components/requirements/Requirements.vue";
import TopicsList from "../components/topics/TopicsList.vue";
import TagsList from "../components/tags/TagsList.vue";
import ChallengeFilesList from "../components/files/ChallengeFilesList.vue";
import HintsList from "../components/hints/HintsList.vue";
import NextChallenge from "../components/next/NextChallenge.vue";
import CommentBox from "./components/comments/CommentBox.vue";
import FlagList from "./components/flags/FlagList.vue";
import Requirements from "./components/requirements/Requirements.vue";
import TopicsList from "./components/topics/TopicsList.vue";
import TagsList from "./components/tags/TagsList.vue";
import ChallengeFilesList from "./components/files/ChallengeFilesList.vue";
import HintsList from "./components/hints/HintsList.vue";
import NextChallenge from "./components/next/NextChallenge.vue";

function loadChalTemplate(challenge) {
CTFd._internal.challenge = {};
Expand All @@ -28,7 +28,7 @@ function loadChalTemplate(challenge) {
$("#create-chal-entry-div form").submit(function (event) {
event.preventDefault();
const params = $("#create-chal-entry-div form").serializeJSON();
CTFd.fetch("/api/challenges", {
CTFd.fetch("/userchallenge/api/challenges/", {
method: "POST",
credentials: "same-origin",
headers: {
Expand Down Expand Up @@ -76,7 +76,7 @@ function handleChallengeOptions(event) {
};
// Define a save_challenge function
let save_challenge = function () {
CTFd.fetch("/api/challenges/" + params.challenge_id, {
CTFd.fetch("/userchallenge/api/challenges/" + params.challenge_id, {
method: "PATCH",
credentials: "same-origin",
headers: {
Expand All @@ -94,7 +94,7 @@ function handleChallengeOptions(event) {
if (data.success) {
setTimeout(function () {
window.location =
CTFd.config.urlRoot + "/userChallenge/challenges/" + params.challenge_id;
CTFd.config.urlRoot + "/userchallenge/challenges/" + params.challenge_id;
}, 700);
}
});
Expand All @@ -107,7 +107,7 @@ function handleChallengeOptions(event) {
resolve();
return;
}
CTFd.fetch("/api/flags", {
CTFd.fetch("/userchallenge/api/flags", {
method: "POST",
credentials: "same-origin",
headers: {
Expand Down Expand Up @@ -150,33 +150,11 @@ $(() => {
$("#challenge-comments-window").modal();
});

$(".delete-challenge").click(function (_e) {
ezQuery({
title: "Delete Challenge",
body: `Are you sure you want to delete <strong>${htmlEntities(
window.CHALLENGE_NAME,
)}</strong>`,
success: function () {
CTFd.fetch("/api/challenges/" + window.CHALLENGE_ID, {
method: "DELETE",
})
.then(function (response) {
return response.json();
})
.then(function (response) {
if (response.success) {
window.location = CTFd.config.urlRoot + "/userChallenge/challenges";
}
});
},
});
});

$("#challenge-update-container > form").submit(function (e) {
e.preventDefault();
var params = $(e.target).serializeJSON(true);

CTFd.fetch("/api/challenges/" + window.CHALLENGE_ID + "/flags", {
CTFd.fetch("/userchallenge/api/challenges/" + window.CHALLENGE_ID + "/flags", {
method: "GET",
credentials: "same-origin",
headers: {
Expand All @@ -189,7 +167,7 @@ $(() => {
})
.then(function (response) {
let update_challenge = function () {
CTFd.fetch("/api/challenges/" + window.CHALLENGE_ID, {
CTFd.fetch("/userchallenge/api/challenges/" + window.CHALLENGE_ID, {
method: "PATCH",
credentials: "same-origin",
headers: {
Expand Down Expand Up @@ -334,8 +312,9 @@ $(() => {
}).$mount(vueContainer);
}

$.get(CTFd.config.urlRoot + "/api/challenges/types", function (response) {
const data = response.data;
$.get("/userchallenge/api/challenges/types", function (res) {
console.log(res)
const data = res.data;
loadChalTemplate(data["standard"]);

$("#create-chals-select input[name=type]").change(function () {
Expand Down
2 changes: 2 additions & 0 deletions CTFd/plugins/userchallenge/static/img/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Original Logo by [Laura Barbera](https://dribbble.com/lauragallisa)
Updated by [Kevin Chung](https://github.com/ColdHeat)
Binary file added CTFd/plugins/userchallenge/static/img/favicon.ico
Binary file not shown.
Binary file added CTFd/plugins/userchallenge/static/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c0db4c0

Please sign in to comment.