working on xss mitigation
This commit is contained in:
parent
9fee4b533d
commit
c1322466b1
@ -19,6 +19,24 @@ function getCurrentDateTime() {
|
|||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitise a given string to remove HTML, making it DOM safe.
|
||||||
|
function sanitise(string) {
|
||||||
|
if (typeof string !== "string") {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
"/": '/',
|
||||||
|
};
|
||||||
|
const reg = /[&<>"'/]/ig;
|
||||||
|
return string.replace(reg, (match) => map[match]);
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Activate all tooltips
|
// Activate all tooltips
|
||||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||||
|
@ -46,7 +46,9 @@ async function initContentTable() {
|
|||||||
data: "title",
|
data: "title",
|
||||||
className: "text-truncate",
|
className: "text-truncate",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<a href="${row.url}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>`
|
const title = sanitise(data);
|
||||||
|
const url = sanitise(row.url);
|
||||||
|
return `<a href="${url}" class="btn btn-link text-start text-decoration-none" target="_blank">${title}</a>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -54,7 +56,8 @@ async function initContentTable() {
|
|||||||
data: "subscription.name",
|
data: "subscription.name",
|
||||||
className: "text-nowrap",
|
className: "text-nowrap",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`
|
const subName = sanitise(data);
|
||||||
|
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-start text-decoration-none">${subName}</button>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -70,7 +73,9 @@ async function initContentTable() {
|
|||||||
data: "channel_id",
|
data: "channel_id",
|
||||||
className: "text-start",
|
className: "text-start",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<div class="resolve-channel-name text-center" data-channel-id="${data}" data-msg-id="${row.message_id}">
|
const channelId = sanitise(data);
|
||||||
|
const messageId = sanitise(row.message_id);
|
||||||
|
return `<div class="resolve-channel-name text-center" data-channel-id="${channelId}" data-msg-id="${messageId}">
|
||||||
<div class="spinner-border spinner-border-sm" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
@ -98,7 +103,8 @@ async function initContentTable() {
|
|||||||
orderable: false,
|
orderable: false,
|
||||||
className: "p-0",
|
className: "p-0",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<div class="h-100" style="background-color: #${row.subscription.embed_colour}; width: .25rem;"> </div>`
|
const embedColour = sanitise(row.subscription.embed_colour);
|
||||||
|
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;"> </div>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -166,17 +172,18 @@ async function deleteSelectedContent() {
|
|||||||
const rows = contentTable.rows(".selected").data().toArray();
|
const rows = contentTable.rows(".selected").data().toArray();
|
||||||
const names = rows.map(row => { return row.title });
|
const names = rows.map(row => { return row.title });
|
||||||
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
||||||
const multiple = names.length > 1;
|
const isMany = names.length > 1;
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
|
`Delete ${isMany ? "Many Tracked Contents" : "a Tracked Content"}`,
|
||||||
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> content${multiple ? "s" : ""}?<br><br>${namesString}`,
|
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> Tracked Content${isMany ? "s" : ""}?<br><br>${namesString}`,
|
||||||
|
"danger",
|
||||||
async () => {
|
async () => {
|
||||||
rows.forEach(async row => { await deleteTrackedContent(row.id) });
|
rows.forEach(async row => { await deleteTrackedContent(row.id) });
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
"danger",
|
"danger",
|
||||||
`Deleted ${names.length} Content${multiple ? "s" : ""}`,
|
`Deleted ${names.length} Content${isMany ? "s" : ""}`,
|
||||||
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
||||||
12000
|
12000
|
||||||
);
|
);
|
||||||
|
@ -40,7 +40,8 @@ async function initFiltersTable() {
|
|||||||
title: "Name",
|
title: "Name",
|
||||||
data: "name",
|
data: "name",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`
|
const name = sanitise(data);
|
||||||
|
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-start text-decoration-none">${name}</button>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -55,7 +56,7 @@ async function initFiltersTable() {
|
|||||||
case 5: return "Fuzzy Match";
|
case 5: return "Fuzzy Match";
|
||||||
default:
|
default:
|
||||||
console.error(`unknown matching algorithm '${data}'`);
|
console.error(`unknown matching algorithm '${data}'`);
|
||||||
return data;
|
return sanitise(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -252,12 +253,14 @@ $(document).on("selectedServerChange", async function() {
|
|||||||
$("#deleteEditFilter").on("click", async function() {
|
$("#deleteEditFilter").on("click", async function() {
|
||||||
const filterId = parseInt($("#filterId").val());
|
const filterId = parseInt($("#filterId").val());
|
||||||
const filter = filtersTable.row(function(idx, row) { return row.id === filterId }).data();
|
const filter = filtersTable.row(function(idx, row) { return row.id === filterId }).data();
|
||||||
|
const filterName = sanitise(filter.name);
|
||||||
|
|
||||||
$("#filterFormModal").modal("hide");
|
$("#filterFormModal").modal("hide");
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
"Confirm Deletion",
|
"Delete a Filter",
|
||||||
`Do you wish to permanently delete <b>${filter.name}</b>?`, // FIX: potential xss attack
|
`Do you wish to permanently delete <b>${filterName}</b>?`,
|
||||||
|
"danger",
|
||||||
async () => {
|
async () => {
|
||||||
await deleteFilter(filterId);
|
await deleteFilter(filterId);
|
||||||
await loadFilters(getCurrentlyActiveServer().guild_id);
|
await loadFilters(getCurrentlyActiveServer().guild_id);
|
||||||
@ -265,7 +268,7 @@ $("#deleteEditFilter").on("click", async function() {
|
|||||||
showToast(
|
showToast(
|
||||||
"danger",
|
"danger",
|
||||||
"Deleted a Filter",
|
"Deleted a Filter",
|
||||||
filter.name, // FIX: potential xss attack
|
filterName,
|
||||||
12000
|
12000
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -279,27 +282,29 @@ async function deleteSelectedFilters() {
|
|||||||
const rows = filtersTable.rows(".selected").data().toArray();
|
const rows = filtersTable.rows(".selected").data().toArray();
|
||||||
const names = rows.map(row => row.name);
|
const names = rows.map(row => row.name);
|
||||||
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
||||||
const multiple = names.length > 1;
|
const isMany = names.length > 1;
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
|
`Delete ${isMany ? "Many Filters" : "a Filter"}`,
|
||||||
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> filter${multiple ? "s" : ""}?<br><br>${namesString}`,
|
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> filter${isMany ? "s" : ""}?<br><br>${namesString}`,
|
||||||
|
"danger",
|
||||||
async () => {
|
async () => {
|
||||||
rows.forEach(async row => { await deleteFilter(row.id) });
|
rows.forEach(async row => { await deleteFilter(row.id) });
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
"danger",
|
"danger",
|
||||||
`Delete ${names.length} Subscription${multiple ? "s" : ""}`,
|
`Delete ${names.length} Subscription${isMany ? "s" : ""}`,
|
||||||
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
||||||
12000
|
12000
|
||||||
);
|
);
|
||||||
|
|
||||||
// Multi-deletion can take time, this timeout ensures the refresh is accurate
|
// Multi-deletion can take time, this timeout ensures the refresh is accurate
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await loadFilters(getCurrentlyActiveServer().guild_id);
|
await loadFilters(getCurrentlyActiveServer().guild_id);
|
||||||
}, 600);
|
}, 600);
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,23 +155,33 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function confirmDeleteModal(title, description, acceptFunc, declineFunc) {
|
async function confirmationModal(title, bodyText, style, acceptFunc, declineFunc) {
|
||||||
let $modal = $("#confirmDeleteModal");
|
let $modal = $("#confirmationModal");
|
||||||
|
|
||||||
|
// Ensure valid style and apply it to the confirm button
|
||||||
|
if (!["danger", "success", "warning", "info", "primary", "secondary"].includes(style)) {
|
||||||
|
throw new Error(`${style} is not a valid style`);
|
||||||
|
}
|
||||||
|
$modal.find(".modal-confirm-btn").addClass(`btn-${style}`);
|
||||||
|
|
||||||
$modal.find(".modal-title").text(title);
|
$modal.find(".modal-title").text(title);
|
||||||
$modal.find(".modal-body > p").html(description);
|
$modal.find(".modal-body > p").html(bodyText);
|
||||||
$modal.find(".confirm-delete-btn").off("click").on("click", async function(e) {
|
|
||||||
|
$modal.find(".modal-confirm-btn").off("click").on("click", async function(e) {
|
||||||
await acceptFunc()
|
await acceptFunc()
|
||||||
$modal.modal("hide");
|
$modal.modal("hide");
|
||||||
});
|
});
|
||||||
$modal.find(".dismiss-delete-btn").off("click").on("click", async function(e) {
|
|
||||||
|
$modal.find(".modal-dismiss-btn").off("click").on("click", async function(e) {
|
||||||
if (declineFunc) await declineFunc();
|
if (declineFunc) await declineFunc();
|
||||||
$modal.modal("hide");
|
$modal.modal("hide");
|
||||||
});
|
});
|
||||||
|
|
||||||
$modal.modal("show");
|
$modal.modal("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayToHtmlList(array, bold=false) {
|
function arrayToHtmlList(array, bold=false) {
|
||||||
$ul = $("<ul>");
|
$ul = $("<ul>").addClass("mb-0");
|
||||||
|
|
||||||
array.forEach(item => {
|
array.forEach(item => {
|
||||||
let $li = $("<li>");
|
let $li = $("<li>");
|
||||||
|
@ -35,7 +35,7 @@ function addToLoadedServers(server, selectNew=true) {
|
|||||||
loadedServers[id] = server;
|
loadedServers[id] = server;
|
||||||
|
|
||||||
// Display the loaded server
|
// Display the loaded server
|
||||||
addServerTemplate(id, server.guild_id, server.name, server.icon, server.permissions, server.owner);
|
addServerTemplate(id, sanitise(server.guild_id), sanitise(server.name), sanitise(server.icon), sanitise(server.permissions), sanitise(server.owner));
|
||||||
|
|
||||||
// Select the newly added server
|
// Select the newly added server
|
||||||
if (selectNew) {
|
if (selectNew) {
|
||||||
@ -90,10 +90,10 @@ async function loadServerOptions() {
|
|||||||
servers.forEach(server => {
|
servers.forEach(server => {
|
||||||
$("#serverOptions").append($("<option>", {
|
$("#serverOptions").append($("<option>", {
|
||||||
value: server.id,
|
value: server.id,
|
||||||
text: server.name,
|
text: sanitise(server.name),
|
||||||
"data-icon": server.icon,
|
"data-icon": sanitise(server.icon),
|
||||||
"data-permissions": server.permissions,
|
"data-permissions": sanitise(server.permissions),
|
||||||
"data-isowner": server.owner
|
"data-isowner": sanitise(server.owner)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ async function registerNewServer(serverName, serverGuildId, serverIconHash, serv
|
|||||||
try { response = await newSavedGuild(formData); }
|
try { response = await newSavedGuild(formData); }
|
||||||
catch (err) {
|
catch (err) {
|
||||||
if (err.status === 409)
|
if (err.status === 409)
|
||||||
showToast("warning", "Server Conflict", `Can't add ${serverName} because it already exists.`, 10000);
|
showToast("warning", "Server Conflict", `Can't add ${sanitise(serverName)} because it already exists.`, 10000);
|
||||||
else
|
else
|
||||||
console.error(JSON.stringify(err, null, 4));
|
console.error(JSON.stringify(err, null, 4));
|
||||||
|
|
||||||
@ -221,8 +221,8 @@ function selectServer(primaryKey) {
|
|||||||
$(`#serverList .server-item[data-id=${primaryKey}]`).addClass("active")
|
$(`#serverList .server-item[data-id=${primaryKey}]`).addClass("active")
|
||||||
|
|
||||||
// Display details of the selected server
|
// Display details of the selected server
|
||||||
$("#selectedServerContainer .selected-server-name").text(server.name);
|
$("#selectedServerContainer .selected-server-name").text(sanitise(server.name));
|
||||||
$("#selectedServerContainer .selected-server-id").text(server.guild_id);
|
$("#selectedServerContainer .selected-server-id").text(sanitise(server.guild_id));
|
||||||
$("#selectedServerContainer .selected-server-icon").attr("src", `https://cdn.discordapp.com/icons/${server.guild_id}/${server.icon}.webp?size=80`);
|
$("#selectedServerContainer .selected-server-icon").attr("src", `https://cdn.discordapp.com/icons/${server.guild_id}/${server.icon}.webp?size=80`);
|
||||||
|
|
||||||
// Disable all loaded servers
|
// Disable all loaded servers
|
||||||
@ -253,9 +253,10 @@ $("#deleteSelectedServerBtn").on("click", async function() {
|
|||||||
];
|
];
|
||||||
const notesString = arrayToHtmlList(notes).prop("outerHTML");
|
const notesString = arrayToHtmlList(notes).prop("outerHTML");
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
"Close this server?",
|
"Close this server?",
|
||||||
`This is a safe, non-permanent action:<br><br>${notesString}`,
|
`This is a safe, non-permanent action:<br><br>${notesString}`,
|
||||||
|
"warning",
|
||||||
deleteSelectedServer,
|
deleteSelectedServer,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@ -294,17 +295,17 @@ function resolveServerStrings() {
|
|||||||
const server = getCurrentlyActiveServer();
|
const server = getCurrentlyActiveServer();
|
||||||
|
|
||||||
// Server names
|
// Server names
|
||||||
$(".resolve-to-server-name").text(server.name);
|
$(".resolve-to-server-name").text(sanitise(server.name));
|
||||||
|
|
||||||
// Server Guild Ids
|
// Server Guild Ids
|
||||||
$(".resolve-to-server-id").text(server.guild_id)
|
$(".resolve-to-server-id").text(sanitise(server.guild_id))
|
||||||
|
|
||||||
// Bot Invite links
|
// Bot Invite links
|
||||||
$(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize
|
$(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize
|
||||||
?client_id=${discordClientId}
|
?client_id=${discordClientId}
|
||||||
&permissions=2147534848
|
&permissions=2147534848
|
||||||
&scope=bot+applications.commands
|
&scope=bot+applications.commands
|
||||||
&guild_id=${server.guild_id}
|
&guild_id=${sanitise(server.guild_id)}
|
||||||
&disable_guild_select=true`);
|
&disable_guild_select=true`);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,8 @@ async function initSubscriptionTable() {
|
|||||||
data: "name",
|
data: "name",
|
||||||
className: "text-truncate",
|
className: "text-truncate",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`;
|
const name = sanitise(data);
|
||||||
|
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-start text-decoration-none">${name}</button>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -51,7 +52,8 @@ async function initSubscriptionTable() {
|
|||||||
data: "url",
|
data: "url",
|
||||||
className: "text-truncate",
|
className: "text-truncate",
|
||||||
render: function(data, type) {
|
render: function(data, type) {
|
||||||
return `<a href="${data}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>`;
|
const url = sanitise(data);
|
||||||
|
return `<a href="${url}" class="btn btn-link text-start text-decoration-none" target="_blank">${url}</a>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -59,7 +61,8 @@ async function initSubscriptionTable() {
|
|||||||
data: "channels_count",
|
data: "channels_count",
|
||||||
className: "text-center",
|
className: "text-center",
|
||||||
render: function(data) {
|
render: function(data) {
|
||||||
return `<span class="badge text-bg-secondary">${data}</span>`;
|
const channelsCount = sanitise(data);
|
||||||
|
return `<span class="badge text-bg-secondary">${channelsCount}</span>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -84,13 +87,14 @@ async function initSubscriptionTable() {
|
|||||||
orderable: false,
|
orderable: false,
|
||||||
className: "text-center",
|
className: "text-center",
|
||||||
render: function(data, type) {
|
render: function(data, type) {
|
||||||
if (!data) return "";
|
if (!data) { return "" }
|
||||||
|
const extraNotes = sanitise(data);
|
||||||
return $(`
|
return $(`
|
||||||
<i class="bi bi-chat-left-text"
|
<i class="bi bi-chat-left-text"
|
||||||
data-bs-trigger="hover focus"
|
data-bs-trigger="hover focus"
|
||||||
data-bs-toggle="popover"
|
data-bs-toggle="popover"
|
||||||
data-bs-title="Extra Notes"
|
data-bs-title="Extra Notes"
|
||||||
data-bs-content="${data}">
|
data-bs-content="${extraNotes}">
|
||||||
</i>
|
</i>
|
||||||
`).popover()[0];
|
`).popover()[0];
|
||||||
}
|
}
|
||||||
@ -108,7 +112,8 @@ async function initSubscriptionTable() {
|
|||||||
orderable: false,
|
orderable: false,
|
||||||
className: "p-0",
|
className: "p-0",
|
||||||
render: function(data, type, row) {
|
render: function(data, type, row) {
|
||||||
return `<div class="h-100" style="background-color: #${row.embed_colour}; width: .25rem;"> </div>`
|
const embedColour = sanitise(row.embed_colour);
|
||||||
|
return `<div class="h-100" style="background-color: #${embedColour}; width: .25rem;"> </div>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -429,20 +434,22 @@ async function updateDefaultSubEmbedColour(settings=null) {
|
|||||||
$("#deleteEditSub").on("click", async function() {
|
$("#deleteEditSub").on("click", async function() {
|
||||||
const subId = parseInt($("#subId").val());
|
const subId = parseInt($("#subId").val());
|
||||||
const sub = subTable.row(function(idx, row) { return row.id === subId }).data();
|
const sub = subTable.row(function(idx, row) { return row.id === subId }).data();
|
||||||
|
const subName = sanitise(sub.name);
|
||||||
|
|
||||||
$("#subFormModal").modal("hide");
|
$("#subFormModal").modal("hide");
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
"Confirm Deletion",
|
"Delete a Subscription",
|
||||||
`Do you wish to permanently delete <b>${sub.name}</b>?`, // FIX: potential xss attack
|
`Do you wish to permanently delete <b>${subName}</b>?`,
|
||||||
|
"danger",
|
||||||
async () => {
|
async () => {
|
||||||
await deleteSubscription(subId);
|
await deleteSubscription(subId);
|
||||||
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
"danger",
|
"danger",
|
||||||
"Deleted a Subscription",
|
"Deleted a Subscription",
|
||||||
sub.name, // FIX: potential xss attack
|
subName,
|
||||||
12000
|
12000
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -456,34 +463,35 @@ async function deleteSelectedSubscriptions() {
|
|||||||
const rows = subTable.rows(".selected").data().toArray();
|
const rows = subTable.rows(".selected").data().toArray();
|
||||||
const names = rows.map(row => row.name);
|
const names = rows.map(row => row.name);
|
||||||
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
const namesString = arrayToHtmlList(names, true).prop("outerHTML");
|
||||||
const multiple = names.length > 1;
|
const isMany = names.length > 1;
|
||||||
|
|
||||||
await confirmDeleteModal(
|
await confirmationModal(
|
||||||
`Confirm ${multiple ? "Multiple Deletions" : "Deletion"}`,
|
`Delete ${isMany ? "Many Subscriptions" : "a Subscription"}`,
|
||||||
`Do you wish to permanently delete ${multiple ? "these" : "this"} <b>${names.length}</b> subscription${multiple ? "s" : ""}?<br><br>${namesString}`,
|
`Do you wish to permanently delete ${isMany ? "these" : "this"} <b>${names.length}</b> subscription${isMany ? "s" : ""}?<br><br>${namesString}`,
|
||||||
|
"danger",
|
||||||
async () => {
|
async () => {
|
||||||
rows.forEach(async row => { await deleteSubscription(row.id) });
|
rows.forEach(async row => { await deleteSubscription(row.id) });
|
||||||
|
|
||||||
showToast(
|
showToast(
|
||||||
"danger",
|
"danger",
|
||||||
`Deleted ${names.length} Subscription${multiple ? "s" : ""}`,
|
`Deleted ${names.length} Subscription${isMany ? "s" : ""}`,
|
||||||
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
`${arrayToHtmlList(names, false).prop("outerHTML")}`,
|
||||||
12000
|
12000
|
||||||
)
|
);
|
||||||
|
|
||||||
// Multi-deletion can take time, this timeout ensures the refresh is accurate
|
// Multi-deletion can take time, this timeout ensures the refresh is accurate
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
||||||
}, 600);
|
}, 600);
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// #region Load Modal Options
|
// #region Load Modal Options
|
||||||
|
|
||||||
async function loadChannelOptions(guildId) {
|
async function loadChannelOptions(guildId) {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<div id="confirmDeleteModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
|
<div id="confirmationModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content rounded-1">
|
<div class="modal-content rounded-1">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title mx-2"></h5>
|
<h5 class="modal-title mx-2"></h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body p-4">
|
||||||
<p class="mx-2"></p>
|
<p class="mb-0"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer px-4">
|
<div class="modal-footer px-4">
|
||||||
<button type="button" class="btn btn-danger rounded-1 confirm-delete-btn" tabindex="1">Delete</button>
|
<button type="button" class="btn rounded-1 modal-confirm-btn" tabindex="1">Confirm</button>
|
||||||
<button type="button" class="btn btn-secondary rounded-1 ms-3 ms-0 dismiss-delete-btn" tabindex="2">Cancel</button>
|
<button type="button" class="btn btn-secondary rounded-1 ms-3 ms-0 modal-dismiss-btn" tabindex="2">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user