From bff71c6169eaf41d433906fc9a04b620badf385e Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Fri, 23 Jul 2021 15:07:00 -0400 Subject: [PATCH] Make user mode easier to change without having to reset accounts (#1956) * Closes #1889 * Makes user mode easier to change without having to reset accounts. Submissions are still required to be deleted. --- CTFd/themes/admin/assets/css/admin.scss | 13 ++++ CTFd/themes/admin/assets/js/pages/configs.js | 26 ++++++- CTFd/themes/admin/static/css/admin.dev.css | 2 +- CTFd/themes/admin/static/css/admin.min.css | 2 +- .../admin/static/js/pages/configs.dev.js | 2 +- .../admin/static/js/pages/configs.min.js | 2 +- .../admin/templates/challenges/new.html | 14 ---- CTFd/themes/admin/templates/config.html | 5 ++ .../admin/templates/configs/appearance.html | 19 ----- .../admin/templates/configs/usermode.html | 72 +++++++++++++++++++ 10 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 CTFd/themes/admin/templates/configs/usermode.html diff --git a/CTFd/themes/admin/assets/css/admin.scss b/CTFd/themes/admin/assets/css/admin.scss index e1f1acf7..9630d5e7 100644 --- a/CTFd/themes/admin/assets/css/admin.scss +++ b/CTFd/themes/admin/assets/css/admin.scss @@ -79,3 +79,16 @@ tbody tr:hover { input[type="checkbox"] { cursor: pointer; } + +.card-radio:checked + .card { + background-color: transparent !important; + border-color: #a3d39c; + box-shadow: 0 0 0 0.1rem #a3d39c; + transition: background-color 0.3s, border-color 0.3s; +} +.card-radio:checked + .card .card-radio-clone { + visibility: visible !important; +} +.card:hover { + cursor: pointer; +} diff --git a/CTFd/themes/admin/assets/js/pages/configs.js b/CTFd/themes/admin/assets/js/pages/configs.js index 326c941d..ab815cd9 100644 --- a/CTFd/themes/admin/assets/js/pages/configs.js +++ b/CTFd/themes/admin/assets/js/pages/configs.js @@ -162,6 +162,27 @@ function uploadLogo(event) { }); } +function switchUserMode(event) { + event.preventDefault(); + if ( + confirm( + "Are you sure you'd like to switch user modes?\n\nAll user submissions, awards, unlocks, and tracking will be deleted!" + ) + ) { + let formData = new FormData(); + formData.append("submissions", true); + formData.append("nonce", CTFd.config.csrfNonce); + fetch(CTFd.config.urlRoot + "/admin/reset", { + method: "POST", + credentials: "same-origin", + body: formData + }); + // Bind `this` so that we can reuse the updateConfigs function + let binded = updateConfigs.bind(this); + binded(event); + } +} + function removeLogo() { ezQuery({ title: "Remove logo", @@ -382,8 +403,11 @@ $(() => { insertTimezones($("#end-timezone")); insertTimezones($("#freeze-timezone")); - $(".config-section > form:not(.form-upload)").submit(updateConfigs); + $(".config-section > form:not(.form-upload, .custom-config-form)").submit( + updateConfigs + ); $("#logo-upload").submit(uploadLogo); + $("#user-mode-form").submit(switchUserMode); $("#remove-logo").click(removeLogo); $("#ctf-small-icon-upload").submit(smallIconUpload); $("#remove-small-icon").click(removeSmallIcon); diff --git a/CTFd/themes/admin/static/css/admin.dev.css b/CTFd/themes/admin/static/css/admin.dev.css index e246e895..c66bc3ae 100644 --- a/CTFd/themes/admin/static/css/admin.dev.css +++ b/CTFd/themes/admin/static/css/admin.dev.css @@ -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{min-height:400px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{min-height:400px;display:block}#categories-pie-graph{min-height:400px;display:block}#solve-percentages-graph{min-height:400px;display:block}#score-distribution-graph{min-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}[data-href]{cursor:pointer}.sort-col{cursor:pointer}input[type="checkbox"]{cursor:pointer} +#score-graph{min-height:400px;display:block;clear:both}#solves-graph{display:block;height:350px}#keys-pie-graph{min-height:400px;display:block}#categories-pie-graph{min-height:400px;display:block}#solve-percentages-graph{min-height:400px;display:block}#score-distribution-graph{min-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}[data-href]{cursor:pointer}.sort-col{cursor:pointer}input[type="checkbox"]{cursor:pointer}.card-radio:checked+.card{background-color:transparent !important;border-color:#a3d39c;box-shadow:0 0 0 0.1rem #a3d39c;transition:background-color 0.3s, border-color 0.3s}.card-radio:checked+.card .card-radio-clone{visibility:visible !important}.card:hover{cursor:pointer} diff --git a/CTFd/themes/admin/static/css/admin.min.css b/CTFd/themes/admin/static/css/admin.min.css index 8d4e944b..69dc0f4e 100644 --- a/CTFd/themes/admin/static/css/admin.min.css +++ b/CTFd/themes/admin/static/css/admin.min.css @@ -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{min-height:400px;display:block;clear:both}#solves-graph{display:block;height:350px}#categories-pie-graph,#keys-pie-graph,#score-distribution-graph,#solve-percentages-graph{min-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,[data-href],input[type=checkbox]{cursor:pointer} \ No newline at end of file +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{min-height:400px;display:block;clear:both}#solves-graph{display:block;height:350px}#categories-pie-graph,#keys-pie-graph,#score-distribution-graph,#solve-percentages-graph{min-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,[data-href],input[type=checkbox]{cursor:pointer}.card-radio:checked+.card{background-color:transparent!important;border-color:#a3d39c;box-shadow:0 0 0 .1rem #a3d39c;transition:background-color .3s,border-color .3s}.card-radio:checked+.card .card-radio-clone{visibility:visible!important}.card:hover{cursor:pointer} \ No newline at end of file diff --git a/CTFd/themes/admin/static/js/pages/configs.dev.js b/CTFd/themes/admin/static/js/pages/configs.dev.js index 04a0366f..efb7b40d 100644 --- a/CTFd/themes/admin/static/js/pages/configs.dev.js +++ b/CTFd/themes/admin/static/js/pages/configs.dev.js @@ -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__webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\n__webpack_require__(/*! bootstrap/js/dist/tab */ \"./node_modules/bootstrap/js/dist/tab.js\");\n\nvar _dayjs = _interopRequireDefault(__webpack_require__(/*! dayjs */ \"./node_modules/dayjs/dayjs.min.js\"));\n\nvar _advancedFormat = _interopRequireDefault(__webpack_require__(/*! dayjs/plugin/advancedFormat */ \"./node_modules/dayjs/plugin/advancedFormat.js\"));\n\nvar _utc = _interopRequireDefault(__webpack_require__(/*! dayjs/plugin/utc */ \"./node_modules/dayjs/plugin/utc.js\"));\n\nvar _timezone = _interopRequireDefault(__webpack_require__(/*! dayjs/plugin/timezone */ \"./node_modules/dayjs/plugin/timezone.js\"));\n\nvar _timezones = _interopRequireDefault(__webpack_require__(/*! ../timezones */ \"./CTFd/themes/admin/assets/js/timezones.js\"));\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _helpers = _interopRequireDefault(__webpack_require__(/*! core/helpers */ \"./CTFd/themes/core/assets/js/helpers.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\nvar _codemirror = _interopRequireDefault(__webpack_require__(/*! codemirror */ \"./node_modules/codemirror/lib/codemirror.js\"));\n\n__webpack_require__(/*! codemirror/mode/htmlmixed/htmlmixed.js */ \"./node_modules/codemirror/mode/htmlmixed/htmlmixed.js\");\n\nvar _vueEsm = _interopRequireDefault(__webpack_require__(/*! vue/dist/vue.esm.browser */ \"./node_modules/vue/dist/vue.esm.browser.js\"));\n\nvar _FieldList = _interopRequireDefault(__webpack_require__(/*! ../components/configs/fields/FieldList.vue */ \"./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n_dayjs[\"default\"].extend(_advancedFormat[\"default\"]);\n\n_dayjs[\"default\"].extend(_utc[\"default\"]);\n\n_dayjs[\"default\"].extend(_timezone[\"default\"]);\n\nfunction loadTimestamp(place, timestamp) {\n if (typeof timestamp == \"string\") {\n timestamp = parseInt(timestamp, 10) * 1000;\n }\n\n var d = (0, _dayjs[\"default\"])(timestamp);\n (0, _jquery[\"default\"])(\"#\" + place + \"-month\").val(d.month() + 1); // Months are zero indexed (https://day.js.org/docs/en/get-set/month)\n\n (0, _jquery[\"default\"])(\"#\" + place + \"-day\").val(d.date());\n (0, _jquery[\"default\"])(\"#\" + place + \"-year\").val(d.year());\n (0, _jquery[\"default\"])(\"#\" + place + \"-hour\").val(d.hour());\n (0, _jquery[\"default\"])(\"#\" + place + \"-minute\").val(d.minute());\n loadDateValues(place);\n}\n\nfunction loadDateValues(place) {\n var month = (0, _jquery[\"default\"])(\"#\" + place + \"-month\").val();\n var day = (0, _jquery[\"default\"])(\"#\" + place + \"-day\").val();\n var year = (0, _jquery[\"default\"])(\"#\" + place + \"-year\").val();\n var hour = (0, _jquery[\"default\"])(\"#\" + place + \"-hour\").val();\n var minute = (0, _jquery[\"default\"])(\"#\" + place + \"-minute\").val();\n var timezone_string = (0, _jquery[\"default\"])(\"#\" + place + \"-timezone\").val();\n var utc = convertDateToMoment(month, day, year, hour, minute);\n\n if (utc.unix() && month && day && year && hour && minute) {\n (0, _jquery[\"default\"])(\"#\" + place).val(utc.unix());\n (0, _jquery[\"default\"])(\"#\" + place + \"-local\").val(utc.format(\"dddd, MMMM Do YYYY, h:mm:ss a z (zzz)\"));\n (0, _jquery[\"default\"])(\"#\" + place + \"-zonetime\").val(utc.tz(timezone_string).format(\"dddd, MMMM Do YYYY, h:mm:ss a z (zzz)\"));\n } else {\n (0, _jquery[\"default\"])(\"#\" + place).val(\"\");\n (0, _jquery[\"default\"])(\"#\" + place + \"-local\").val(\"\");\n (0, _jquery[\"default\"])(\"#\" + place + \"-zonetime\").val(\"\");\n }\n}\n\nfunction convertDateToMoment(month, day, year, hour, minute) {\n var month_num = month.toString();\n\n if (month_num.length == 1) {\n month_num = \"0\" + month_num;\n }\n\n var day_str = day.toString();\n\n if (day_str.length == 1) {\n day_str = \"0\" + day_str;\n }\n\n var hour_str = hour.toString();\n\n if (hour_str.length == 1) {\n hour_str = \"0\" + hour_str;\n }\n\n var min_str = minute.toString();\n\n if (min_str.length == 1) {\n min_str = \"0\" + min_str;\n } // 2013-02-08 24:00\n\n\n var date_string = year.toString() + \"-\" + month_num + \"-\" + day_str + \" \" + hour_str + \":\" + min_str + \":00\";\n return (0, _dayjs[\"default\"])(date_string);\n}\n\nfunction updateConfigs(event) {\n event.preventDefault();\n var obj = (0, _jquery[\"default\"])(this).serializeJSON();\n var params = {};\n\n if (obj.mail_useauth === false) {\n obj.mail_username = null;\n obj.mail_password = null;\n } else {\n if (obj.mail_username === \"\") {\n delete obj.mail_username;\n }\n\n if (obj.mail_password === \"\") {\n delete obj.mail_password;\n }\n }\n\n Object.keys(obj).forEach(function (x) {\n if (obj[x] === \"true\") {\n params[x] = true;\n } else if (obj[x] === \"false\") {\n params[x] = false;\n } else {\n params[x] = obj[x];\n }\n });\n\n _CTFd[\"default\"].api.patch_config_list({}, params).then(function (_response) {\n if (_response.success) {\n window.location.reload();\n } else {\n var errors = _response.errors.value.join(\"\\n\");\n\n (0, _ezq.ezAlert)({\n title: \"Error!\",\n body: errors,\n button: \"Okay\"\n });\n }\n });\n}\n\nfunction uploadLogo(event) {\n event.preventDefault();\n var form = event.target;\n\n _helpers[\"default\"].files.upload(form, {}, function (response) {\n var f = response.data[0];\n var params = {\n value: f.location\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/configs/ctf_logo\", {\n method: \"PATCH\",\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _ezq.ezAlert)({\n title: \"Error!\",\n body: \"Logo uploading failed!\",\n button: \"Okay\"\n });\n }\n });\n });\n}\n\nfunction removeLogo() {\n (0, _ezq.ezQuery)({\n title: \"Remove logo\",\n body: \"Are you sure you'd like to remove the CTF logo?\",\n success: function success() {\n var params = {\n value: null\n };\n\n _CTFd[\"default\"].api.patch_config({\n configKey: \"ctf_logo\"\n }, params).then(function (_response) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction smallIconUpload(event) {\n event.preventDefault();\n var form = event.target;\n\n _helpers[\"default\"].files.upload(form, {}, function (response) {\n var f = response.data[0];\n var params = {\n value: f.location\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/configs/ctf_small_icon\", {\n method: \"PATCH\",\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _ezq.ezAlert)({\n title: \"Error!\",\n body: \"Icon uploading failed!\",\n button: \"Okay\"\n });\n }\n });\n });\n}\n\nfunction removeSmallIcon() {\n (0, _ezq.ezQuery)({\n title: \"Remove logo\",\n body: \"Are you sure you'd like to remove the small site icon?\",\n success: function success() {\n var params = {\n value: null\n };\n\n _CTFd[\"default\"].api.patch_config({\n configKey: \"ctf_small_icon\"\n }, params).then(function (_response) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction importConfig(event) {\n event.preventDefault();\n var import_file = document.getElementById(\"import-file\").files[0];\n var form_data = new FormData();\n form_data.append(\"backup\", import_file);\n form_data.append(\"nonce\", _CTFd[\"default\"].config.csrfNonce);\n var pg = (0, _ezq.ezProgressBar)({\n width: 0,\n title: \"Upload Progress\"\n });\n\n _jquery[\"default\"].ajax({\n url: _CTFd[\"default\"].config.urlRoot + \"/admin/import\",\n type: \"POST\",\n data: form_data,\n processData: false,\n contentType: false,\n statusCode: {\n 500: function _(resp) {\n alert(resp.responseText);\n }\n },\n xhr: function xhr() {\n var xhr = _jquery[\"default\"].ajaxSettings.xhr();\n\n xhr.upload.onprogress = function (e) {\n if (e.lengthComputable) {\n var width = e.loaded / e.total * 100;\n pg = (0, _ezq.ezProgressBar)({\n target: pg,\n width: width\n });\n }\n };\n\n return xhr;\n },\n success: function success(_data) {\n pg = (0, _ezq.ezProgressBar)({\n target: pg,\n width: 100\n });\n setTimeout(function () {\n pg.modal(\"hide\");\n }, 500);\n setTimeout(function () {\n window.location.reload();\n }, 700);\n }\n });\n}\n\nfunction exportConfig(event) {\n event.preventDefault();\n window.location.href = (0, _jquery[\"default\"])(this).attr(\"href\");\n}\n\nfunction insertTimezones(target) {\n var current = (0, _jquery[\"default\"])(\"