@developer.krd/discord-dashboard 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -554
- package/dist/index.cjs +569 -711
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -69
- package/dist/index.d.ts +141 -69
- package/dist/index.js +568 -707
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,99 +31,98 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
DashboardDesigner: () => DashboardDesigner,
|
|
34
|
+
DiscordDashboard: () => DiscordDashboard,
|
|
35
|
+
DiscordHelpers: () => DiscordHelpers,
|
|
34
36
|
builtinTemplateRenderers: () => builtinTemplateRenderers,
|
|
35
|
-
createDashboard: () => createDashboard,
|
|
36
|
-
createDashboardDesigner: () => createDashboardDesigner,
|
|
37
|
-
createDiscordHelpers: () => createDiscordHelpers,
|
|
38
37
|
getBuiltinTemplateRenderer: () => getBuiltinTemplateRenderer
|
|
39
38
|
});
|
|
40
39
|
module.exports = __toCommonJS(index_exports);
|
|
41
|
-
|
|
42
|
-
// src/dashboard.ts
|
|
43
40
|
var import_compression = __toESM(require("compression"), 1);
|
|
44
41
|
var import_express = __toESM(require("express"), 1);
|
|
45
42
|
var import_express_session = __toESM(require("express-session"), 1);
|
|
46
43
|
var import_helmet = __toESM(require("helmet"), 1);
|
|
47
|
-
var import_node_http = require("http");
|
|
48
44
|
var import_node_crypto = require("crypto");
|
|
45
|
+
var import_node_http = require("http");
|
|
49
46
|
|
|
50
|
-
// src/
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
// src/handlers/DiscordHelpers.ts
|
|
48
|
+
var DiscordHelpers = class {
|
|
49
|
+
botToken;
|
|
50
|
+
DISCORD_API = "https://discord.com/api/v10";
|
|
51
|
+
constructor(botToken) {
|
|
52
|
+
this.botToken = botToken;
|
|
53
|
+
}
|
|
54
|
+
async fetchDiscordWithBot(path) {
|
|
55
|
+
const response = await fetch(`${this.DISCORD_API}${path}`, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bot ${this.botToken}`
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
return null;
|
|
56
62
|
}
|
|
57
|
-
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
return null;
|
|
63
|
+
return await response.json();
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
75
|
-
return channels.filter((channel) => {
|
|
76
|
-
if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
if (!normalizedQuery) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
return channel.name.toLowerCase().includes(normalizedQuery);
|
|
86
|
-
}).slice(0, limit);
|
|
87
|
-
},
|
|
88
|
-
async getRole(guildId, roleId) {
|
|
89
|
-
const roles = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`);
|
|
90
|
-
if (!roles) {
|
|
91
|
-
return null;
|
|
65
|
+
async getChannel(channelId) {
|
|
66
|
+
return await this.fetchDiscordWithBot(`/channels/${channelId}`);
|
|
67
|
+
}
|
|
68
|
+
async getGuildChannels(guildId) {
|
|
69
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
|
|
70
|
+
}
|
|
71
|
+
async searchGuildChannels(guildId, query, options) {
|
|
72
|
+
const channels = await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
|
|
73
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
74
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
75
|
+
return channels.filter((channel) => {
|
|
76
|
+
if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
|
|
77
|
+
return false;
|
|
92
78
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!normalizedQuery) {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
return role.name.toLowerCase().includes(normalizedQuery);
|
|
110
|
-
}).sort((a, b) => b.position - a.position).slice(0, limit);
|
|
111
|
-
},
|
|
112
|
-
async searchGuildMembers(guildId, query, options) {
|
|
113
|
-
const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
|
|
114
|
-
const params = new URLSearchParams({
|
|
115
|
-
query: query.trim(),
|
|
116
|
-
limit: String(limit)
|
|
117
|
-
});
|
|
118
|
-
return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
|
|
119
|
-
},
|
|
120
|
-
async getGuildMember(guildId, userId) {
|
|
121
|
-
return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/${userId}`);
|
|
79
|
+
if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!normalizedQuery) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return channel.name.toLowerCase().includes(normalizedQuery);
|
|
86
|
+
}).slice(0, limit);
|
|
87
|
+
}
|
|
88
|
+
async getRole(guildId, roleId) {
|
|
89
|
+
const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`);
|
|
90
|
+
if (!roles) {
|
|
91
|
+
return null;
|
|
122
92
|
}
|
|
123
|
-
|
|
124
|
-
}
|
|
93
|
+
return roles.find((role) => role.id === roleId) ?? null;
|
|
94
|
+
}
|
|
95
|
+
async getGuildRoles(guildId) {
|
|
96
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
|
|
97
|
+
}
|
|
98
|
+
async searchGuildRoles(guildId, query, options) {
|
|
99
|
+
const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
|
|
100
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
101
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
102
|
+
return roles.filter((role) => {
|
|
103
|
+
if (!options?.includeManaged && role.managed) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (!normalizedQuery) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return role.name.toLowerCase().includes(normalizedQuery);
|
|
110
|
+
}).sort((a, b) => b.position - a.position).slice(0, limit);
|
|
111
|
+
}
|
|
112
|
+
async searchGuildMembers(guildId, query, options) {
|
|
113
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
|
|
114
|
+
const params = new URLSearchParams({
|
|
115
|
+
query: query.trim(),
|
|
116
|
+
limit: String(limit)
|
|
117
|
+
});
|
|
118
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
|
|
119
|
+
}
|
|
120
|
+
async getGuildMember(guildId, userId) {
|
|
121
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/${userId}`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
125
124
|
|
|
126
|
-
// src/templates.ts
|
|
125
|
+
// src/templates/templates.ts
|
|
127
126
|
var appCss = `
|
|
128
127
|
:root {
|
|
129
128
|
color-scheme: dark;
|
|
@@ -177,7 +176,6 @@ body {
|
|
|
177
176
|
font-weight: 700;
|
|
178
177
|
display: grid;
|
|
179
178
|
place-items: center;
|
|
180
|
-
cursor: pointer;
|
|
181
179
|
transition: border-radius .15s ease, background .15s ease, transform .15s ease;
|
|
182
180
|
}
|
|
183
181
|
.server-item:hover { border-radius: 16px; background: #404249; }
|
|
@@ -292,7 +290,6 @@ button {
|
|
|
292
290
|
color: var(--text);
|
|
293
291
|
border-radius: 8px;
|
|
294
292
|
padding: 8px 12px;
|
|
295
|
-
cursor: pointer;
|
|
296
293
|
}
|
|
297
294
|
button.primary {
|
|
298
295
|
background: var(--primary);
|
|
@@ -448,7 +445,6 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
|
|
|
448
445
|
.drag-handle {
|
|
449
446
|
color: var(--muted);
|
|
450
447
|
user-select: none;
|
|
451
|
-
cursor: grab;
|
|
452
448
|
font-size: 0.9rem;
|
|
453
449
|
}
|
|
454
450
|
.list-input {
|
|
@@ -462,6 +458,7 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
|
|
|
462
458
|
justify-self: start;
|
|
463
459
|
}
|
|
464
460
|
.empty { color: var(--muted); font-size: 0.9rem; }
|
|
461
|
+
.cursor-pointer { cursor: pointer; }
|
|
465
462
|
`;
|
|
466
463
|
function escapeHtml(value) {
|
|
467
464
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -469,13 +466,15 @@ function escapeHtml(value) {
|
|
|
469
466
|
function renderDashboardHtml(name, basePath, setupDesign) {
|
|
470
467
|
const safeName = escapeHtml(name);
|
|
471
468
|
const scriptData = JSON.stringify({ basePath, setupDesign: setupDesign ?? {} });
|
|
469
|
+
const customCssBlock = setupDesign?.customCss ? `
|
|
470
|
+
<style>${setupDesign.customCss}</style>` : "";
|
|
472
471
|
return `<!DOCTYPE html>
|
|
473
472
|
<html lang="en">
|
|
474
473
|
<head>
|
|
475
474
|
<meta charset="UTF-8" />
|
|
476
475
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
477
476
|
<title>${safeName}</title>
|
|
478
|
-
<style>${appCss}</style
|
|
477
|
+
<style>${appCss}</style>${customCssBlock}
|
|
479
478
|
</head>
|
|
480
479
|
<body>
|
|
481
480
|
<div class="layout">
|
|
@@ -492,8 +491,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
492
491
|
|
|
493
492
|
<div class="container">
|
|
494
493
|
<div class="main-tabs">
|
|
495
|
-
<button id="tabHome" class="main-tab active">Home</button>
|
|
496
|
-
<button id="tabPlugins" class="main-tab">Plugins</button>
|
|
494
|
+
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
495
|
+
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
497
496
|
</div>
|
|
498
497
|
|
|
499
498
|
<section id="homeArea">
|
|
@@ -604,7 +603,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
604
603
|
const makeButton = (action, pluginId, panelId, panelElement) => {
|
|
605
604
|
const button = document.createElement("button");
|
|
606
605
|
button.textContent = action.label;
|
|
607
|
-
|
|
606
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
607
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
608
|
+
|
|
608
609
|
button.addEventListener("click", async () => {
|
|
609
610
|
button.disabled = true;
|
|
610
611
|
try {
|
|
@@ -709,7 +710,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
709
710
|
el.serverRail.innerHTML = "";
|
|
710
711
|
items.forEach((item) => {
|
|
711
712
|
const button = document.createElement("button");
|
|
712
|
-
button.className = "server-item" + (item.id === state.selectedGuildId ? " active" : "");
|
|
713
|
+
button.className = "server-item cursor-pointer" + (item.id === state.selectedGuildId ? " active" : "");
|
|
713
714
|
button.title = item.id && !item.botInGuild ? (item.name + " \u2022 Invite bot") : item.name;
|
|
714
715
|
|
|
715
716
|
const activeIndicator = document.createElement("span");
|
|
@@ -746,8 +747,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
746
747
|
const homeActive = state.activeMainTab === "home";
|
|
747
748
|
el.homeArea.style.display = homeActive ? "block" : "none";
|
|
748
749
|
el.pluginsArea.style.display = homeActive ? "none" : "block";
|
|
749
|
-
el.tabHome.className = "main-tab" + (homeActive ? " active" : "");
|
|
750
|
-
el.tabPlugins.className = "main-tab" + (!homeActive ? " active" : "");
|
|
750
|
+
el.tabHome.className = "main-tab cursor-pointer" + (homeActive ? " active" : "");
|
|
751
|
+
el.tabPlugins.className = "main-tab cursor-pointer" + (!homeActive ? " active" : "");
|
|
751
752
|
};
|
|
752
753
|
|
|
753
754
|
const updateContextLabel = () => {
|
|
@@ -769,7 +770,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
769
770
|
el.homeCategories.innerHTML = "";
|
|
770
771
|
state.homeCategories.forEach((category) => {
|
|
771
772
|
const button = document.createElement("button");
|
|
772
|
-
button.className = "home-category-btn" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
773
|
+
button.className = "home-category-btn cursor-pointer" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
773
774
|
button.textContent = category.label;
|
|
774
775
|
button.title = category.description || category.label;
|
|
775
776
|
button.addEventListener("click", async () => {
|
|
@@ -830,7 +831,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
830
831
|
itemsWrap.className = "list-items";
|
|
831
832
|
const addButton = document.createElement("button");
|
|
832
833
|
addButton.type = "button";
|
|
833
|
-
addButton.className = "list-add";
|
|
834
|
+
addButton.className = "list-add cursor-pointer";
|
|
834
835
|
addButton.textContent = "Add Button";
|
|
835
836
|
|
|
836
837
|
const normalizeValues = () => {
|
|
@@ -846,7 +847,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
846
847
|
row.draggable = true;
|
|
847
848
|
|
|
848
849
|
const handle = document.createElement("span");
|
|
849
|
-
handle.className = "drag-handle";
|
|
850
|
+
handle.className = "drag-handle cursor-pointer";
|
|
850
851
|
handle.textContent = "\u22EE\u22EE";
|
|
851
852
|
|
|
852
853
|
const textInput = document.createElement("input");
|
|
@@ -858,6 +859,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
858
859
|
|
|
859
860
|
const removeButton = document.createElement("button");
|
|
860
861
|
removeButton.type = "button";
|
|
862
|
+
removeButton.className = "cursor-pointer";
|
|
861
863
|
removeButton.textContent = "\xD7";
|
|
862
864
|
removeButton.addEventListener("click", () => {
|
|
863
865
|
row.remove();
|
|
@@ -935,7 +937,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
935
937
|
items.forEach((item) => {
|
|
936
938
|
const btn = document.createElement("button");
|
|
937
939
|
btn.type = "button";
|
|
938
|
-
btn.className = "lookup-item";
|
|
940
|
+
btn.className = "lookup-item cursor-pointer";
|
|
939
941
|
btn.textContent = labelResolver(item);
|
|
940
942
|
btn.addEventListener("click", () => {
|
|
941
943
|
onSelect(item);
|
|
@@ -1091,7 +1093,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1091
1093
|
input.value = field.value == null ? "" : String(field.value);
|
|
1092
1094
|
} else if (field.type === "select") {
|
|
1093
1095
|
input = document.createElement("select");
|
|
1094
|
-
input.className = "home-select";
|
|
1096
|
+
input.className = "home-select cursor-pointer";
|
|
1095
1097
|
(field.options || []).forEach((option) => {
|
|
1096
1098
|
const optionEl = document.createElement("option");
|
|
1097
1099
|
optionEl.value = option.value;
|
|
@@ -1106,7 +1108,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1106
1108
|
row.className = "home-field-row";
|
|
1107
1109
|
input = document.createElement("input");
|
|
1108
1110
|
input.type = "checkbox";
|
|
1109
|
-
input.className = "home-checkbox";
|
|
1111
|
+
input.className = "home-checkbox cursor-pointer";
|
|
1110
1112
|
input.checked = Boolean(field.value);
|
|
1111
1113
|
const stateText = document.createElement("span");
|
|
1112
1114
|
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
@@ -1161,7 +1163,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1161
1163
|
section.actions.forEach((action) => {
|
|
1162
1164
|
const button = document.createElement("button");
|
|
1163
1165
|
button.textContent = action.label;
|
|
1164
|
-
|
|
1166
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
1167
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
1168
|
+
|
|
1165
1169
|
button.addEventListener("click", async () => {
|
|
1166
1170
|
button.disabled = true;
|
|
1167
1171
|
try {
|
|
@@ -1275,7 +1279,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1275
1279
|
input.value = field.value == null ? "" : String(field.value);
|
|
1276
1280
|
} else if (field.type === "select") {
|
|
1277
1281
|
input = document.createElement("select");
|
|
1278
|
-
input.className = "home-select";
|
|
1282
|
+
input.className = "home-select cursor-pointer";
|
|
1279
1283
|
(field.options || []).forEach((option) => {
|
|
1280
1284
|
const optionEl = document.createElement("option");
|
|
1281
1285
|
optionEl.value = option.value;
|
|
@@ -1290,7 +1294,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1290
1294
|
row.className = "home-field-row";
|
|
1291
1295
|
input = document.createElement("input");
|
|
1292
1296
|
input.type = "checkbox";
|
|
1293
|
-
input.className = "home-checkbox";
|
|
1297
|
+
input.className = "home-checkbox cursor-pointer";
|
|
1294
1298
|
input.checked = Boolean(field.value);
|
|
1295
1299
|
const stateText = document.createElement("span");
|
|
1296
1300
|
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
@@ -1500,7 +1504,6 @@ body {
|
|
|
1500
1504
|
background: var(--panel);
|
|
1501
1505
|
color: #fff;
|
|
1502
1506
|
font-weight: 700;
|
|
1503
|
-
cursor: pointer;
|
|
1504
1507
|
transition: transform .15s ease, background .15s ease;
|
|
1505
1508
|
}
|
|
1506
1509
|
.server-item:hover { transform: translateY(-1px); background: #323b5f; }
|
|
@@ -1551,7 +1554,6 @@ button {
|
|
|
1551
1554
|
color: var(--text);
|
|
1552
1555
|
border-radius: 8px;
|
|
1553
1556
|
padding: 7px 10px;
|
|
1554
|
-
cursor: pointer;
|
|
1555
1557
|
}
|
|
1556
1558
|
button.primary { background: var(--primary); border: none; }
|
|
1557
1559
|
button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
@@ -1642,29 +1644,28 @@ button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
|
1642
1644
|
background: var(--panel);
|
|
1643
1645
|
}
|
|
1644
1646
|
.list-item.dragging { opacity: .6; }
|
|
1645
|
-
.drag-handle { color: var(--muted); user-select: none;
|
|
1647
|
+
.drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
|
|
1646
1648
|
.list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
|
|
1647
1649
|
.list-add { justify-self: start; }
|
|
1648
1650
|
.empty { color: var(--muted); font-size: .9rem; }
|
|
1651
|
+
.cursor-pointer { cursor: pointer; }
|
|
1649
1652
|
@media (max-width: 980px) {
|
|
1650
1653
|
.layout { grid-template-columns: 70px 1fr; }
|
|
1651
1654
|
.home-width-50, .home-width-33, .home-width-20 { flex-basis: 100%; max-width: 100%; }
|
|
1652
1655
|
}
|
|
1653
1656
|
`;
|
|
1654
|
-
var compactDashboardTemplateRenderer = ({
|
|
1655
|
-
dashboardName,
|
|
1656
|
-
basePath,
|
|
1657
|
-
setupDesign
|
|
1658
|
-
}) => {
|
|
1657
|
+
var compactDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => {
|
|
1659
1658
|
const script = extractDashboardScript(renderDashboardHtml(dashboardName, basePath, setupDesign));
|
|
1660
1659
|
const safeName = escapeHtml2(dashboardName);
|
|
1660
|
+
const customCssBlock = setupDesign?.customCss ? `
|
|
1661
|
+
<style>${setupDesign.customCss}</style>` : "";
|
|
1661
1662
|
return `<!DOCTYPE html>
|
|
1662
1663
|
<html lang="en">
|
|
1663
1664
|
<head>
|
|
1664
1665
|
<meta charset="UTF-8" />
|
|
1665
1666
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1666
1667
|
<title>${safeName}</title>
|
|
1667
|
-
<style>${compactCss}</style
|
|
1668
|
+
<style>${compactCss}</style>${customCssBlock}
|
|
1668
1669
|
</head>
|
|
1669
1670
|
<body>
|
|
1670
1671
|
<div class="shell">
|
|
@@ -1682,8 +1683,8 @@ var compactDashboardTemplateRenderer = ({
|
|
|
1682
1683
|
<main class="content">
|
|
1683
1684
|
<div class="container">
|
|
1684
1685
|
<div class="main-tabs">
|
|
1685
|
-
<button id="tabHome" class="main-tab active">Home</button>
|
|
1686
|
-
<button id="tabPlugins" class="main-tab">Plugins</button>
|
|
1686
|
+
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
1687
|
+
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
1687
1688
|
</div>
|
|
1688
1689
|
|
|
1689
1690
|
<section id="homeArea">
|
|
@@ -1712,11 +1713,7 @@ var compactDashboardTemplateRenderer = ({
|
|
|
1712
1713
|
};
|
|
1713
1714
|
|
|
1714
1715
|
// src/templates/default.ts
|
|
1715
|
-
var defaultDashboardTemplateRenderer = ({
|
|
1716
|
-
dashboardName,
|
|
1717
|
-
basePath,
|
|
1718
|
-
setupDesign
|
|
1719
|
-
}) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1716
|
+
var defaultDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1720
1717
|
|
|
1721
1718
|
// src/templates/index.ts
|
|
1722
1719
|
var builtinTemplateRenderers = {
|
|
@@ -1727,565 +1724,7 @@ function getBuiltinTemplateRenderer(templateId) {
|
|
|
1727
1724
|
return builtinTemplateRenderers[templateId];
|
|
1728
1725
|
}
|
|
1729
1726
|
|
|
1730
|
-
// src/
|
|
1731
|
-
var DISCORD_API2 = "https://discord.com/api/v10";
|
|
1732
|
-
var MANAGE_GUILD_PERMISSION = 0x20n;
|
|
1733
|
-
var ADMIN_PERMISSION = 0x8n;
|
|
1734
|
-
function normalizeBasePath(basePath) {
|
|
1735
|
-
if (!basePath || basePath === "/") {
|
|
1736
|
-
return "/dashboard";
|
|
1737
|
-
}
|
|
1738
|
-
return basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
1739
|
-
}
|
|
1740
|
-
function canManageGuild(permissions) {
|
|
1741
|
-
const value = BigInt(permissions);
|
|
1742
|
-
return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
|
|
1743
|
-
}
|
|
1744
|
-
function toQuery(params) {
|
|
1745
|
-
const url = new URLSearchParams();
|
|
1746
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1747
|
-
url.set(key, value);
|
|
1748
|
-
}
|
|
1749
|
-
return url.toString();
|
|
1750
|
-
}
|
|
1751
|
-
async function fetchDiscord(path, token) {
|
|
1752
|
-
const response = await fetch(`${DISCORD_API2}${path}`, {
|
|
1753
|
-
headers: {
|
|
1754
|
-
Authorization: `Bearer ${token}`
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
1757
|
-
if (!response.ok) {
|
|
1758
|
-
throw new Error(`Discord API request failed (${response.status})`);
|
|
1759
|
-
}
|
|
1760
|
-
return await response.json();
|
|
1761
|
-
}
|
|
1762
|
-
async function exchangeCodeForToken(options, code) {
|
|
1763
|
-
const response = await fetch(`${DISCORD_API2}/oauth2/token`, {
|
|
1764
|
-
method: "POST",
|
|
1765
|
-
headers: {
|
|
1766
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1767
|
-
},
|
|
1768
|
-
body: toQuery({
|
|
1769
|
-
client_id: options.clientId,
|
|
1770
|
-
client_secret: options.clientSecret,
|
|
1771
|
-
grant_type: "authorization_code",
|
|
1772
|
-
code,
|
|
1773
|
-
redirect_uri: options.redirectUri
|
|
1774
|
-
})
|
|
1775
|
-
});
|
|
1776
|
-
if (!response.ok) {
|
|
1777
|
-
const text = await response.text();
|
|
1778
|
-
throw new Error(`Failed token exchange: ${response.status} ${text}`);
|
|
1779
|
-
}
|
|
1780
|
-
return await response.json();
|
|
1781
|
-
}
|
|
1782
|
-
function createContext(req, options) {
|
|
1783
|
-
const auth = req.session.discordAuth;
|
|
1784
|
-
if (!auth) {
|
|
1785
|
-
throw new Error("Not authenticated");
|
|
1786
|
-
}
|
|
1787
|
-
const selectedGuildId = typeof req.query.guildId === "string" ? req.query.guildId : void 0;
|
|
1788
|
-
return {
|
|
1789
|
-
user: auth.user,
|
|
1790
|
-
guilds: auth.guilds,
|
|
1791
|
-
accessToken: auth.accessToken,
|
|
1792
|
-
selectedGuildId,
|
|
1793
|
-
helpers: createDiscordHelpers(options.botToken)
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
function ensureAuthenticated(req, res, next) {
|
|
1797
|
-
if (!req.session.discordAuth) {
|
|
1798
|
-
res.status(401).json({ authenticated: false, message: "Authentication required" });
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
next();
|
|
1802
|
-
}
|
|
1803
|
-
async function resolveOverviewCards(options, context) {
|
|
1804
|
-
if (options.getOverviewCards) {
|
|
1805
|
-
return await options.getOverviewCards(context);
|
|
1806
|
-
}
|
|
1807
|
-
const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
1808
|
-
return [
|
|
1809
|
-
{
|
|
1810
|
-
id: "user",
|
|
1811
|
-
title: "Logged-in User",
|
|
1812
|
-
value: context.user.global_name || context.user.username,
|
|
1813
|
-
subtitle: `ID: ${context.user.id}`,
|
|
1814
|
-
intent: "info"
|
|
1815
|
-
},
|
|
1816
|
-
{
|
|
1817
|
-
id: "guilds",
|
|
1818
|
-
title: "Manageable Guilds",
|
|
1819
|
-
value: manageableGuildCount,
|
|
1820
|
-
subtitle: "Owner or Manage Server permissions",
|
|
1821
|
-
intent: "success"
|
|
1822
|
-
},
|
|
1823
|
-
{
|
|
1824
|
-
id: "plugins",
|
|
1825
|
-
title: "Plugins Loaded",
|
|
1826
|
-
value: options.plugins?.length ?? 0,
|
|
1827
|
-
subtitle: "Dynamic server modules",
|
|
1828
|
-
intent: "neutral"
|
|
1829
|
-
}
|
|
1830
|
-
];
|
|
1831
|
-
}
|
|
1832
|
-
async function resolveHomeSections(options, context) {
|
|
1833
|
-
const customSections = options.home?.getSections ? await options.home.getSections(context) : [];
|
|
1834
|
-
const overviewSections = options.home?.getOverviewSections ? await options.home.getOverviewSections(context) : [];
|
|
1835
|
-
if (customSections.length > 0 || overviewSections.length > 0) {
|
|
1836
|
-
const normalizedOverview = overviewSections.map((section) => ({
|
|
1837
|
-
...section,
|
|
1838
|
-
categoryId: section.categoryId ?? "overview"
|
|
1839
|
-
}));
|
|
1840
|
-
return [...normalizedOverview, ...customSections];
|
|
1841
|
-
}
|
|
1842
|
-
const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
|
|
1843
|
-
return [
|
|
1844
|
-
{
|
|
1845
|
-
id: "setup",
|
|
1846
|
-
title: "Setup Details",
|
|
1847
|
-
description: "Core dashboard setup information",
|
|
1848
|
-
scope: "setup",
|
|
1849
|
-
categoryId: "setup",
|
|
1850
|
-
fields: [
|
|
1851
|
-
{
|
|
1852
|
-
id: "dashboardName",
|
|
1853
|
-
label: "Dashboard Name",
|
|
1854
|
-
type: "text",
|
|
1855
|
-
value: options.dashboardName ?? "Discord Dashboard",
|
|
1856
|
-
readOnly: true
|
|
1857
|
-
},
|
|
1858
|
-
{
|
|
1859
|
-
id: "basePath",
|
|
1860
|
-
label: "Base Path",
|
|
1861
|
-
type: "text",
|
|
1862
|
-
value: options.basePath ?? "/dashboard",
|
|
1863
|
-
readOnly: true
|
|
1864
|
-
}
|
|
1865
|
-
]
|
|
1866
|
-
},
|
|
1867
|
-
{
|
|
1868
|
-
id: "context",
|
|
1869
|
-
title: "Dashboard Context",
|
|
1870
|
-
description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
|
|
1871
|
-
scope: resolveScope(context),
|
|
1872
|
-
categoryId: "overview",
|
|
1873
|
-
fields: [
|
|
1874
|
-
{
|
|
1875
|
-
id: "mode",
|
|
1876
|
-
label: "Mode",
|
|
1877
|
-
type: "text",
|
|
1878
|
-
value: selectedGuild ? "Guild" : "User",
|
|
1879
|
-
readOnly: true
|
|
1880
|
-
},
|
|
1881
|
-
{
|
|
1882
|
-
id: "target",
|
|
1883
|
-
label: "Target",
|
|
1884
|
-
type: "text",
|
|
1885
|
-
value: selectedGuild ? selectedGuild.name : context.user.username,
|
|
1886
|
-
readOnly: true
|
|
1887
|
-
}
|
|
1888
|
-
]
|
|
1889
|
-
}
|
|
1890
|
-
];
|
|
1891
|
-
}
|
|
1892
|
-
function resolveScope(context) {
|
|
1893
|
-
return context.selectedGuildId ? "guild" : "user";
|
|
1894
|
-
}
|
|
1895
|
-
async function resolveHomeCategories(options, context) {
|
|
1896
|
-
if (options.home?.getCategories) {
|
|
1897
|
-
const categories = await options.home.getCategories(context);
|
|
1898
|
-
return [...categories].sort((a, b) => {
|
|
1899
|
-
if (a.id === "overview") return -1;
|
|
1900
|
-
if (b.id === "overview") return 1;
|
|
1901
|
-
return 0;
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
return [
|
|
1905
|
-
{ id: "overview", label: "Overview", scope: resolveScope(context) },
|
|
1906
|
-
{ id: "setup", label: "Setup", scope: "setup" }
|
|
1907
|
-
];
|
|
1908
|
-
}
|
|
1909
|
-
function getUserAvatarUrl(user) {
|
|
1910
|
-
if (user.avatar) {
|
|
1911
|
-
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
|
1912
|
-
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
|
|
1913
|
-
}
|
|
1914
|
-
const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
|
|
1915
|
-
return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
|
|
1916
|
-
}
|
|
1917
|
-
function getGuildIconUrl(guild) {
|
|
1918
|
-
if (!guild.icon) {
|
|
1919
|
-
return null;
|
|
1920
|
-
}
|
|
1921
|
-
const ext = guild.icon.startsWith("a_") ? "gif" : "png";
|
|
1922
|
-
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
|
|
1923
|
-
}
|
|
1924
|
-
function createGuildInviteUrl(options, guildId) {
|
|
1925
|
-
const scopes = options.botInviteScopes && options.botInviteScopes.length > 0 ? options.botInviteScopes : ["bot", "applications.commands"];
|
|
1926
|
-
return `https://discord.com/oauth2/authorize?${toQuery({
|
|
1927
|
-
client_id: options.clientId,
|
|
1928
|
-
scope: scopes.join(" "),
|
|
1929
|
-
permissions: options.botInvitePermissions ?? "8",
|
|
1930
|
-
guild_id: guildId,
|
|
1931
|
-
disable_guild_select: "true"
|
|
1932
|
-
})}`;
|
|
1933
|
-
}
|
|
1934
|
-
async function fetchBotGuildIds(botToken) {
|
|
1935
|
-
const response = await fetch(`${DISCORD_API2}/users/@me/guilds`, {
|
|
1936
|
-
headers: {
|
|
1937
|
-
Authorization: `Bot ${botToken}`
|
|
1938
|
-
}
|
|
1939
|
-
});
|
|
1940
|
-
if (!response.ok) {
|
|
1941
|
-
return /* @__PURE__ */ new Set();
|
|
1942
|
-
}
|
|
1943
|
-
const guilds = await response.json();
|
|
1944
|
-
return new Set(guilds.map((guild) => guild.id));
|
|
1945
|
-
}
|
|
1946
|
-
function resolveTemplateRenderer(options) {
|
|
1947
|
-
const selectedTemplate = options.uiTemplate ?? "default";
|
|
1948
|
-
const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1949
|
-
const customRenderer = options.uiTemplates?.[selectedTemplate];
|
|
1950
|
-
if (customRenderer) {
|
|
1951
|
-
return customRenderer;
|
|
1952
|
-
}
|
|
1953
|
-
const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
|
|
1954
|
-
if (builtinRenderer) {
|
|
1955
|
-
return builtinRenderer;
|
|
1956
|
-
}
|
|
1957
|
-
if (selectedTemplate !== "default") {
|
|
1958
|
-
throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
|
|
1959
|
-
}
|
|
1960
|
-
return defaultRenderer;
|
|
1961
|
-
}
|
|
1962
|
-
function createDashboard(options) {
|
|
1963
|
-
const app = options.app ?? (0, import_express.default)();
|
|
1964
|
-
const basePath = normalizeBasePath(options.basePath);
|
|
1965
|
-
const dashboardName = options.dashboardName ?? "Discord Dashboard";
|
|
1966
|
-
const templateRenderer = resolveTemplateRenderer(options);
|
|
1967
|
-
const plugins = options.plugins ?? [];
|
|
1968
|
-
if (!options.botToken) throw new Error("botToken is required");
|
|
1969
|
-
if (!options.clientId) throw new Error("clientId is required");
|
|
1970
|
-
if (!options.clientSecret) throw new Error("clientSecret is required");
|
|
1971
|
-
if (!options.redirectUri) throw new Error("redirectUri is required");
|
|
1972
|
-
if (!options.sessionSecret) throw new Error("sessionSecret is required");
|
|
1973
|
-
if (!options.app && options.trustProxy !== void 0) {
|
|
1974
|
-
app.set("trust proxy", options.trustProxy);
|
|
1975
|
-
}
|
|
1976
|
-
const router = import_express.default.Router();
|
|
1977
|
-
const sessionMiddleware = (0, import_express_session.default)({
|
|
1978
|
-
name: options.sessionName ?? "discord_dashboard.sid",
|
|
1979
|
-
secret: options.sessionSecret,
|
|
1980
|
-
resave: false,
|
|
1981
|
-
saveUninitialized: false,
|
|
1982
|
-
cookie: {
|
|
1983
|
-
httpOnly: true,
|
|
1984
|
-
sameSite: "lax",
|
|
1985
|
-
maxAge: options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
router.use((0, import_compression.default)());
|
|
1989
|
-
router.use(
|
|
1990
|
-
(0, import_helmet.default)({
|
|
1991
|
-
contentSecurityPolicy: false
|
|
1992
|
-
})
|
|
1993
|
-
);
|
|
1994
|
-
router.use(import_express.default.json());
|
|
1995
|
-
router.use(sessionMiddleware);
|
|
1996
|
-
router.get("/", (req, res) => {
|
|
1997
|
-
if (!req.session.discordAuth) {
|
|
1998
|
-
res.redirect(`${basePath}/login`);
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
|
-
res.setHeader("Cache-Control", "no-store");
|
|
2002
|
-
res.type("html").send(templateRenderer({
|
|
2003
|
-
dashboardName,
|
|
2004
|
-
basePath,
|
|
2005
|
-
setupDesign: options.setupDesign
|
|
2006
|
-
}));
|
|
2007
|
-
});
|
|
2008
|
-
router.get("/login", (req, res) => {
|
|
2009
|
-
const state = (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
2010
|
-
req.session.oauthState = state;
|
|
2011
|
-
const scope = (options.scopes && options.scopes.length > 0 ? options.scopes : ["identify", "guilds"]).join(" ");
|
|
2012
|
-
const query = toQuery({
|
|
2013
|
-
client_id: options.clientId,
|
|
2014
|
-
redirect_uri: options.redirectUri,
|
|
2015
|
-
response_type: "code",
|
|
2016
|
-
scope,
|
|
2017
|
-
state,
|
|
2018
|
-
prompt: "none"
|
|
2019
|
-
});
|
|
2020
|
-
res.redirect(`https://discord.com/oauth2/authorize?${query}`);
|
|
2021
|
-
});
|
|
2022
|
-
router.get("/callback", async (req, res) => {
|
|
2023
|
-
try {
|
|
2024
|
-
const code = typeof req.query.code === "string" ? req.query.code : void 0;
|
|
2025
|
-
const state = typeof req.query.state === "string" ? req.query.state : void 0;
|
|
2026
|
-
if (!code || !state) {
|
|
2027
|
-
res.status(400).send("Missing OAuth2 code/state");
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
if (!req.session.oauthState || req.session.oauthState !== state) {
|
|
2031
|
-
res.status(403).send("Invalid OAuth2 state");
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
const tokenData = await exchangeCodeForToken(options, code);
|
|
2035
|
-
const [user, guilds] = await Promise.all([
|
|
2036
|
-
fetchDiscord("/users/@me", tokenData.access_token),
|
|
2037
|
-
fetchDiscord("/users/@me/guilds", tokenData.access_token)
|
|
2038
|
-
]);
|
|
2039
|
-
req.session.discordAuth = {
|
|
2040
|
-
accessToken: tokenData.access_token,
|
|
2041
|
-
refreshToken: tokenData.refresh_token,
|
|
2042
|
-
expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0,
|
|
2043
|
-
user,
|
|
2044
|
-
guilds
|
|
2045
|
-
};
|
|
2046
|
-
req.session.oauthState = void 0;
|
|
2047
|
-
res.redirect(basePath);
|
|
2048
|
-
} catch (error) {
|
|
2049
|
-
const message = error instanceof Error ? error.message : "OAuth callback failed";
|
|
2050
|
-
res.status(500).send(message);
|
|
2051
|
-
}
|
|
2052
|
-
});
|
|
2053
|
-
router.post("/logout", (req, res) => {
|
|
2054
|
-
req.session.destroy((sessionError) => {
|
|
2055
|
-
if (sessionError) {
|
|
2056
|
-
res.status(500).json({ ok: false, message: "Failed to destroy session" });
|
|
2057
|
-
return;
|
|
2058
|
-
}
|
|
2059
|
-
res.clearCookie(options.sessionName ?? "discord_dashboard.sid");
|
|
2060
|
-
res.json({ ok: true });
|
|
2061
|
-
});
|
|
2062
|
-
});
|
|
2063
|
-
router.get("/api/session", (req, res) => {
|
|
2064
|
-
const auth = req.session.discordAuth;
|
|
2065
|
-
if (!auth) {
|
|
2066
|
-
res.status(200).json({ authenticated: false });
|
|
2067
|
-
return;
|
|
2068
|
-
}
|
|
2069
|
-
const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2070
|
-
res.json({
|
|
2071
|
-
authenticated: true,
|
|
2072
|
-
user: {
|
|
2073
|
-
...auth.user,
|
|
2074
|
-
avatarUrl: getUserAvatarUrl(auth.user)
|
|
2075
|
-
},
|
|
2076
|
-
guildCount: manageableGuildCount,
|
|
2077
|
-
expiresAt: auth.expiresAt
|
|
2078
|
-
});
|
|
2079
|
-
});
|
|
2080
|
-
router.get("/api/guilds", ensureAuthenticated, async (req, res) => {
|
|
2081
|
-
const context = createContext(req, options);
|
|
2082
|
-
if (options.ownerIds && options.ownerIds.length > 0 && !options.ownerIds.includes(context.user.id)) {
|
|
2083
|
-
res.status(403).json({ message: "You are not allowed to access this dashboard." });
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
|
|
2087
|
-
if (options.guildFilter) {
|
|
2088
|
-
const filtered = [];
|
|
2089
|
-
for (const guild of manageableGuilds) {
|
|
2090
|
-
const allowed = await options.guildFilter(guild, context);
|
|
2091
|
-
if (allowed) {
|
|
2092
|
-
filtered.push(guild);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
manageableGuilds = filtered;
|
|
2096
|
-
}
|
|
2097
|
-
const botGuildIds = await fetchBotGuildIds(options.botToken);
|
|
2098
|
-
const enrichedGuilds = manageableGuilds.map((guild) => {
|
|
2099
|
-
const botInGuild = botGuildIds.has(guild.id);
|
|
2100
|
-
return {
|
|
2101
|
-
...guild,
|
|
2102
|
-
iconUrl: getGuildIconUrl(guild),
|
|
2103
|
-
botInGuild,
|
|
2104
|
-
inviteUrl: botInGuild ? void 0 : createGuildInviteUrl(options, guild.id)
|
|
2105
|
-
};
|
|
2106
|
-
});
|
|
2107
|
-
res.json({ guilds: enrichedGuilds });
|
|
2108
|
-
});
|
|
2109
|
-
router.get("/api/overview", ensureAuthenticated, async (req, res) => {
|
|
2110
|
-
const context = createContext(req, options);
|
|
2111
|
-
const cards = await resolveOverviewCards(options, context);
|
|
2112
|
-
res.json({ cards });
|
|
2113
|
-
});
|
|
2114
|
-
router.get("/api/home/categories", ensureAuthenticated, async (req, res) => {
|
|
2115
|
-
const context = createContext(req, options);
|
|
2116
|
-
const activeScope = resolveScope(context);
|
|
2117
|
-
const categories = await resolveHomeCategories(options, context);
|
|
2118
|
-
const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
|
|
2119
|
-
res.json({ categories: visible, activeScope });
|
|
2120
|
-
});
|
|
2121
|
-
router.get("/api/home", ensureAuthenticated, async (req, res) => {
|
|
2122
|
-
const context = createContext(req, options);
|
|
2123
|
-
const activeScope = resolveScope(context);
|
|
2124
|
-
const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
|
|
2125
|
-
let sections = await resolveHomeSections(options, context);
|
|
2126
|
-
sections = sections.filter((section) => {
|
|
2127
|
-
const sectionScope = section.scope ?? activeScope;
|
|
2128
|
-
if (sectionScope !== "setup" && sectionScope !== activeScope) {
|
|
2129
|
-
return false;
|
|
2130
|
-
}
|
|
2131
|
-
if (!categoryId) {
|
|
2132
|
-
return true;
|
|
2133
|
-
}
|
|
2134
|
-
return section.categoryId === categoryId;
|
|
2135
|
-
});
|
|
2136
|
-
res.json({ sections, activeScope });
|
|
2137
|
-
});
|
|
2138
|
-
router.get("/api/lookup/roles", ensureAuthenticated, async (req, res) => {
|
|
2139
|
-
const context = createContext(req, options);
|
|
2140
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2141
|
-
if (!guildId) {
|
|
2142
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2146
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2147
|
-
const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
|
|
2148
|
-
const roles = await context.helpers.searchGuildRoles(guildId, query, {
|
|
2149
|
-
limit: Number.isFinite(limit) ? limit : void 0,
|
|
2150
|
-
includeManaged
|
|
2151
|
-
});
|
|
2152
|
-
res.json({ roles });
|
|
2153
|
-
});
|
|
2154
|
-
router.get("/api/lookup/channels", ensureAuthenticated, async (req, res) => {
|
|
2155
|
-
const context = createContext(req, options);
|
|
2156
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2157
|
-
if (!guildId) {
|
|
2158
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2159
|
-
return;
|
|
2160
|
-
}
|
|
2161
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2162
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2163
|
-
const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
|
|
2164
|
-
const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
|
|
2165
|
-
const channels = await context.helpers.searchGuildChannels(guildId, query, {
|
|
2166
|
-
limit: Number.isFinite(limit) ? limit : void 0,
|
|
2167
|
-
nsfw,
|
|
2168
|
-
channelTypes
|
|
2169
|
-
});
|
|
2170
|
-
res.json({ channels });
|
|
2171
|
-
});
|
|
2172
|
-
router.get("/api/lookup/members", ensureAuthenticated, async (req, res) => {
|
|
2173
|
-
const context = createContext(req, options);
|
|
2174
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2175
|
-
if (!guildId) {
|
|
2176
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2180
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2181
|
-
const members = await context.helpers.searchGuildMembers(guildId, query, {
|
|
2182
|
-
limit: Number.isFinite(limit) ? limit : void 0
|
|
2183
|
-
});
|
|
2184
|
-
res.json({ members });
|
|
2185
|
-
});
|
|
2186
|
-
router.post("/api/home/:actionId", ensureAuthenticated, async (req, res) => {
|
|
2187
|
-
const context = createContext(req, options);
|
|
2188
|
-
const action = options.home?.actions?.[req.params.actionId];
|
|
2189
|
-
if (!action) {
|
|
2190
|
-
res.status(404).json({ ok: false, message: "Home action not found" });
|
|
2191
|
-
return;
|
|
2192
|
-
}
|
|
2193
|
-
const payload = req.body;
|
|
2194
|
-
if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
|
|
2195
|
-
res.status(400).json({ ok: false, message: "Invalid home action payload" });
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
let result;
|
|
2199
|
-
try {
|
|
2200
|
-
result = await action(context, {
|
|
2201
|
-
sectionId: payload.sectionId,
|
|
2202
|
-
values: payload.values
|
|
2203
|
-
});
|
|
2204
|
-
} catch (error) {
|
|
2205
|
-
const message = error instanceof Error ? error.message : "Home action failed";
|
|
2206
|
-
res.status(500).json({ ok: false, message });
|
|
2207
|
-
return;
|
|
2208
|
-
}
|
|
2209
|
-
res.json(result);
|
|
2210
|
-
});
|
|
2211
|
-
router.get("/api/plugins", ensureAuthenticated, async (req, res) => {
|
|
2212
|
-
const context = createContext(req, options);
|
|
2213
|
-
const activeScope = context.selectedGuildId ? "guild" : "user";
|
|
2214
|
-
const payload = [];
|
|
2215
|
-
for (const plugin of plugins) {
|
|
2216
|
-
const pluginScope = plugin.scope ?? "both";
|
|
2217
|
-
if (pluginScope !== "both" && pluginScope !== activeScope) {
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
const panels = await plugin.getPanels(context);
|
|
2221
|
-
payload.push({
|
|
2222
|
-
id: plugin.id,
|
|
2223
|
-
name: plugin.name,
|
|
2224
|
-
description: plugin.description,
|
|
2225
|
-
panels
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
res.json({ plugins: payload });
|
|
2229
|
-
});
|
|
2230
|
-
router.post("/api/plugins/:pluginId/:actionId", ensureAuthenticated, async (req, res) => {
|
|
2231
|
-
const context = createContext(req, options);
|
|
2232
|
-
const plugin = plugins.find((item) => item.id === req.params.pluginId);
|
|
2233
|
-
if (!plugin) {
|
|
2234
|
-
res.status(404).json({ ok: false, message: "Plugin not found" });
|
|
2235
|
-
return;
|
|
2236
|
-
}
|
|
2237
|
-
const action = plugin.actions?.[req.params.actionId];
|
|
2238
|
-
if (!action) {
|
|
2239
|
-
res.status(404).json({ ok: false, message: "Action not found" });
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2242
|
-
let result;
|
|
2243
|
-
try {
|
|
2244
|
-
result = await action(context, req.body);
|
|
2245
|
-
} catch (error) {
|
|
2246
|
-
const message = error instanceof Error ? error.message : "Plugin action failed";
|
|
2247
|
-
res.status(500).json({ ok: false, message });
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
res.json(result);
|
|
2251
|
-
});
|
|
2252
|
-
app.use(basePath, router);
|
|
2253
|
-
let server;
|
|
2254
|
-
return {
|
|
2255
|
-
app,
|
|
2256
|
-
async start() {
|
|
2257
|
-
if (options.app) {
|
|
2258
|
-
return;
|
|
2259
|
-
}
|
|
2260
|
-
if (server) {
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
const port = options.port ?? 3e3;
|
|
2264
|
-
const host = options.host ?? "0.0.0.0";
|
|
2265
|
-
server = (0, import_node_http.createServer)(app);
|
|
2266
|
-
await new Promise((resolve) => {
|
|
2267
|
-
server.listen(port, host, () => resolve());
|
|
2268
|
-
});
|
|
2269
|
-
},
|
|
2270
|
-
async stop() {
|
|
2271
|
-
if (!server) {
|
|
2272
|
-
return;
|
|
2273
|
-
}
|
|
2274
|
-
await new Promise((resolve, reject) => {
|
|
2275
|
-
server.close((error) => {
|
|
2276
|
-
if (error) {
|
|
2277
|
-
reject(error);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
resolve();
|
|
2281
|
-
});
|
|
2282
|
-
});
|
|
2283
|
-
server = void 0;
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// src/designer.ts
|
|
1727
|
+
// src/handlers/DashboardDesigner.ts
|
|
2289
1728
|
var CategoryBuilder = class {
|
|
2290
1729
|
constructor(scope, categoryId, categoryLabel) {
|
|
2291
1730
|
this.scope = scope;
|
|
@@ -2307,11 +1746,7 @@ var CategoryBuilder = class {
|
|
|
2307
1746
|
return this;
|
|
2308
1747
|
}
|
|
2309
1748
|
buildCategory() {
|
|
2310
|
-
return {
|
|
2311
|
-
id: this.categoryId,
|
|
2312
|
-
label: this.categoryLabel,
|
|
2313
|
-
scope: this.scope
|
|
2314
|
-
};
|
|
1749
|
+
return { id: this.categoryId, label: this.categoryLabel, scope: this.scope };
|
|
2315
1750
|
}
|
|
2316
1751
|
buildSections() {
|
|
2317
1752
|
return [...this.sections];
|
|
@@ -2328,12 +1763,7 @@ var DashboardDesigner = class {
|
|
|
2328
1763
|
this.partialOptions = { ...baseOptions };
|
|
2329
1764
|
}
|
|
2330
1765
|
setup(input) {
|
|
2331
|
-
|
|
2332
|
-
if (input.botInvitePermissions) this.partialOptions.botInvitePermissions = input.botInvitePermissions;
|
|
2333
|
-
if (input.botInviteScopes) this.partialOptions.botInviteScopes = input.botInviteScopes;
|
|
2334
|
-
if (input.dashboardName) this.partialOptions.dashboardName = input.dashboardName;
|
|
2335
|
-
if (input.basePath) this.partialOptions.basePath = input.basePath;
|
|
2336
|
-
if (input.uiTemplate) this.partialOptions.uiTemplate = input.uiTemplate;
|
|
1766
|
+
Object.assign(this.partialOptions, input);
|
|
2337
1767
|
return this;
|
|
2338
1768
|
}
|
|
2339
1769
|
useTemplate(templateId) {
|
|
@@ -2377,11 +1807,7 @@ var DashboardDesigner = class {
|
|
|
2377
1807
|
const categoryId = input.categoryId ?? input.id;
|
|
2378
1808
|
this.pages.push({
|
|
2379
1809
|
pageId: input.id,
|
|
2380
|
-
category: {
|
|
2381
|
-
id: categoryId,
|
|
2382
|
-
label: input.label ?? input.title,
|
|
2383
|
-
scope
|
|
2384
|
-
},
|
|
1810
|
+
category: { id: categoryId, label: input.label ?? input.title, scope },
|
|
2385
1811
|
section: {
|
|
2386
1812
|
id: input.id,
|
|
2387
1813
|
title: input.title,
|
|
@@ -2395,10 +1821,6 @@ var DashboardDesigner = class {
|
|
|
2395
1821
|
});
|
|
2396
1822
|
return this;
|
|
2397
1823
|
}
|
|
2398
|
-
onHomeAction(actionId, handler) {
|
|
2399
|
-
this.homeActions[actionId] = handler;
|
|
2400
|
-
return this;
|
|
2401
|
-
}
|
|
2402
1824
|
onLoad(pageId, handler) {
|
|
2403
1825
|
this.loadHandlers[pageId] = handler;
|
|
2404
1826
|
return this;
|
|
@@ -2413,6 +1835,17 @@ var DashboardDesigner = class {
|
|
|
2413
1835
|
onsave(pageId, handler) {
|
|
2414
1836
|
return this.onSave(pageId, handler);
|
|
2415
1837
|
}
|
|
1838
|
+
onHomeAction(actionId, handler) {
|
|
1839
|
+
this.homeActions[actionId] = handler;
|
|
1840
|
+
return this;
|
|
1841
|
+
}
|
|
1842
|
+
customCss(cssString) {
|
|
1843
|
+
this.partialOptions.setupDesign = {
|
|
1844
|
+
...this.partialOptions.setupDesign ?? {},
|
|
1845
|
+
customCss: cssString
|
|
1846
|
+
};
|
|
1847
|
+
return this;
|
|
1848
|
+
}
|
|
2416
1849
|
build() {
|
|
2417
1850
|
const staticCategories = this.categories.map((item) => item.buildCategory());
|
|
2418
1851
|
const staticSections = this.categories.flatMap((item) => item.buildSections());
|
|
@@ -2421,20 +1854,14 @@ var DashboardDesigner = class {
|
|
|
2421
1854
|
const categoryMap = /* @__PURE__ */ new Map();
|
|
2422
1855
|
for (const category of [...staticCategories, ...pageCategories]) {
|
|
2423
1856
|
const key = `${category.scope}:${category.id}`;
|
|
2424
|
-
if (!categoryMap.has(key))
|
|
2425
|
-
categoryMap.set(key, category);
|
|
2426
|
-
}
|
|
1857
|
+
if (!categoryMap.has(key)) categoryMap.set(key, category);
|
|
2427
1858
|
}
|
|
2428
1859
|
const categories = [...categoryMap.values()];
|
|
2429
1860
|
const saveActionIds = {};
|
|
2430
1861
|
for (const section of baseSections) {
|
|
2431
|
-
if (this.saveHandlers[section.id]) {
|
|
2432
|
-
saveActionIds[section.id] = `save:${section.id}`;
|
|
2433
|
-
}
|
|
1862
|
+
if (this.saveHandlers[section.id]) saveActionIds[section.id] = `save:${section.id}`;
|
|
2434
1863
|
}
|
|
2435
|
-
const resolvedActions = {
|
|
2436
|
-
...this.homeActions
|
|
2437
|
-
};
|
|
1864
|
+
const resolvedActions = { ...this.homeActions };
|
|
2438
1865
|
for (const [sectionId, handler] of Object.entries(this.saveHandlers)) {
|
|
2439
1866
|
resolvedActions[saveActionIds[sectionId]] = handler;
|
|
2440
1867
|
}
|
|
@@ -2450,11 +1877,7 @@ var DashboardDesigner = class {
|
|
|
2450
1877
|
};
|
|
2451
1878
|
const saveActionId = saveActionIds[section.id];
|
|
2452
1879
|
if (saveActionId && !section.actions?.some((action) => action.id === saveActionId)) {
|
|
2453
|
-
section.actions = [...section.actions ?? [], {
|
|
2454
|
-
id: saveActionId,
|
|
2455
|
-
label: "Save",
|
|
2456
|
-
variant: "primary"
|
|
2457
|
-
}];
|
|
1880
|
+
section.actions = [...section.actions ?? [], { id: saveActionId, label: "Save", variant: "primary" }];
|
|
2458
1881
|
}
|
|
2459
1882
|
const loadHandler = this.loadHandlers[section.id];
|
|
2460
1883
|
if (loadHandler) {
|
|
@@ -2479,17 +1902,452 @@ var DashboardDesigner = class {
|
|
|
2479
1902
|
home
|
|
2480
1903
|
};
|
|
2481
1904
|
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Builds the configuration and immediately instantiates the Dashboard.
|
|
1907
|
+
*/
|
|
1908
|
+
createDashboard() {
|
|
1909
|
+
const options = this.build();
|
|
1910
|
+
return new DiscordDashboard(options);
|
|
1911
|
+
}
|
|
2482
1912
|
};
|
|
2483
|
-
|
|
2484
|
-
|
|
1913
|
+
|
|
1914
|
+
// src/index.ts
|
|
1915
|
+
var DISCORD_API = "https://discord.com/api/v10";
|
|
1916
|
+
var MANAGE_GUILD_PERMISSION = 0x20n;
|
|
1917
|
+
var ADMIN_PERMISSION = 0x8n;
|
|
1918
|
+
function normalizeBasePath(basePath) {
|
|
1919
|
+
if (!basePath || basePath === "/") return "/dashboard";
|
|
1920
|
+
return basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
1921
|
+
}
|
|
1922
|
+
function canManageGuild(permissions) {
|
|
1923
|
+
const value = BigInt(permissions);
|
|
1924
|
+
return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
|
|
1925
|
+
}
|
|
1926
|
+
function toQuery(params) {
|
|
1927
|
+
const url = new URLSearchParams();
|
|
1928
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1929
|
+
url.set(key, value);
|
|
1930
|
+
}
|
|
1931
|
+
return url.toString();
|
|
1932
|
+
}
|
|
1933
|
+
async function fetchDiscord(path, token) {
|
|
1934
|
+
const response = await fetch(`${DISCORD_API}${path}`, {
|
|
1935
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1936
|
+
});
|
|
1937
|
+
if (!response.ok) throw new Error(`Discord API request failed (${response.status})`);
|
|
1938
|
+
return await response.json();
|
|
2485
1939
|
}
|
|
1940
|
+
function getUserAvatarUrl(user) {
|
|
1941
|
+
if (user.avatar) {
|
|
1942
|
+
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
|
1943
|
+
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
|
|
1944
|
+
}
|
|
1945
|
+
const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
|
|
1946
|
+
return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
|
|
1947
|
+
}
|
|
1948
|
+
function getGuildIconUrl(guild) {
|
|
1949
|
+
if (!guild.icon) return null;
|
|
1950
|
+
const ext = guild.icon.startsWith("a_") ? "gif" : "png";
|
|
1951
|
+
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
|
|
1952
|
+
}
|
|
1953
|
+
var DiscordDashboard = class {
|
|
1954
|
+
app;
|
|
1955
|
+
options;
|
|
1956
|
+
helpers;
|
|
1957
|
+
router;
|
|
1958
|
+
server;
|
|
1959
|
+
basePath;
|
|
1960
|
+
templateRenderer;
|
|
1961
|
+
plugins;
|
|
1962
|
+
constructor(options) {
|
|
1963
|
+
this.validateOptions(options);
|
|
1964
|
+
this.options = options;
|
|
1965
|
+
this.app = options.app ?? (0, import_express.default)();
|
|
1966
|
+
this.router = import_express.default.Router();
|
|
1967
|
+
this.helpers = new DiscordHelpers(options.botToken);
|
|
1968
|
+
this.basePath = normalizeBasePath(options.basePath);
|
|
1969
|
+
this.templateRenderer = this.resolveTemplateRenderer();
|
|
1970
|
+
this.plugins = options.plugins ?? [];
|
|
1971
|
+
if (!options.app && options.trustProxy !== void 0) {
|
|
1972
|
+
this.app.set("trust proxy", options.trustProxy);
|
|
1973
|
+
}
|
|
1974
|
+
this.setupMiddleware();
|
|
1975
|
+
this.setupRoutes();
|
|
1976
|
+
this.app.use(this.basePath, this.router);
|
|
1977
|
+
}
|
|
1978
|
+
validateOptions(options) {
|
|
1979
|
+
if (!options.botToken) throw new Error("botToken is required");
|
|
1980
|
+
if (!options.clientId) throw new Error("clientId is required");
|
|
1981
|
+
if (!options.clientSecret) throw new Error("clientSecret is required");
|
|
1982
|
+
if (!options.redirectUri) throw new Error("redirectUri is required");
|
|
1983
|
+
if (!options.sessionSecret) throw new Error("sessionSecret is required");
|
|
1984
|
+
}
|
|
1985
|
+
setupMiddleware() {
|
|
1986
|
+
this.router.use((0, import_compression.default)());
|
|
1987
|
+
this.router.use((0, import_helmet.default)({ contentSecurityPolicy: false }));
|
|
1988
|
+
this.router.use(import_express.default.json());
|
|
1989
|
+
this.router.use(
|
|
1990
|
+
(0, import_express_session.default)({
|
|
1991
|
+
name: this.options.sessionName ?? "discord_dashboard.sid",
|
|
1992
|
+
secret: this.options.sessionSecret,
|
|
1993
|
+
resave: false,
|
|
1994
|
+
saveUninitialized: false,
|
|
1995
|
+
cookie: {
|
|
1996
|
+
httpOnly: true,
|
|
1997
|
+
sameSite: "lax",
|
|
1998
|
+
maxAge: this.options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
|
|
1999
|
+
}
|
|
2000
|
+
})
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
setupRoutes() {
|
|
2004
|
+
this.router.get("/", this.handleRoot.bind(this));
|
|
2005
|
+
this.router.get("/login", this.handleLogin.bind(this));
|
|
2006
|
+
this.router.get("/callback", this.handleCallback.bind(this));
|
|
2007
|
+
this.router.post("/logout", this.handleLogout.bind(this));
|
|
2008
|
+
this.router.get("/api/session", this.handleSession.bind(this));
|
|
2009
|
+
this.router.get("/api/guilds", this.ensureAuthenticated, this.handleGuilds.bind(this));
|
|
2010
|
+
this.router.get("/api/overview", this.ensureAuthenticated, this.handleOverview.bind(this));
|
|
2011
|
+
this.router.get("/api/home/categories", this.ensureAuthenticated, this.handleHomeCategories.bind(this));
|
|
2012
|
+
this.router.get("/api/home", this.ensureAuthenticated, this.handleHome.bind(this));
|
|
2013
|
+
this.router.get("/api/lookup/roles", this.ensureAuthenticated, this.handleLookupRoles.bind(this));
|
|
2014
|
+
this.router.get("/api/lookup/channels", this.ensureAuthenticated, this.handleLookupChannels.bind(this));
|
|
2015
|
+
this.router.get("/api/lookup/members", this.ensureAuthenticated, this.handleLookupMembers.bind(this));
|
|
2016
|
+
this.router.post("/api/home/:actionId", this.ensureAuthenticated, this.handleHomeAction.bind(this));
|
|
2017
|
+
this.router.get("/api/plugins", this.ensureAuthenticated, this.handlePlugins.bind(this));
|
|
2018
|
+
this.router.post("/api/plugins/:pluginId/:actionId", this.ensureAuthenticated, this.handlePluginAction.bind(this));
|
|
2019
|
+
}
|
|
2020
|
+
// --- Start/Stop Methods ---
|
|
2021
|
+
async start() {
|
|
2022
|
+
if (this.options.app || this.server) return;
|
|
2023
|
+
const port = this.options.port ?? 3e3;
|
|
2024
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
2025
|
+
this.server = (0, import_node_http.createServer)(this.app);
|
|
2026
|
+
return new Promise((resolve) => {
|
|
2027
|
+
this.server.listen(port, host, () => resolve());
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
async stop() {
|
|
2031
|
+
if (!this.server) return;
|
|
2032
|
+
return new Promise((resolve, reject) => {
|
|
2033
|
+
this.server.close((error) => {
|
|
2034
|
+
if (error) return reject(error);
|
|
2035
|
+
resolve();
|
|
2036
|
+
});
|
|
2037
|
+
this.server = void 0;
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
// --- Route Handlers ---
|
|
2041
|
+
handleRoot(req, res) {
|
|
2042
|
+
if (!req.session.discordAuth) {
|
|
2043
|
+
res.redirect(`${this.basePath}/login`);
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
res.setHeader("Cache-Control", "no-store");
|
|
2047
|
+
res.type("html").send(
|
|
2048
|
+
this.templateRenderer({
|
|
2049
|
+
dashboardName: this.options.dashboardName ?? "Discord Dashboard",
|
|
2050
|
+
basePath: this.basePath,
|
|
2051
|
+
setupDesign: this.options.setupDesign
|
|
2052
|
+
})
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
handleLogin(req, res) {
|
|
2056
|
+
const state = (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
2057
|
+
req.session.oauthState = state;
|
|
2058
|
+
const scope = (this.options.scopes && this.options.scopes.length > 0 ? this.options.scopes : ["identify", "guilds"]).join(" ");
|
|
2059
|
+
const query = toQuery({
|
|
2060
|
+
client_id: this.options.clientId,
|
|
2061
|
+
redirect_uri: this.options.redirectUri,
|
|
2062
|
+
response_type: "code",
|
|
2063
|
+
scope,
|
|
2064
|
+
state,
|
|
2065
|
+
prompt: "none"
|
|
2066
|
+
});
|
|
2067
|
+
res.redirect(`https://discord.com/oauth2/authorize?${query}`);
|
|
2068
|
+
}
|
|
2069
|
+
async handleCallback(req, res) {
|
|
2070
|
+
try {
|
|
2071
|
+
const code = typeof req.query.code === "string" ? req.query.code : void 0;
|
|
2072
|
+
const state = typeof req.query.state === "string" ? req.query.state : void 0;
|
|
2073
|
+
if (!code || !state) return res.status(400).send("Missing OAuth2 code/state");
|
|
2074
|
+
if (!req.session.oauthState || req.session.oauthState !== state) return res.status(403).send("Invalid OAuth2 state");
|
|
2075
|
+
const tokenData = await this.exchangeCodeForToken(code);
|
|
2076
|
+
const [user, guilds] = await Promise.all([fetchDiscord("/users/@me", tokenData.access_token), fetchDiscord("/users/@me/guilds", tokenData.access_token)]);
|
|
2077
|
+
req.session.discordAuth = {
|
|
2078
|
+
accessToken: tokenData.access_token,
|
|
2079
|
+
refreshToken: tokenData.refresh_token,
|
|
2080
|
+
expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0,
|
|
2081
|
+
user,
|
|
2082
|
+
guilds
|
|
2083
|
+
};
|
|
2084
|
+
req.session.oauthState = void 0;
|
|
2085
|
+
res.redirect(this.basePath);
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
const message = error instanceof Error ? error.message : "OAuth callback failed";
|
|
2088
|
+
res.status(500).send(message);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
handleLogout(req, res) {
|
|
2092
|
+
req.session.destroy((sessionError) => {
|
|
2093
|
+
if (sessionError) return res.status(500).json({ ok: false, message: "Failed to destroy session" });
|
|
2094
|
+
res.clearCookie(this.options.sessionName ?? "discord_dashboard.sid");
|
|
2095
|
+
res.json({ ok: true });
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
handleSession(req, res) {
|
|
2099
|
+
const auth = req.session.discordAuth;
|
|
2100
|
+
if (!auth) return res.status(200).json({ authenticated: false });
|
|
2101
|
+
const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2102
|
+
res.json({
|
|
2103
|
+
authenticated: true,
|
|
2104
|
+
user: { ...auth.user, avatarUrl: getUserAvatarUrl(auth.user) },
|
|
2105
|
+
guildCount: manageableGuildCount,
|
|
2106
|
+
expiresAt: auth.expiresAt
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
async handleGuilds(req, res) {
|
|
2110
|
+
const context = this.createContext(req);
|
|
2111
|
+
if (this.options.ownerIds && this.options.ownerIds.length > 0 && !this.options.ownerIds.includes(context.user.id)) {
|
|
2112
|
+
return res.status(403).json({ message: "You are not allowed to access this dashboard." });
|
|
2113
|
+
}
|
|
2114
|
+
let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
|
|
2115
|
+
if (this.options.guildFilter) {
|
|
2116
|
+
const filtered = [];
|
|
2117
|
+
for (const guild of manageableGuilds) {
|
|
2118
|
+
if (await this.options.guildFilter(guild, context)) filtered.push(guild);
|
|
2119
|
+
}
|
|
2120
|
+
manageableGuilds = filtered;
|
|
2121
|
+
}
|
|
2122
|
+
const botGuildIds = await this.fetchBotGuildIds();
|
|
2123
|
+
const enrichedGuilds = manageableGuilds.map((guild) => {
|
|
2124
|
+
const botInGuild = botGuildIds.has(guild.id);
|
|
2125
|
+
return {
|
|
2126
|
+
...guild,
|
|
2127
|
+
iconUrl: getGuildIconUrl(guild),
|
|
2128
|
+
botInGuild,
|
|
2129
|
+
inviteUrl: botInGuild ? void 0 : this.createGuildInviteUrl(guild.id)
|
|
2130
|
+
};
|
|
2131
|
+
});
|
|
2132
|
+
res.json({ guilds: enrichedGuilds });
|
|
2133
|
+
}
|
|
2134
|
+
async handleOverview(req, res) {
|
|
2135
|
+
const context = this.createContext(req);
|
|
2136
|
+
const cards = await this.resolveOverviewCards(context);
|
|
2137
|
+
res.json({ cards });
|
|
2138
|
+
}
|
|
2139
|
+
async handleHomeCategories(req, res) {
|
|
2140
|
+
const context = this.createContext(req);
|
|
2141
|
+
const activeScope = this.resolveScope(context);
|
|
2142
|
+
const categories = await this.resolveHomeCategories(context);
|
|
2143
|
+
const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
|
|
2144
|
+
res.json({ categories: visible, activeScope });
|
|
2145
|
+
}
|
|
2146
|
+
async handleHome(req, res) {
|
|
2147
|
+
const context = this.createContext(req);
|
|
2148
|
+
const activeScope = this.resolveScope(context);
|
|
2149
|
+
const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
|
|
2150
|
+
let sections = await this.resolveHomeSections(context);
|
|
2151
|
+
sections = sections.filter((section) => {
|
|
2152
|
+
const sectionScope = section.scope ?? activeScope;
|
|
2153
|
+
if (sectionScope !== "setup" && sectionScope !== activeScope) return false;
|
|
2154
|
+
if (!categoryId) return true;
|
|
2155
|
+
return section.categoryId === categoryId;
|
|
2156
|
+
});
|
|
2157
|
+
res.json({ sections, activeScope });
|
|
2158
|
+
}
|
|
2159
|
+
async handleLookupRoles(req, res) {
|
|
2160
|
+
const context = this.createContext(req);
|
|
2161
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2162
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2163
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2164
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2165
|
+
const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
|
|
2166
|
+
const roles = await context.helpers.searchGuildRoles(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, includeManaged });
|
|
2167
|
+
res.json({ roles });
|
|
2168
|
+
}
|
|
2169
|
+
async handleLookupChannels(req, res) {
|
|
2170
|
+
const context = this.createContext(req);
|
|
2171
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2172
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2173
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2174
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2175
|
+
const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
|
|
2176
|
+
const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
|
|
2177
|
+
const channels = await context.helpers.searchGuildChannels(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, nsfw, channelTypes });
|
|
2178
|
+
res.json({ channels });
|
|
2179
|
+
}
|
|
2180
|
+
async handleLookupMembers(req, res) {
|
|
2181
|
+
const context = this.createContext(req);
|
|
2182
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2183
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2184
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2185
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2186
|
+
const members = await context.helpers.searchGuildMembers(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0 });
|
|
2187
|
+
res.json({ members });
|
|
2188
|
+
}
|
|
2189
|
+
async handleHomeAction(req, res) {
|
|
2190
|
+
const context = this.createContext(req);
|
|
2191
|
+
const action = this.options.home?.actions?.[req.params.actionId];
|
|
2192
|
+
if (!action) return res.status(404).json({ ok: false, message: "Home action not found" });
|
|
2193
|
+
const payload = req.body;
|
|
2194
|
+
if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
|
|
2195
|
+
return res.status(400).json({ ok: false, message: "Invalid home action payload" });
|
|
2196
|
+
}
|
|
2197
|
+
try {
|
|
2198
|
+
const result = await action(context, { sectionId: payload.sectionId, values: payload.values });
|
|
2199
|
+
res.json(result);
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Home action failed" });
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async handlePlugins(req, res) {
|
|
2205
|
+
const context = this.createContext(req);
|
|
2206
|
+
const activeScope = context.selectedGuildId ? "guild" : "user";
|
|
2207
|
+
const payload = [];
|
|
2208
|
+
for (const plugin of this.plugins) {
|
|
2209
|
+
const pluginScope = plugin.scope ?? "both";
|
|
2210
|
+
if (pluginScope !== "both" && pluginScope !== activeScope) continue;
|
|
2211
|
+
const panels = await plugin.getPanels(context);
|
|
2212
|
+
payload.push({ id: plugin.id, name: plugin.name, description: plugin.description, panels });
|
|
2213
|
+
}
|
|
2214
|
+
res.json({ plugins: payload });
|
|
2215
|
+
}
|
|
2216
|
+
async handlePluginAction(req, res) {
|
|
2217
|
+
const context = this.createContext(req);
|
|
2218
|
+
const plugin = this.plugins.find((item) => item.id === req.params.pluginId);
|
|
2219
|
+
if (!plugin) return res.status(404).json({ ok: false, message: "Plugin not found" });
|
|
2220
|
+
const action = plugin.actions?.[req.params.actionId];
|
|
2221
|
+
if (!action) return res.status(404).json({ ok: false, message: "Action not found" });
|
|
2222
|
+
try {
|
|
2223
|
+
const result = await action(context, req.body);
|
|
2224
|
+
res.json(result);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Plugin action failed" });
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
// --- Internal Dashboard Resolvers & Helpers ---
|
|
2230
|
+
ensureAuthenticated = (req, res, next) => {
|
|
2231
|
+
if (!req.session.discordAuth) {
|
|
2232
|
+
res.status(401).json({ authenticated: false, message: "Authentication required" });
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
next();
|
|
2236
|
+
};
|
|
2237
|
+
createContext(req) {
|
|
2238
|
+
const auth = req.session.discordAuth;
|
|
2239
|
+
if (!auth) throw new Error("Not authenticated");
|
|
2240
|
+
return {
|
|
2241
|
+
user: auth.user,
|
|
2242
|
+
guilds: auth.guilds,
|
|
2243
|
+
accessToken: auth.accessToken,
|
|
2244
|
+
selectedGuildId: typeof req.query.guildId === "string" ? req.query.guildId : void 0,
|
|
2245
|
+
helpers: this.helpers
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
async exchangeCodeForToken(code) {
|
|
2249
|
+
const response = await fetch(`${DISCORD_API}/oauth2/token`, {
|
|
2250
|
+
method: "POST",
|
|
2251
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2252
|
+
body: toQuery({
|
|
2253
|
+
client_id: this.options.clientId,
|
|
2254
|
+
client_secret: this.options.clientSecret,
|
|
2255
|
+
grant_type: "authorization_code",
|
|
2256
|
+
code,
|
|
2257
|
+
redirect_uri: this.options.redirectUri
|
|
2258
|
+
})
|
|
2259
|
+
});
|
|
2260
|
+
if (!response.ok) throw new Error(`Failed token exchange: ${response.status} ${await response.text()}`);
|
|
2261
|
+
return await response.json();
|
|
2262
|
+
}
|
|
2263
|
+
async resolveOverviewCards(context) {
|
|
2264
|
+
if (this.options.getOverviewCards) return await this.options.getOverviewCards(context);
|
|
2265
|
+
const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2266
|
+
return [
|
|
2267
|
+
{ id: "user", title: "Logged-in User", value: context.user.global_name || context.user.username, subtitle: `ID: ${context.user.id}`, intent: "info" },
|
|
2268
|
+
{ id: "guilds", title: "Manageable Guilds", value: manageableGuildCount, subtitle: "Owner or Manage Server permissions", intent: "success" },
|
|
2269
|
+
{ id: "plugins", title: "Plugins Loaded", value: this.plugins.length, subtitle: "Dynamic server modules", intent: "neutral" }
|
|
2270
|
+
];
|
|
2271
|
+
}
|
|
2272
|
+
async resolveHomeSections(context) {
|
|
2273
|
+
const customSections = this.options.home?.getSections ? await this.options.home.getSections(context) : [];
|
|
2274
|
+
const overviewSections = this.options.home?.getOverviewSections ? await this.options.home.getOverviewSections(context) : [];
|
|
2275
|
+
if (customSections.length > 0 || overviewSections.length > 0) {
|
|
2276
|
+
const normalizedOverview = overviewSections.map((section) => ({ ...section, categoryId: section.categoryId ?? "overview" }));
|
|
2277
|
+
return [...normalizedOverview, ...customSections];
|
|
2278
|
+
}
|
|
2279
|
+
const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
|
|
2280
|
+
return [
|
|
2281
|
+
{
|
|
2282
|
+
id: "setup",
|
|
2283
|
+
title: "Setup Details",
|
|
2284
|
+
description: "Core dashboard setup information",
|
|
2285
|
+
scope: "setup",
|
|
2286
|
+
categoryId: "setup",
|
|
2287
|
+
fields: [
|
|
2288
|
+
{ id: "dashboardName", label: "Dashboard Name", type: "text", value: this.options.dashboardName ?? "Discord Dashboard", readOnly: true },
|
|
2289
|
+
{ id: "basePath", label: "Base Path", type: "text", value: this.basePath, readOnly: true }
|
|
2290
|
+
]
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
id: "context",
|
|
2294
|
+
title: "Dashboard Context",
|
|
2295
|
+
description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
|
|
2296
|
+
scope: this.resolveScope(context),
|
|
2297
|
+
categoryId: "overview",
|
|
2298
|
+
fields: [
|
|
2299
|
+
{ id: "mode", label: "Mode", type: "text", value: selectedGuild ? "Guild" : "User", readOnly: true },
|
|
2300
|
+
{ id: "target", label: "Target", type: "text", value: selectedGuild ? selectedGuild.name : context.user.username, readOnly: true }
|
|
2301
|
+
]
|
|
2302
|
+
}
|
|
2303
|
+
];
|
|
2304
|
+
}
|
|
2305
|
+
resolveScope(context) {
|
|
2306
|
+
return context.selectedGuildId ? "guild" : "user";
|
|
2307
|
+
}
|
|
2308
|
+
async resolveHomeCategories(context) {
|
|
2309
|
+
if (this.options.home?.getCategories) {
|
|
2310
|
+
const categories = await this.options.home.getCategories(context);
|
|
2311
|
+
return [...categories].sort((a, b) => a.id === "overview" ? -1 : b.id === "overview" ? 1 : 0);
|
|
2312
|
+
}
|
|
2313
|
+
return [
|
|
2314
|
+
{ id: "overview", label: "Overview", scope: this.resolveScope(context) },
|
|
2315
|
+
{ id: "setup", label: "Setup", scope: "setup" }
|
|
2316
|
+
];
|
|
2317
|
+
}
|
|
2318
|
+
createGuildInviteUrl(guildId) {
|
|
2319
|
+
const scopes = this.options.botInviteScopes && this.options.botInviteScopes.length > 0 ? this.options.botInviteScopes : ["bot", "applications.commands"];
|
|
2320
|
+
return `https://discord.com/oauth2/authorize?${toQuery({
|
|
2321
|
+
client_id: this.options.clientId,
|
|
2322
|
+
scope: scopes.join(" "),
|
|
2323
|
+
permissions: this.options.botInvitePermissions ?? "8",
|
|
2324
|
+
guild_id: guildId,
|
|
2325
|
+
disable_guild_select: "true"
|
|
2326
|
+
})}`;
|
|
2327
|
+
}
|
|
2328
|
+
async fetchBotGuildIds() {
|
|
2329
|
+
const response = await fetch(`${DISCORD_API}/users/@me/guilds`, { headers: { Authorization: `Bot ${this.options.botToken}` } });
|
|
2330
|
+
if (!response.ok) return /* @__PURE__ */ new Set();
|
|
2331
|
+
const guilds = await response.json();
|
|
2332
|
+
return new Set(guilds.map((guild) => guild.id));
|
|
2333
|
+
}
|
|
2334
|
+
resolveTemplateRenderer() {
|
|
2335
|
+
const selectedTemplate = this.options.uiTemplate ?? "default";
|
|
2336
|
+
const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
2337
|
+
const customRenderer = this.options.uiTemplates?.[selectedTemplate];
|
|
2338
|
+
if (customRenderer) return customRenderer;
|
|
2339
|
+
const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
|
|
2340
|
+
if (builtinRenderer) return builtinRenderer;
|
|
2341
|
+
if (selectedTemplate !== "default") throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
|
|
2342
|
+
return defaultRenderer;
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2486
2345
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2487
2346
|
0 && (module.exports = {
|
|
2488
2347
|
DashboardDesigner,
|
|
2348
|
+
DiscordDashboard,
|
|
2349
|
+
DiscordHelpers,
|
|
2489
2350
|
builtinTemplateRenderers,
|
|
2490
|
-
createDashboard,
|
|
2491
|
-
createDashboardDesigner,
|
|
2492
|
-
createDiscordHelpers,
|
|
2493
2351
|
getBuiltinTemplateRenderer
|
|
2494
2352
|
});
|
|
2495
2353
|
//# sourceMappingURL=index.cjs.map
|