@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.js
CHANGED
|
@@ -1,88 +1,90 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/index.ts
|
|
2
2
|
import compression from "compression";
|
|
3
3
|
import express from "express";
|
|
4
4
|
import session from "express-session";
|
|
5
5
|
import helmet from "helmet";
|
|
6
|
-
import { createServer } from "http";
|
|
7
6
|
import { randomBytes } from "crypto";
|
|
7
|
+
import { createServer } from "http";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
// src/handlers/DiscordHelpers.ts
|
|
10
|
+
var DiscordHelpers = class {
|
|
11
|
+
botToken;
|
|
12
|
+
DISCORD_API = "https://discord.com/api/v10";
|
|
13
|
+
constructor(botToken) {
|
|
14
|
+
this.botToken = botToken;
|
|
15
|
+
}
|
|
16
|
+
async fetchDiscordWithBot(path) {
|
|
17
|
+
const response = await fetch(`${this.DISCORD_API}${path}`, {
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bot ${this.botToken}`
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
return null;
|
|
15
24
|
}
|
|
16
|
-
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
return null;
|
|
25
|
+
return await response.json();
|
|
19
26
|
}
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
34
|
-
return channels.filter((channel) => {
|
|
35
|
-
if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
if (!normalizedQuery) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
return channel.name.toLowerCase().includes(normalizedQuery);
|
|
45
|
-
}).slice(0, limit);
|
|
46
|
-
},
|
|
47
|
-
async getRole(guildId, roleId) {
|
|
48
|
-
const roles = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`);
|
|
49
|
-
if (!roles) {
|
|
50
|
-
return null;
|
|
27
|
+
async getChannel(channelId) {
|
|
28
|
+
return await this.fetchDiscordWithBot(`/channels/${channelId}`);
|
|
29
|
+
}
|
|
30
|
+
async getGuildChannels(guildId) {
|
|
31
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
|
|
32
|
+
}
|
|
33
|
+
async searchGuildChannels(guildId, query, options) {
|
|
34
|
+
const channels = await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
|
|
35
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
36
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
37
|
+
return channels.filter((channel) => {
|
|
38
|
+
if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
|
|
39
|
+
return false;
|
|
51
40
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!normalizedQuery) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
return role.name.toLowerCase().includes(normalizedQuery);
|
|
69
|
-
}).sort((a, b) => b.position - a.position).slice(0, limit);
|
|
70
|
-
},
|
|
71
|
-
async searchGuildMembers(guildId, query, options) {
|
|
72
|
-
const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
|
|
73
|
-
const params = new URLSearchParams({
|
|
74
|
-
query: query.trim(),
|
|
75
|
-
limit: String(limit)
|
|
76
|
-
});
|
|
77
|
-
return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
|
|
78
|
-
},
|
|
79
|
-
async getGuildMember(guildId, userId) {
|
|
80
|
-
return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/${userId}`);
|
|
41
|
+
if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!normalizedQuery) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return channel.name.toLowerCase().includes(normalizedQuery);
|
|
48
|
+
}).slice(0, limit);
|
|
49
|
+
}
|
|
50
|
+
async getRole(guildId, roleId) {
|
|
51
|
+
const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`);
|
|
52
|
+
if (!roles) {
|
|
53
|
+
return null;
|
|
81
54
|
}
|
|
82
|
-
|
|
83
|
-
}
|
|
55
|
+
return roles.find((role) => role.id === roleId) ?? null;
|
|
56
|
+
}
|
|
57
|
+
async getGuildRoles(guildId) {
|
|
58
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
|
|
59
|
+
}
|
|
60
|
+
async searchGuildRoles(guildId, query, options) {
|
|
61
|
+
const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
|
|
62
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
63
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
|
|
64
|
+
return roles.filter((role) => {
|
|
65
|
+
if (!options?.includeManaged && role.managed) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (!normalizedQuery) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return role.name.toLowerCase().includes(normalizedQuery);
|
|
72
|
+
}).sort((a, b) => b.position - a.position).slice(0, limit);
|
|
73
|
+
}
|
|
74
|
+
async searchGuildMembers(guildId, query, options) {
|
|
75
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
|
|
76
|
+
const params = new URLSearchParams({
|
|
77
|
+
query: query.trim(),
|
|
78
|
+
limit: String(limit)
|
|
79
|
+
});
|
|
80
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
|
|
81
|
+
}
|
|
82
|
+
async getGuildMember(guildId, userId) {
|
|
83
|
+
return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/${userId}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
84
86
|
|
|
85
|
-
// src/templates.ts
|
|
87
|
+
// src/templates/templates.ts
|
|
86
88
|
var appCss = `
|
|
87
89
|
:root {
|
|
88
90
|
color-scheme: dark;
|
|
@@ -136,7 +138,6 @@ body {
|
|
|
136
138
|
font-weight: 700;
|
|
137
139
|
display: grid;
|
|
138
140
|
place-items: center;
|
|
139
|
-
cursor: pointer;
|
|
140
141
|
transition: border-radius .15s ease, background .15s ease, transform .15s ease;
|
|
141
142
|
}
|
|
142
143
|
.server-item:hover { border-radius: 16px; background: #404249; }
|
|
@@ -251,7 +252,6 @@ button {
|
|
|
251
252
|
color: var(--text);
|
|
252
253
|
border-radius: 8px;
|
|
253
254
|
padding: 8px 12px;
|
|
254
|
-
cursor: pointer;
|
|
255
255
|
}
|
|
256
256
|
button.primary {
|
|
257
257
|
background: var(--primary);
|
|
@@ -407,7 +407,6 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
|
|
|
407
407
|
.drag-handle {
|
|
408
408
|
color: var(--muted);
|
|
409
409
|
user-select: none;
|
|
410
|
-
cursor: grab;
|
|
411
410
|
font-size: 0.9rem;
|
|
412
411
|
}
|
|
413
412
|
.list-input {
|
|
@@ -421,6 +420,7 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
|
|
|
421
420
|
justify-self: start;
|
|
422
421
|
}
|
|
423
422
|
.empty { color: var(--muted); font-size: 0.9rem; }
|
|
423
|
+
.cursor-pointer { cursor: pointer; }
|
|
424
424
|
`;
|
|
425
425
|
function escapeHtml(value) {
|
|
426
426
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
@@ -428,13 +428,15 @@ function escapeHtml(value) {
|
|
|
428
428
|
function renderDashboardHtml(name, basePath, setupDesign) {
|
|
429
429
|
const safeName = escapeHtml(name);
|
|
430
430
|
const scriptData = JSON.stringify({ basePath, setupDesign: setupDesign ?? {} });
|
|
431
|
+
const customCssBlock = setupDesign?.customCss ? `
|
|
432
|
+
<style>${setupDesign.customCss}</style>` : "";
|
|
431
433
|
return `<!DOCTYPE html>
|
|
432
434
|
<html lang="en">
|
|
433
435
|
<head>
|
|
434
436
|
<meta charset="UTF-8" />
|
|
435
437
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
436
438
|
<title>${safeName}</title>
|
|
437
|
-
<style>${appCss}</style
|
|
439
|
+
<style>${appCss}</style>${customCssBlock}
|
|
438
440
|
</head>
|
|
439
441
|
<body>
|
|
440
442
|
<div class="layout">
|
|
@@ -451,8 +453,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
451
453
|
|
|
452
454
|
<div class="container">
|
|
453
455
|
<div class="main-tabs">
|
|
454
|
-
<button id="tabHome" class="main-tab active">Home</button>
|
|
455
|
-
<button id="tabPlugins" class="main-tab">Plugins</button>
|
|
456
|
+
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
457
|
+
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
456
458
|
</div>
|
|
457
459
|
|
|
458
460
|
<section id="homeArea">
|
|
@@ -563,7 +565,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
563
565
|
const makeButton = (action, pluginId, panelId, panelElement) => {
|
|
564
566
|
const button = document.createElement("button");
|
|
565
567
|
button.textContent = action.label;
|
|
566
|
-
|
|
568
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
569
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
570
|
+
|
|
567
571
|
button.addEventListener("click", async () => {
|
|
568
572
|
button.disabled = true;
|
|
569
573
|
try {
|
|
@@ -668,7 +672,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
668
672
|
el.serverRail.innerHTML = "";
|
|
669
673
|
items.forEach((item) => {
|
|
670
674
|
const button = document.createElement("button");
|
|
671
|
-
button.className = "server-item" + (item.id === state.selectedGuildId ? " active" : "");
|
|
675
|
+
button.className = "server-item cursor-pointer" + (item.id === state.selectedGuildId ? " active" : "");
|
|
672
676
|
button.title = item.id && !item.botInGuild ? (item.name + " \u2022 Invite bot") : item.name;
|
|
673
677
|
|
|
674
678
|
const activeIndicator = document.createElement("span");
|
|
@@ -705,8 +709,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
705
709
|
const homeActive = state.activeMainTab === "home";
|
|
706
710
|
el.homeArea.style.display = homeActive ? "block" : "none";
|
|
707
711
|
el.pluginsArea.style.display = homeActive ? "none" : "block";
|
|
708
|
-
el.tabHome.className = "main-tab" + (homeActive ? " active" : "");
|
|
709
|
-
el.tabPlugins.className = "main-tab" + (!homeActive ? " active" : "");
|
|
712
|
+
el.tabHome.className = "main-tab cursor-pointer" + (homeActive ? " active" : "");
|
|
713
|
+
el.tabPlugins.className = "main-tab cursor-pointer" + (!homeActive ? " active" : "");
|
|
710
714
|
};
|
|
711
715
|
|
|
712
716
|
const updateContextLabel = () => {
|
|
@@ -728,7 +732,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
728
732
|
el.homeCategories.innerHTML = "";
|
|
729
733
|
state.homeCategories.forEach((category) => {
|
|
730
734
|
const button = document.createElement("button");
|
|
731
|
-
button.className = "home-category-btn" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
735
|
+
button.className = "home-category-btn cursor-pointer" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
732
736
|
button.textContent = category.label;
|
|
733
737
|
button.title = category.description || category.label;
|
|
734
738
|
button.addEventListener("click", async () => {
|
|
@@ -789,7 +793,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
789
793
|
itemsWrap.className = "list-items";
|
|
790
794
|
const addButton = document.createElement("button");
|
|
791
795
|
addButton.type = "button";
|
|
792
|
-
addButton.className = "list-add";
|
|
796
|
+
addButton.className = "list-add cursor-pointer";
|
|
793
797
|
addButton.textContent = "Add Button";
|
|
794
798
|
|
|
795
799
|
const normalizeValues = () => {
|
|
@@ -805,7 +809,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
805
809
|
row.draggable = true;
|
|
806
810
|
|
|
807
811
|
const handle = document.createElement("span");
|
|
808
|
-
handle.className = "drag-handle";
|
|
812
|
+
handle.className = "drag-handle cursor-pointer";
|
|
809
813
|
handle.textContent = "\u22EE\u22EE";
|
|
810
814
|
|
|
811
815
|
const textInput = document.createElement("input");
|
|
@@ -817,6 +821,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
817
821
|
|
|
818
822
|
const removeButton = document.createElement("button");
|
|
819
823
|
removeButton.type = "button";
|
|
824
|
+
removeButton.className = "cursor-pointer";
|
|
820
825
|
removeButton.textContent = "\xD7";
|
|
821
826
|
removeButton.addEventListener("click", () => {
|
|
822
827
|
row.remove();
|
|
@@ -894,7 +899,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
894
899
|
items.forEach((item) => {
|
|
895
900
|
const btn = document.createElement("button");
|
|
896
901
|
btn.type = "button";
|
|
897
|
-
btn.className = "lookup-item";
|
|
902
|
+
btn.className = "lookup-item cursor-pointer";
|
|
898
903
|
btn.textContent = labelResolver(item);
|
|
899
904
|
btn.addEventListener("click", () => {
|
|
900
905
|
onSelect(item);
|
|
@@ -1050,7 +1055,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1050
1055
|
input.value = field.value == null ? "" : String(field.value);
|
|
1051
1056
|
} else if (field.type === "select") {
|
|
1052
1057
|
input = document.createElement("select");
|
|
1053
|
-
input.className = "home-select";
|
|
1058
|
+
input.className = "home-select cursor-pointer";
|
|
1054
1059
|
(field.options || []).forEach((option) => {
|
|
1055
1060
|
const optionEl = document.createElement("option");
|
|
1056
1061
|
optionEl.value = option.value;
|
|
@@ -1065,7 +1070,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1065
1070
|
row.className = "home-field-row";
|
|
1066
1071
|
input = document.createElement("input");
|
|
1067
1072
|
input.type = "checkbox";
|
|
1068
|
-
input.className = "home-checkbox";
|
|
1073
|
+
input.className = "home-checkbox cursor-pointer";
|
|
1069
1074
|
input.checked = Boolean(field.value);
|
|
1070
1075
|
const stateText = document.createElement("span");
|
|
1071
1076
|
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
@@ -1120,7 +1125,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1120
1125
|
section.actions.forEach((action) => {
|
|
1121
1126
|
const button = document.createElement("button");
|
|
1122
1127
|
button.textContent = action.label;
|
|
1123
|
-
|
|
1128
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
1129
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
1130
|
+
|
|
1124
1131
|
button.addEventListener("click", async () => {
|
|
1125
1132
|
button.disabled = true;
|
|
1126
1133
|
try {
|
|
@@ -1234,7 +1241,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1234
1241
|
input.value = field.value == null ? "" : String(field.value);
|
|
1235
1242
|
} else if (field.type === "select") {
|
|
1236
1243
|
input = document.createElement("select");
|
|
1237
|
-
input.className = "home-select";
|
|
1244
|
+
input.className = "home-select cursor-pointer";
|
|
1238
1245
|
(field.options || []).forEach((option) => {
|
|
1239
1246
|
const optionEl = document.createElement("option");
|
|
1240
1247
|
optionEl.value = option.value;
|
|
@@ -1249,7 +1256,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
|
|
|
1249
1256
|
row.className = "home-field-row";
|
|
1250
1257
|
input = document.createElement("input");
|
|
1251
1258
|
input.type = "checkbox";
|
|
1252
|
-
input.className = "home-checkbox";
|
|
1259
|
+
input.className = "home-checkbox cursor-pointer";
|
|
1253
1260
|
input.checked = Boolean(field.value);
|
|
1254
1261
|
const stateText = document.createElement("span");
|
|
1255
1262
|
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
@@ -1459,7 +1466,6 @@ body {
|
|
|
1459
1466
|
background: var(--panel);
|
|
1460
1467
|
color: #fff;
|
|
1461
1468
|
font-weight: 700;
|
|
1462
|
-
cursor: pointer;
|
|
1463
1469
|
transition: transform .15s ease, background .15s ease;
|
|
1464
1470
|
}
|
|
1465
1471
|
.server-item:hover { transform: translateY(-1px); background: #323b5f; }
|
|
@@ -1510,7 +1516,6 @@ button {
|
|
|
1510
1516
|
color: var(--text);
|
|
1511
1517
|
border-radius: 8px;
|
|
1512
1518
|
padding: 7px 10px;
|
|
1513
|
-
cursor: pointer;
|
|
1514
1519
|
}
|
|
1515
1520
|
button.primary { background: var(--primary); border: none; }
|
|
1516
1521
|
button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
@@ -1601,29 +1606,28 @@ button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
|
1601
1606
|
background: var(--panel);
|
|
1602
1607
|
}
|
|
1603
1608
|
.list-item.dragging { opacity: .6; }
|
|
1604
|
-
.drag-handle { color: var(--muted); user-select: none;
|
|
1609
|
+
.drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
|
|
1605
1610
|
.list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
|
|
1606
1611
|
.list-add { justify-self: start; }
|
|
1607
1612
|
.empty { color: var(--muted); font-size: .9rem; }
|
|
1613
|
+
.cursor-pointer { cursor: pointer; }
|
|
1608
1614
|
@media (max-width: 980px) {
|
|
1609
1615
|
.layout { grid-template-columns: 70px 1fr; }
|
|
1610
1616
|
.home-width-50, .home-width-33, .home-width-20 { flex-basis: 100%; max-width: 100%; }
|
|
1611
1617
|
}
|
|
1612
1618
|
`;
|
|
1613
|
-
var compactDashboardTemplateRenderer = ({
|
|
1614
|
-
dashboardName,
|
|
1615
|
-
basePath,
|
|
1616
|
-
setupDesign
|
|
1617
|
-
}) => {
|
|
1619
|
+
var compactDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => {
|
|
1618
1620
|
const script = extractDashboardScript(renderDashboardHtml(dashboardName, basePath, setupDesign));
|
|
1619
1621
|
const safeName = escapeHtml2(dashboardName);
|
|
1622
|
+
const customCssBlock = setupDesign?.customCss ? `
|
|
1623
|
+
<style>${setupDesign.customCss}</style>` : "";
|
|
1620
1624
|
return `<!DOCTYPE html>
|
|
1621
1625
|
<html lang="en">
|
|
1622
1626
|
<head>
|
|
1623
1627
|
<meta charset="UTF-8" />
|
|
1624
1628
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1625
1629
|
<title>${safeName}</title>
|
|
1626
|
-
<style>${compactCss}</style
|
|
1630
|
+
<style>${compactCss}</style>${customCssBlock}
|
|
1627
1631
|
</head>
|
|
1628
1632
|
<body>
|
|
1629
1633
|
<div class="shell">
|
|
@@ -1641,8 +1645,8 @@ var compactDashboardTemplateRenderer = ({
|
|
|
1641
1645
|
<main class="content">
|
|
1642
1646
|
<div class="container">
|
|
1643
1647
|
<div class="main-tabs">
|
|
1644
|
-
<button id="tabHome" class="main-tab active">Home</button>
|
|
1645
|
-
<button id="tabPlugins" class="main-tab">Plugins</button>
|
|
1648
|
+
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
1649
|
+
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
1646
1650
|
</div>
|
|
1647
1651
|
|
|
1648
1652
|
<section id="homeArea">
|
|
@@ -1671,11 +1675,7 @@ var compactDashboardTemplateRenderer = ({
|
|
|
1671
1675
|
};
|
|
1672
1676
|
|
|
1673
1677
|
// src/templates/default.ts
|
|
1674
|
-
var defaultDashboardTemplateRenderer = ({
|
|
1675
|
-
dashboardName,
|
|
1676
|
-
basePath,
|
|
1677
|
-
setupDesign
|
|
1678
|
-
}) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1678
|
+
var defaultDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1679
1679
|
|
|
1680
1680
|
// src/templates/index.ts
|
|
1681
1681
|
var builtinTemplateRenderers = {
|
|
@@ -1686,565 +1686,7 @@ function getBuiltinTemplateRenderer(templateId) {
|
|
|
1686
1686
|
return builtinTemplateRenderers[templateId];
|
|
1687
1687
|
}
|
|
1688
1688
|
|
|
1689
|
-
// src/
|
|
1690
|
-
var DISCORD_API2 = "https://discord.com/api/v10";
|
|
1691
|
-
var MANAGE_GUILD_PERMISSION = 0x20n;
|
|
1692
|
-
var ADMIN_PERMISSION = 0x8n;
|
|
1693
|
-
function normalizeBasePath(basePath) {
|
|
1694
|
-
if (!basePath || basePath === "/") {
|
|
1695
|
-
return "/dashboard";
|
|
1696
|
-
}
|
|
1697
|
-
return basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
1698
|
-
}
|
|
1699
|
-
function canManageGuild(permissions) {
|
|
1700
|
-
const value = BigInt(permissions);
|
|
1701
|
-
return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
|
|
1702
|
-
}
|
|
1703
|
-
function toQuery(params) {
|
|
1704
|
-
const url = new URLSearchParams();
|
|
1705
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1706
|
-
url.set(key, value);
|
|
1707
|
-
}
|
|
1708
|
-
return url.toString();
|
|
1709
|
-
}
|
|
1710
|
-
async function fetchDiscord(path, token) {
|
|
1711
|
-
const response = await fetch(`${DISCORD_API2}${path}`, {
|
|
1712
|
-
headers: {
|
|
1713
|
-
Authorization: `Bearer ${token}`
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1716
|
-
if (!response.ok) {
|
|
1717
|
-
throw new Error(`Discord API request failed (${response.status})`);
|
|
1718
|
-
}
|
|
1719
|
-
return await response.json();
|
|
1720
|
-
}
|
|
1721
|
-
async function exchangeCodeForToken(options, code) {
|
|
1722
|
-
const response = await fetch(`${DISCORD_API2}/oauth2/token`, {
|
|
1723
|
-
method: "POST",
|
|
1724
|
-
headers: {
|
|
1725
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1726
|
-
},
|
|
1727
|
-
body: toQuery({
|
|
1728
|
-
client_id: options.clientId,
|
|
1729
|
-
client_secret: options.clientSecret,
|
|
1730
|
-
grant_type: "authorization_code",
|
|
1731
|
-
code,
|
|
1732
|
-
redirect_uri: options.redirectUri
|
|
1733
|
-
})
|
|
1734
|
-
});
|
|
1735
|
-
if (!response.ok) {
|
|
1736
|
-
const text = await response.text();
|
|
1737
|
-
throw new Error(`Failed token exchange: ${response.status} ${text}`);
|
|
1738
|
-
}
|
|
1739
|
-
return await response.json();
|
|
1740
|
-
}
|
|
1741
|
-
function createContext(req, options) {
|
|
1742
|
-
const auth = req.session.discordAuth;
|
|
1743
|
-
if (!auth) {
|
|
1744
|
-
throw new Error("Not authenticated");
|
|
1745
|
-
}
|
|
1746
|
-
const selectedGuildId = typeof req.query.guildId === "string" ? req.query.guildId : void 0;
|
|
1747
|
-
return {
|
|
1748
|
-
user: auth.user,
|
|
1749
|
-
guilds: auth.guilds,
|
|
1750
|
-
accessToken: auth.accessToken,
|
|
1751
|
-
selectedGuildId,
|
|
1752
|
-
helpers: createDiscordHelpers(options.botToken)
|
|
1753
|
-
};
|
|
1754
|
-
}
|
|
1755
|
-
function ensureAuthenticated(req, res, next) {
|
|
1756
|
-
if (!req.session.discordAuth) {
|
|
1757
|
-
res.status(401).json({ authenticated: false, message: "Authentication required" });
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
next();
|
|
1761
|
-
}
|
|
1762
|
-
async function resolveOverviewCards(options, context) {
|
|
1763
|
-
if (options.getOverviewCards) {
|
|
1764
|
-
return await options.getOverviewCards(context);
|
|
1765
|
-
}
|
|
1766
|
-
const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
1767
|
-
return [
|
|
1768
|
-
{
|
|
1769
|
-
id: "user",
|
|
1770
|
-
title: "Logged-in User",
|
|
1771
|
-
value: context.user.global_name || context.user.username,
|
|
1772
|
-
subtitle: `ID: ${context.user.id}`,
|
|
1773
|
-
intent: "info"
|
|
1774
|
-
},
|
|
1775
|
-
{
|
|
1776
|
-
id: "guilds",
|
|
1777
|
-
title: "Manageable Guilds",
|
|
1778
|
-
value: manageableGuildCount,
|
|
1779
|
-
subtitle: "Owner or Manage Server permissions",
|
|
1780
|
-
intent: "success"
|
|
1781
|
-
},
|
|
1782
|
-
{
|
|
1783
|
-
id: "plugins",
|
|
1784
|
-
title: "Plugins Loaded",
|
|
1785
|
-
value: options.plugins?.length ?? 0,
|
|
1786
|
-
subtitle: "Dynamic server modules",
|
|
1787
|
-
intent: "neutral"
|
|
1788
|
-
}
|
|
1789
|
-
];
|
|
1790
|
-
}
|
|
1791
|
-
async function resolveHomeSections(options, context) {
|
|
1792
|
-
const customSections = options.home?.getSections ? await options.home.getSections(context) : [];
|
|
1793
|
-
const overviewSections = options.home?.getOverviewSections ? await options.home.getOverviewSections(context) : [];
|
|
1794
|
-
if (customSections.length > 0 || overviewSections.length > 0) {
|
|
1795
|
-
const normalizedOverview = overviewSections.map((section) => ({
|
|
1796
|
-
...section,
|
|
1797
|
-
categoryId: section.categoryId ?? "overview"
|
|
1798
|
-
}));
|
|
1799
|
-
return [...normalizedOverview, ...customSections];
|
|
1800
|
-
}
|
|
1801
|
-
const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
|
|
1802
|
-
return [
|
|
1803
|
-
{
|
|
1804
|
-
id: "setup",
|
|
1805
|
-
title: "Setup Details",
|
|
1806
|
-
description: "Core dashboard setup information",
|
|
1807
|
-
scope: "setup",
|
|
1808
|
-
categoryId: "setup",
|
|
1809
|
-
fields: [
|
|
1810
|
-
{
|
|
1811
|
-
id: "dashboardName",
|
|
1812
|
-
label: "Dashboard Name",
|
|
1813
|
-
type: "text",
|
|
1814
|
-
value: options.dashboardName ?? "Discord Dashboard",
|
|
1815
|
-
readOnly: true
|
|
1816
|
-
},
|
|
1817
|
-
{
|
|
1818
|
-
id: "basePath",
|
|
1819
|
-
label: "Base Path",
|
|
1820
|
-
type: "text",
|
|
1821
|
-
value: options.basePath ?? "/dashboard",
|
|
1822
|
-
readOnly: true
|
|
1823
|
-
}
|
|
1824
|
-
]
|
|
1825
|
-
},
|
|
1826
|
-
{
|
|
1827
|
-
id: "context",
|
|
1828
|
-
title: "Dashboard Context",
|
|
1829
|
-
description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
|
|
1830
|
-
scope: resolveScope(context),
|
|
1831
|
-
categoryId: "overview",
|
|
1832
|
-
fields: [
|
|
1833
|
-
{
|
|
1834
|
-
id: "mode",
|
|
1835
|
-
label: "Mode",
|
|
1836
|
-
type: "text",
|
|
1837
|
-
value: selectedGuild ? "Guild" : "User",
|
|
1838
|
-
readOnly: true
|
|
1839
|
-
},
|
|
1840
|
-
{
|
|
1841
|
-
id: "target",
|
|
1842
|
-
label: "Target",
|
|
1843
|
-
type: "text",
|
|
1844
|
-
value: selectedGuild ? selectedGuild.name : context.user.username,
|
|
1845
|
-
readOnly: true
|
|
1846
|
-
}
|
|
1847
|
-
]
|
|
1848
|
-
}
|
|
1849
|
-
];
|
|
1850
|
-
}
|
|
1851
|
-
function resolveScope(context) {
|
|
1852
|
-
return context.selectedGuildId ? "guild" : "user";
|
|
1853
|
-
}
|
|
1854
|
-
async function resolveHomeCategories(options, context) {
|
|
1855
|
-
if (options.home?.getCategories) {
|
|
1856
|
-
const categories = await options.home.getCategories(context);
|
|
1857
|
-
return [...categories].sort((a, b) => {
|
|
1858
|
-
if (a.id === "overview") return -1;
|
|
1859
|
-
if (b.id === "overview") return 1;
|
|
1860
|
-
return 0;
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
return [
|
|
1864
|
-
{ id: "overview", label: "Overview", scope: resolveScope(context) },
|
|
1865
|
-
{ id: "setup", label: "Setup", scope: "setup" }
|
|
1866
|
-
];
|
|
1867
|
-
}
|
|
1868
|
-
function getUserAvatarUrl(user) {
|
|
1869
|
-
if (user.avatar) {
|
|
1870
|
-
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
|
1871
|
-
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
|
|
1872
|
-
}
|
|
1873
|
-
const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
|
|
1874
|
-
return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
|
|
1875
|
-
}
|
|
1876
|
-
function getGuildIconUrl(guild) {
|
|
1877
|
-
if (!guild.icon) {
|
|
1878
|
-
return null;
|
|
1879
|
-
}
|
|
1880
|
-
const ext = guild.icon.startsWith("a_") ? "gif" : "png";
|
|
1881
|
-
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
|
|
1882
|
-
}
|
|
1883
|
-
function createGuildInviteUrl(options, guildId) {
|
|
1884
|
-
const scopes = options.botInviteScopes && options.botInviteScopes.length > 0 ? options.botInviteScopes : ["bot", "applications.commands"];
|
|
1885
|
-
return `https://discord.com/oauth2/authorize?${toQuery({
|
|
1886
|
-
client_id: options.clientId,
|
|
1887
|
-
scope: scopes.join(" "),
|
|
1888
|
-
permissions: options.botInvitePermissions ?? "8",
|
|
1889
|
-
guild_id: guildId,
|
|
1890
|
-
disable_guild_select: "true"
|
|
1891
|
-
})}`;
|
|
1892
|
-
}
|
|
1893
|
-
async function fetchBotGuildIds(botToken) {
|
|
1894
|
-
const response = await fetch(`${DISCORD_API2}/users/@me/guilds`, {
|
|
1895
|
-
headers: {
|
|
1896
|
-
Authorization: `Bot ${botToken}`
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
if (!response.ok) {
|
|
1900
|
-
return /* @__PURE__ */ new Set();
|
|
1901
|
-
}
|
|
1902
|
-
const guilds = await response.json();
|
|
1903
|
-
return new Set(guilds.map((guild) => guild.id));
|
|
1904
|
-
}
|
|
1905
|
-
function resolveTemplateRenderer(options) {
|
|
1906
|
-
const selectedTemplate = options.uiTemplate ?? "default";
|
|
1907
|
-
const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
1908
|
-
const customRenderer = options.uiTemplates?.[selectedTemplate];
|
|
1909
|
-
if (customRenderer) {
|
|
1910
|
-
return customRenderer;
|
|
1911
|
-
}
|
|
1912
|
-
const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
|
|
1913
|
-
if (builtinRenderer) {
|
|
1914
|
-
return builtinRenderer;
|
|
1915
|
-
}
|
|
1916
|
-
if (selectedTemplate !== "default") {
|
|
1917
|
-
throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
|
|
1918
|
-
}
|
|
1919
|
-
return defaultRenderer;
|
|
1920
|
-
}
|
|
1921
|
-
function createDashboard(options) {
|
|
1922
|
-
const app = options.app ?? express();
|
|
1923
|
-
const basePath = normalizeBasePath(options.basePath);
|
|
1924
|
-
const dashboardName = options.dashboardName ?? "Discord Dashboard";
|
|
1925
|
-
const templateRenderer = resolveTemplateRenderer(options);
|
|
1926
|
-
const plugins = options.plugins ?? [];
|
|
1927
|
-
if (!options.botToken) throw new Error("botToken is required");
|
|
1928
|
-
if (!options.clientId) throw new Error("clientId is required");
|
|
1929
|
-
if (!options.clientSecret) throw new Error("clientSecret is required");
|
|
1930
|
-
if (!options.redirectUri) throw new Error("redirectUri is required");
|
|
1931
|
-
if (!options.sessionSecret) throw new Error("sessionSecret is required");
|
|
1932
|
-
if (!options.app && options.trustProxy !== void 0) {
|
|
1933
|
-
app.set("trust proxy", options.trustProxy);
|
|
1934
|
-
}
|
|
1935
|
-
const router = express.Router();
|
|
1936
|
-
const sessionMiddleware = session({
|
|
1937
|
-
name: options.sessionName ?? "discord_dashboard.sid",
|
|
1938
|
-
secret: options.sessionSecret,
|
|
1939
|
-
resave: false,
|
|
1940
|
-
saveUninitialized: false,
|
|
1941
|
-
cookie: {
|
|
1942
|
-
httpOnly: true,
|
|
1943
|
-
sameSite: "lax",
|
|
1944
|
-
maxAge: options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
|
|
1945
|
-
}
|
|
1946
|
-
});
|
|
1947
|
-
router.use(compression());
|
|
1948
|
-
router.use(
|
|
1949
|
-
helmet({
|
|
1950
|
-
contentSecurityPolicy: false
|
|
1951
|
-
})
|
|
1952
|
-
);
|
|
1953
|
-
router.use(express.json());
|
|
1954
|
-
router.use(sessionMiddleware);
|
|
1955
|
-
router.get("/", (req, res) => {
|
|
1956
|
-
if (!req.session.discordAuth) {
|
|
1957
|
-
res.redirect(`${basePath}/login`);
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
res.setHeader("Cache-Control", "no-store");
|
|
1961
|
-
res.type("html").send(templateRenderer({
|
|
1962
|
-
dashboardName,
|
|
1963
|
-
basePath,
|
|
1964
|
-
setupDesign: options.setupDesign
|
|
1965
|
-
}));
|
|
1966
|
-
});
|
|
1967
|
-
router.get("/login", (req, res) => {
|
|
1968
|
-
const state = randomBytes(16).toString("hex");
|
|
1969
|
-
req.session.oauthState = state;
|
|
1970
|
-
const scope = (options.scopes && options.scopes.length > 0 ? options.scopes : ["identify", "guilds"]).join(" ");
|
|
1971
|
-
const query = toQuery({
|
|
1972
|
-
client_id: options.clientId,
|
|
1973
|
-
redirect_uri: options.redirectUri,
|
|
1974
|
-
response_type: "code",
|
|
1975
|
-
scope,
|
|
1976
|
-
state,
|
|
1977
|
-
prompt: "none"
|
|
1978
|
-
});
|
|
1979
|
-
res.redirect(`https://discord.com/oauth2/authorize?${query}`);
|
|
1980
|
-
});
|
|
1981
|
-
router.get("/callback", async (req, res) => {
|
|
1982
|
-
try {
|
|
1983
|
-
const code = typeof req.query.code === "string" ? req.query.code : void 0;
|
|
1984
|
-
const state = typeof req.query.state === "string" ? req.query.state : void 0;
|
|
1985
|
-
if (!code || !state) {
|
|
1986
|
-
res.status(400).send("Missing OAuth2 code/state");
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
if (!req.session.oauthState || req.session.oauthState !== state) {
|
|
1990
|
-
res.status(403).send("Invalid OAuth2 state");
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
const tokenData = await exchangeCodeForToken(options, code);
|
|
1994
|
-
const [user, guilds] = await Promise.all([
|
|
1995
|
-
fetchDiscord("/users/@me", tokenData.access_token),
|
|
1996
|
-
fetchDiscord("/users/@me/guilds", tokenData.access_token)
|
|
1997
|
-
]);
|
|
1998
|
-
req.session.discordAuth = {
|
|
1999
|
-
accessToken: tokenData.access_token,
|
|
2000
|
-
refreshToken: tokenData.refresh_token,
|
|
2001
|
-
expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0,
|
|
2002
|
-
user,
|
|
2003
|
-
guilds
|
|
2004
|
-
};
|
|
2005
|
-
req.session.oauthState = void 0;
|
|
2006
|
-
res.redirect(basePath);
|
|
2007
|
-
} catch (error) {
|
|
2008
|
-
const message = error instanceof Error ? error.message : "OAuth callback failed";
|
|
2009
|
-
res.status(500).send(message);
|
|
2010
|
-
}
|
|
2011
|
-
});
|
|
2012
|
-
router.post("/logout", (req, res) => {
|
|
2013
|
-
req.session.destroy((sessionError) => {
|
|
2014
|
-
if (sessionError) {
|
|
2015
|
-
res.status(500).json({ ok: false, message: "Failed to destroy session" });
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
res.clearCookie(options.sessionName ?? "discord_dashboard.sid");
|
|
2019
|
-
res.json({ ok: true });
|
|
2020
|
-
});
|
|
2021
|
-
});
|
|
2022
|
-
router.get("/api/session", (req, res) => {
|
|
2023
|
-
const auth = req.session.discordAuth;
|
|
2024
|
-
if (!auth) {
|
|
2025
|
-
res.status(200).json({ authenticated: false });
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2029
|
-
res.json({
|
|
2030
|
-
authenticated: true,
|
|
2031
|
-
user: {
|
|
2032
|
-
...auth.user,
|
|
2033
|
-
avatarUrl: getUserAvatarUrl(auth.user)
|
|
2034
|
-
},
|
|
2035
|
-
guildCount: manageableGuildCount,
|
|
2036
|
-
expiresAt: auth.expiresAt
|
|
2037
|
-
});
|
|
2038
|
-
});
|
|
2039
|
-
router.get("/api/guilds", ensureAuthenticated, async (req, res) => {
|
|
2040
|
-
const context = createContext(req, options);
|
|
2041
|
-
if (options.ownerIds && options.ownerIds.length > 0 && !options.ownerIds.includes(context.user.id)) {
|
|
2042
|
-
res.status(403).json({ message: "You are not allowed to access this dashboard." });
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
|
|
2046
|
-
if (options.guildFilter) {
|
|
2047
|
-
const filtered = [];
|
|
2048
|
-
for (const guild of manageableGuilds) {
|
|
2049
|
-
const allowed = await options.guildFilter(guild, context);
|
|
2050
|
-
if (allowed) {
|
|
2051
|
-
filtered.push(guild);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
manageableGuilds = filtered;
|
|
2055
|
-
}
|
|
2056
|
-
const botGuildIds = await fetchBotGuildIds(options.botToken);
|
|
2057
|
-
const enrichedGuilds = manageableGuilds.map((guild) => {
|
|
2058
|
-
const botInGuild = botGuildIds.has(guild.id);
|
|
2059
|
-
return {
|
|
2060
|
-
...guild,
|
|
2061
|
-
iconUrl: getGuildIconUrl(guild),
|
|
2062
|
-
botInGuild,
|
|
2063
|
-
inviteUrl: botInGuild ? void 0 : createGuildInviteUrl(options, guild.id)
|
|
2064
|
-
};
|
|
2065
|
-
});
|
|
2066
|
-
res.json({ guilds: enrichedGuilds });
|
|
2067
|
-
});
|
|
2068
|
-
router.get("/api/overview", ensureAuthenticated, async (req, res) => {
|
|
2069
|
-
const context = createContext(req, options);
|
|
2070
|
-
const cards = await resolveOverviewCards(options, context);
|
|
2071
|
-
res.json({ cards });
|
|
2072
|
-
});
|
|
2073
|
-
router.get("/api/home/categories", ensureAuthenticated, async (req, res) => {
|
|
2074
|
-
const context = createContext(req, options);
|
|
2075
|
-
const activeScope = resolveScope(context);
|
|
2076
|
-
const categories = await resolveHomeCategories(options, context);
|
|
2077
|
-
const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
|
|
2078
|
-
res.json({ categories: visible, activeScope });
|
|
2079
|
-
});
|
|
2080
|
-
router.get("/api/home", ensureAuthenticated, async (req, res) => {
|
|
2081
|
-
const context = createContext(req, options);
|
|
2082
|
-
const activeScope = resolveScope(context);
|
|
2083
|
-
const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
|
|
2084
|
-
let sections = await resolveHomeSections(options, context);
|
|
2085
|
-
sections = sections.filter((section) => {
|
|
2086
|
-
const sectionScope = section.scope ?? activeScope;
|
|
2087
|
-
if (sectionScope !== "setup" && sectionScope !== activeScope) {
|
|
2088
|
-
return false;
|
|
2089
|
-
}
|
|
2090
|
-
if (!categoryId) {
|
|
2091
|
-
return true;
|
|
2092
|
-
}
|
|
2093
|
-
return section.categoryId === categoryId;
|
|
2094
|
-
});
|
|
2095
|
-
res.json({ sections, activeScope });
|
|
2096
|
-
});
|
|
2097
|
-
router.get("/api/lookup/roles", ensureAuthenticated, async (req, res) => {
|
|
2098
|
-
const context = createContext(req, options);
|
|
2099
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2100
|
-
if (!guildId) {
|
|
2101
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2102
|
-
return;
|
|
2103
|
-
}
|
|
2104
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2105
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2106
|
-
const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
|
|
2107
|
-
const roles = await context.helpers.searchGuildRoles(guildId, query, {
|
|
2108
|
-
limit: Number.isFinite(limit) ? limit : void 0,
|
|
2109
|
-
includeManaged
|
|
2110
|
-
});
|
|
2111
|
-
res.json({ roles });
|
|
2112
|
-
});
|
|
2113
|
-
router.get("/api/lookup/channels", ensureAuthenticated, async (req, res) => {
|
|
2114
|
-
const context = createContext(req, options);
|
|
2115
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2116
|
-
if (!guildId) {
|
|
2117
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2121
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2122
|
-
const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
|
|
2123
|
-
const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
|
|
2124
|
-
const channels = await context.helpers.searchGuildChannels(guildId, query, {
|
|
2125
|
-
limit: Number.isFinite(limit) ? limit : void 0,
|
|
2126
|
-
nsfw,
|
|
2127
|
-
channelTypes
|
|
2128
|
-
});
|
|
2129
|
-
res.json({ channels });
|
|
2130
|
-
});
|
|
2131
|
-
router.get("/api/lookup/members", ensureAuthenticated, async (req, res) => {
|
|
2132
|
-
const context = createContext(req, options);
|
|
2133
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2134
|
-
if (!guildId) {
|
|
2135
|
-
res.status(400).json({ message: "guildId is required" });
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2139
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2140
|
-
const members = await context.helpers.searchGuildMembers(guildId, query, {
|
|
2141
|
-
limit: Number.isFinite(limit) ? limit : void 0
|
|
2142
|
-
});
|
|
2143
|
-
res.json({ members });
|
|
2144
|
-
});
|
|
2145
|
-
router.post("/api/home/:actionId", ensureAuthenticated, async (req, res) => {
|
|
2146
|
-
const context = createContext(req, options);
|
|
2147
|
-
const action = options.home?.actions?.[req.params.actionId];
|
|
2148
|
-
if (!action) {
|
|
2149
|
-
res.status(404).json({ ok: false, message: "Home action not found" });
|
|
2150
|
-
return;
|
|
2151
|
-
}
|
|
2152
|
-
const payload = req.body;
|
|
2153
|
-
if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
|
|
2154
|
-
res.status(400).json({ ok: false, message: "Invalid home action payload" });
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
let result;
|
|
2158
|
-
try {
|
|
2159
|
-
result = await action(context, {
|
|
2160
|
-
sectionId: payload.sectionId,
|
|
2161
|
-
values: payload.values
|
|
2162
|
-
});
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
const message = error instanceof Error ? error.message : "Home action failed";
|
|
2165
|
-
res.status(500).json({ ok: false, message });
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
res.json(result);
|
|
2169
|
-
});
|
|
2170
|
-
router.get("/api/plugins", ensureAuthenticated, async (req, res) => {
|
|
2171
|
-
const context = createContext(req, options);
|
|
2172
|
-
const activeScope = context.selectedGuildId ? "guild" : "user";
|
|
2173
|
-
const payload = [];
|
|
2174
|
-
for (const plugin of plugins) {
|
|
2175
|
-
const pluginScope = plugin.scope ?? "both";
|
|
2176
|
-
if (pluginScope !== "both" && pluginScope !== activeScope) {
|
|
2177
|
-
continue;
|
|
2178
|
-
}
|
|
2179
|
-
const panels = await plugin.getPanels(context);
|
|
2180
|
-
payload.push({
|
|
2181
|
-
id: plugin.id,
|
|
2182
|
-
name: plugin.name,
|
|
2183
|
-
description: plugin.description,
|
|
2184
|
-
panels
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
res.json({ plugins: payload });
|
|
2188
|
-
});
|
|
2189
|
-
router.post("/api/plugins/:pluginId/:actionId", ensureAuthenticated, async (req, res) => {
|
|
2190
|
-
const context = createContext(req, options);
|
|
2191
|
-
const plugin = plugins.find((item) => item.id === req.params.pluginId);
|
|
2192
|
-
if (!plugin) {
|
|
2193
|
-
res.status(404).json({ ok: false, message: "Plugin not found" });
|
|
2194
|
-
return;
|
|
2195
|
-
}
|
|
2196
|
-
const action = plugin.actions?.[req.params.actionId];
|
|
2197
|
-
if (!action) {
|
|
2198
|
-
res.status(404).json({ ok: false, message: "Action not found" });
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
let result;
|
|
2202
|
-
try {
|
|
2203
|
-
result = await action(context, req.body);
|
|
2204
|
-
} catch (error) {
|
|
2205
|
-
const message = error instanceof Error ? error.message : "Plugin action failed";
|
|
2206
|
-
res.status(500).json({ ok: false, message });
|
|
2207
|
-
return;
|
|
2208
|
-
}
|
|
2209
|
-
res.json(result);
|
|
2210
|
-
});
|
|
2211
|
-
app.use(basePath, router);
|
|
2212
|
-
let server;
|
|
2213
|
-
return {
|
|
2214
|
-
app,
|
|
2215
|
-
async start() {
|
|
2216
|
-
if (options.app) {
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
if (server) {
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
const port = options.port ?? 3e3;
|
|
2223
|
-
const host = options.host ?? "0.0.0.0";
|
|
2224
|
-
server = createServer(app);
|
|
2225
|
-
await new Promise((resolve) => {
|
|
2226
|
-
server.listen(port, host, () => resolve());
|
|
2227
|
-
});
|
|
2228
|
-
},
|
|
2229
|
-
async stop() {
|
|
2230
|
-
if (!server) {
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
await new Promise((resolve, reject) => {
|
|
2234
|
-
server.close((error) => {
|
|
2235
|
-
if (error) {
|
|
2236
|
-
reject(error);
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
resolve();
|
|
2240
|
-
});
|
|
2241
|
-
});
|
|
2242
|
-
server = void 0;
|
|
2243
|
-
}
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// src/designer.ts
|
|
1689
|
+
// src/handlers/DashboardDesigner.ts
|
|
2248
1690
|
var CategoryBuilder = class {
|
|
2249
1691
|
constructor(scope, categoryId, categoryLabel) {
|
|
2250
1692
|
this.scope = scope;
|
|
@@ -2266,11 +1708,7 @@ var CategoryBuilder = class {
|
|
|
2266
1708
|
return this;
|
|
2267
1709
|
}
|
|
2268
1710
|
buildCategory() {
|
|
2269
|
-
return {
|
|
2270
|
-
id: this.categoryId,
|
|
2271
|
-
label: this.categoryLabel,
|
|
2272
|
-
scope: this.scope
|
|
2273
|
-
};
|
|
1711
|
+
return { id: this.categoryId, label: this.categoryLabel, scope: this.scope };
|
|
2274
1712
|
}
|
|
2275
1713
|
buildSections() {
|
|
2276
1714
|
return [...this.sections];
|
|
@@ -2287,12 +1725,7 @@ var DashboardDesigner = class {
|
|
|
2287
1725
|
this.partialOptions = { ...baseOptions };
|
|
2288
1726
|
}
|
|
2289
1727
|
setup(input) {
|
|
2290
|
-
|
|
2291
|
-
if (input.botInvitePermissions) this.partialOptions.botInvitePermissions = input.botInvitePermissions;
|
|
2292
|
-
if (input.botInviteScopes) this.partialOptions.botInviteScopes = input.botInviteScopes;
|
|
2293
|
-
if (input.dashboardName) this.partialOptions.dashboardName = input.dashboardName;
|
|
2294
|
-
if (input.basePath) this.partialOptions.basePath = input.basePath;
|
|
2295
|
-
if (input.uiTemplate) this.partialOptions.uiTemplate = input.uiTemplate;
|
|
1728
|
+
Object.assign(this.partialOptions, input);
|
|
2296
1729
|
return this;
|
|
2297
1730
|
}
|
|
2298
1731
|
useTemplate(templateId) {
|
|
@@ -2336,11 +1769,7 @@ var DashboardDesigner = class {
|
|
|
2336
1769
|
const categoryId = input.categoryId ?? input.id;
|
|
2337
1770
|
this.pages.push({
|
|
2338
1771
|
pageId: input.id,
|
|
2339
|
-
category: {
|
|
2340
|
-
id: categoryId,
|
|
2341
|
-
label: input.label ?? input.title,
|
|
2342
|
-
scope
|
|
2343
|
-
},
|
|
1772
|
+
category: { id: categoryId, label: input.label ?? input.title, scope },
|
|
2344
1773
|
section: {
|
|
2345
1774
|
id: input.id,
|
|
2346
1775
|
title: input.title,
|
|
@@ -2354,10 +1783,6 @@ var DashboardDesigner = class {
|
|
|
2354
1783
|
});
|
|
2355
1784
|
return this;
|
|
2356
1785
|
}
|
|
2357
|
-
onHomeAction(actionId, handler) {
|
|
2358
|
-
this.homeActions[actionId] = handler;
|
|
2359
|
-
return this;
|
|
2360
|
-
}
|
|
2361
1786
|
onLoad(pageId, handler) {
|
|
2362
1787
|
this.loadHandlers[pageId] = handler;
|
|
2363
1788
|
return this;
|
|
@@ -2372,6 +1797,17 @@ var DashboardDesigner = class {
|
|
|
2372
1797
|
onsave(pageId, handler) {
|
|
2373
1798
|
return this.onSave(pageId, handler);
|
|
2374
1799
|
}
|
|
1800
|
+
onHomeAction(actionId, handler) {
|
|
1801
|
+
this.homeActions[actionId] = handler;
|
|
1802
|
+
return this;
|
|
1803
|
+
}
|
|
1804
|
+
customCss(cssString) {
|
|
1805
|
+
this.partialOptions.setupDesign = {
|
|
1806
|
+
...this.partialOptions.setupDesign ?? {},
|
|
1807
|
+
customCss: cssString
|
|
1808
|
+
};
|
|
1809
|
+
return this;
|
|
1810
|
+
}
|
|
2375
1811
|
build() {
|
|
2376
1812
|
const staticCategories = this.categories.map((item) => item.buildCategory());
|
|
2377
1813
|
const staticSections = this.categories.flatMap((item) => item.buildSections());
|
|
@@ -2380,20 +1816,14 @@ var DashboardDesigner = class {
|
|
|
2380
1816
|
const categoryMap = /* @__PURE__ */ new Map();
|
|
2381
1817
|
for (const category of [...staticCategories, ...pageCategories]) {
|
|
2382
1818
|
const key = `${category.scope}:${category.id}`;
|
|
2383
|
-
if (!categoryMap.has(key))
|
|
2384
|
-
categoryMap.set(key, category);
|
|
2385
|
-
}
|
|
1819
|
+
if (!categoryMap.has(key)) categoryMap.set(key, category);
|
|
2386
1820
|
}
|
|
2387
1821
|
const categories = [...categoryMap.values()];
|
|
2388
1822
|
const saveActionIds = {};
|
|
2389
1823
|
for (const section of baseSections) {
|
|
2390
|
-
if (this.saveHandlers[section.id]) {
|
|
2391
|
-
saveActionIds[section.id] = `save:${section.id}`;
|
|
2392
|
-
}
|
|
1824
|
+
if (this.saveHandlers[section.id]) saveActionIds[section.id] = `save:${section.id}`;
|
|
2393
1825
|
}
|
|
2394
|
-
const resolvedActions = {
|
|
2395
|
-
...this.homeActions
|
|
2396
|
-
};
|
|
1826
|
+
const resolvedActions = { ...this.homeActions };
|
|
2397
1827
|
for (const [sectionId, handler] of Object.entries(this.saveHandlers)) {
|
|
2398
1828
|
resolvedActions[saveActionIds[sectionId]] = handler;
|
|
2399
1829
|
}
|
|
@@ -2409,11 +1839,7 @@ var DashboardDesigner = class {
|
|
|
2409
1839
|
};
|
|
2410
1840
|
const saveActionId = saveActionIds[section.id];
|
|
2411
1841
|
if (saveActionId && !section.actions?.some((action) => action.id === saveActionId)) {
|
|
2412
|
-
section.actions = [...section.actions ?? [], {
|
|
2413
|
-
id: saveActionId,
|
|
2414
|
-
label: "Save",
|
|
2415
|
-
variant: "primary"
|
|
2416
|
-
}];
|
|
1842
|
+
section.actions = [...section.actions ?? [], { id: saveActionId, label: "Save", variant: "primary" }];
|
|
2417
1843
|
}
|
|
2418
1844
|
const loadHandler = this.loadHandlers[section.id];
|
|
2419
1845
|
if (loadHandler) {
|
|
@@ -2438,16 +1864,451 @@ var DashboardDesigner = class {
|
|
|
2438
1864
|
home
|
|
2439
1865
|
};
|
|
2440
1866
|
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Builds the configuration and immediately instantiates the Dashboard.
|
|
1869
|
+
*/
|
|
1870
|
+
createDashboard() {
|
|
1871
|
+
const options = this.build();
|
|
1872
|
+
return new DiscordDashboard(options);
|
|
1873
|
+
}
|
|
2441
1874
|
};
|
|
2442
|
-
|
|
2443
|
-
|
|
1875
|
+
|
|
1876
|
+
// src/index.ts
|
|
1877
|
+
var DISCORD_API = "https://discord.com/api/v10";
|
|
1878
|
+
var MANAGE_GUILD_PERMISSION = 0x20n;
|
|
1879
|
+
var ADMIN_PERMISSION = 0x8n;
|
|
1880
|
+
function normalizeBasePath(basePath) {
|
|
1881
|
+
if (!basePath || basePath === "/") return "/dashboard";
|
|
1882
|
+
return basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
1883
|
+
}
|
|
1884
|
+
function canManageGuild(permissions) {
|
|
1885
|
+
const value = BigInt(permissions);
|
|
1886
|
+
return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
|
|
1887
|
+
}
|
|
1888
|
+
function toQuery(params) {
|
|
1889
|
+
const url = new URLSearchParams();
|
|
1890
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1891
|
+
url.set(key, value);
|
|
1892
|
+
}
|
|
1893
|
+
return url.toString();
|
|
1894
|
+
}
|
|
1895
|
+
async function fetchDiscord(path, token) {
|
|
1896
|
+
const response = await fetch(`${DISCORD_API}${path}`, {
|
|
1897
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1898
|
+
});
|
|
1899
|
+
if (!response.ok) throw new Error(`Discord API request failed (${response.status})`);
|
|
1900
|
+
return await response.json();
|
|
1901
|
+
}
|
|
1902
|
+
function getUserAvatarUrl(user) {
|
|
1903
|
+
if (user.avatar) {
|
|
1904
|
+
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
|
1905
|
+
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
|
|
1906
|
+
}
|
|
1907
|
+
const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
|
|
1908
|
+
return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
|
|
2444
1909
|
}
|
|
1910
|
+
function getGuildIconUrl(guild) {
|
|
1911
|
+
if (!guild.icon) return null;
|
|
1912
|
+
const ext = guild.icon.startsWith("a_") ? "gif" : "png";
|
|
1913
|
+
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
|
|
1914
|
+
}
|
|
1915
|
+
var DiscordDashboard = class {
|
|
1916
|
+
app;
|
|
1917
|
+
options;
|
|
1918
|
+
helpers;
|
|
1919
|
+
router;
|
|
1920
|
+
server;
|
|
1921
|
+
basePath;
|
|
1922
|
+
templateRenderer;
|
|
1923
|
+
plugins;
|
|
1924
|
+
constructor(options) {
|
|
1925
|
+
this.validateOptions(options);
|
|
1926
|
+
this.options = options;
|
|
1927
|
+
this.app = options.app ?? express();
|
|
1928
|
+
this.router = express.Router();
|
|
1929
|
+
this.helpers = new DiscordHelpers(options.botToken);
|
|
1930
|
+
this.basePath = normalizeBasePath(options.basePath);
|
|
1931
|
+
this.templateRenderer = this.resolveTemplateRenderer();
|
|
1932
|
+
this.plugins = options.plugins ?? [];
|
|
1933
|
+
if (!options.app && options.trustProxy !== void 0) {
|
|
1934
|
+
this.app.set("trust proxy", options.trustProxy);
|
|
1935
|
+
}
|
|
1936
|
+
this.setupMiddleware();
|
|
1937
|
+
this.setupRoutes();
|
|
1938
|
+
this.app.use(this.basePath, this.router);
|
|
1939
|
+
}
|
|
1940
|
+
validateOptions(options) {
|
|
1941
|
+
if (!options.botToken) throw new Error("botToken is required");
|
|
1942
|
+
if (!options.clientId) throw new Error("clientId is required");
|
|
1943
|
+
if (!options.clientSecret) throw new Error("clientSecret is required");
|
|
1944
|
+
if (!options.redirectUri) throw new Error("redirectUri is required");
|
|
1945
|
+
if (!options.sessionSecret) throw new Error("sessionSecret is required");
|
|
1946
|
+
}
|
|
1947
|
+
setupMiddleware() {
|
|
1948
|
+
this.router.use(compression());
|
|
1949
|
+
this.router.use(helmet({ contentSecurityPolicy: false }));
|
|
1950
|
+
this.router.use(express.json());
|
|
1951
|
+
this.router.use(
|
|
1952
|
+
session({
|
|
1953
|
+
name: this.options.sessionName ?? "discord_dashboard.sid",
|
|
1954
|
+
secret: this.options.sessionSecret,
|
|
1955
|
+
resave: false,
|
|
1956
|
+
saveUninitialized: false,
|
|
1957
|
+
cookie: {
|
|
1958
|
+
httpOnly: true,
|
|
1959
|
+
sameSite: "lax",
|
|
1960
|
+
maxAge: this.options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
|
|
1961
|
+
}
|
|
1962
|
+
})
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
setupRoutes() {
|
|
1966
|
+
this.router.get("/", this.handleRoot.bind(this));
|
|
1967
|
+
this.router.get("/login", this.handleLogin.bind(this));
|
|
1968
|
+
this.router.get("/callback", this.handleCallback.bind(this));
|
|
1969
|
+
this.router.post("/logout", this.handleLogout.bind(this));
|
|
1970
|
+
this.router.get("/api/session", this.handleSession.bind(this));
|
|
1971
|
+
this.router.get("/api/guilds", this.ensureAuthenticated, this.handleGuilds.bind(this));
|
|
1972
|
+
this.router.get("/api/overview", this.ensureAuthenticated, this.handleOverview.bind(this));
|
|
1973
|
+
this.router.get("/api/home/categories", this.ensureAuthenticated, this.handleHomeCategories.bind(this));
|
|
1974
|
+
this.router.get("/api/home", this.ensureAuthenticated, this.handleHome.bind(this));
|
|
1975
|
+
this.router.get("/api/lookup/roles", this.ensureAuthenticated, this.handleLookupRoles.bind(this));
|
|
1976
|
+
this.router.get("/api/lookup/channels", this.ensureAuthenticated, this.handleLookupChannels.bind(this));
|
|
1977
|
+
this.router.get("/api/lookup/members", this.ensureAuthenticated, this.handleLookupMembers.bind(this));
|
|
1978
|
+
this.router.post("/api/home/:actionId", this.ensureAuthenticated, this.handleHomeAction.bind(this));
|
|
1979
|
+
this.router.get("/api/plugins", this.ensureAuthenticated, this.handlePlugins.bind(this));
|
|
1980
|
+
this.router.post("/api/plugins/:pluginId/:actionId", this.ensureAuthenticated, this.handlePluginAction.bind(this));
|
|
1981
|
+
}
|
|
1982
|
+
// --- Start/Stop Methods ---
|
|
1983
|
+
async start() {
|
|
1984
|
+
if (this.options.app || this.server) return;
|
|
1985
|
+
const port = this.options.port ?? 3e3;
|
|
1986
|
+
const host = this.options.host ?? "0.0.0.0";
|
|
1987
|
+
this.server = createServer(this.app);
|
|
1988
|
+
return new Promise((resolve) => {
|
|
1989
|
+
this.server.listen(port, host, () => resolve());
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
async stop() {
|
|
1993
|
+
if (!this.server) return;
|
|
1994
|
+
return new Promise((resolve, reject) => {
|
|
1995
|
+
this.server.close((error) => {
|
|
1996
|
+
if (error) return reject(error);
|
|
1997
|
+
resolve();
|
|
1998
|
+
});
|
|
1999
|
+
this.server = void 0;
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
// --- Route Handlers ---
|
|
2003
|
+
handleRoot(req, res) {
|
|
2004
|
+
if (!req.session.discordAuth) {
|
|
2005
|
+
res.redirect(`${this.basePath}/login`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
res.setHeader("Cache-Control", "no-store");
|
|
2009
|
+
res.type("html").send(
|
|
2010
|
+
this.templateRenderer({
|
|
2011
|
+
dashboardName: this.options.dashboardName ?? "Discord Dashboard",
|
|
2012
|
+
basePath: this.basePath,
|
|
2013
|
+
setupDesign: this.options.setupDesign
|
|
2014
|
+
})
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
handleLogin(req, res) {
|
|
2018
|
+
const state = randomBytes(16).toString("hex");
|
|
2019
|
+
req.session.oauthState = state;
|
|
2020
|
+
const scope = (this.options.scopes && this.options.scopes.length > 0 ? this.options.scopes : ["identify", "guilds"]).join(" ");
|
|
2021
|
+
const query = toQuery({
|
|
2022
|
+
client_id: this.options.clientId,
|
|
2023
|
+
redirect_uri: this.options.redirectUri,
|
|
2024
|
+
response_type: "code",
|
|
2025
|
+
scope,
|
|
2026
|
+
state,
|
|
2027
|
+
prompt: "none"
|
|
2028
|
+
});
|
|
2029
|
+
res.redirect(`https://discord.com/oauth2/authorize?${query}`);
|
|
2030
|
+
}
|
|
2031
|
+
async handleCallback(req, res) {
|
|
2032
|
+
try {
|
|
2033
|
+
const code = typeof req.query.code === "string" ? req.query.code : void 0;
|
|
2034
|
+
const state = typeof req.query.state === "string" ? req.query.state : void 0;
|
|
2035
|
+
if (!code || !state) return res.status(400).send("Missing OAuth2 code/state");
|
|
2036
|
+
if (!req.session.oauthState || req.session.oauthState !== state) return res.status(403).send("Invalid OAuth2 state");
|
|
2037
|
+
const tokenData = await this.exchangeCodeForToken(code);
|
|
2038
|
+
const [user, guilds] = await Promise.all([fetchDiscord("/users/@me", tokenData.access_token), fetchDiscord("/users/@me/guilds", tokenData.access_token)]);
|
|
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(this.basePath);
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
const message = error instanceof Error ? error.message : "OAuth callback failed";
|
|
2050
|
+
res.status(500).send(message);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
handleLogout(req, res) {
|
|
2054
|
+
req.session.destroy((sessionError) => {
|
|
2055
|
+
if (sessionError) return res.status(500).json({ ok: false, message: "Failed to destroy session" });
|
|
2056
|
+
res.clearCookie(this.options.sessionName ?? "discord_dashboard.sid");
|
|
2057
|
+
res.json({ ok: true });
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
handleSession(req, res) {
|
|
2061
|
+
const auth = req.session.discordAuth;
|
|
2062
|
+
if (!auth) return res.status(200).json({ authenticated: false });
|
|
2063
|
+
const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2064
|
+
res.json({
|
|
2065
|
+
authenticated: true,
|
|
2066
|
+
user: { ...auth.user, avatarUrl: getUserAvatarUrl(auth.user) },
|
|
2067
|
+
guildCount: manageableGuildCount,
|
|
2068
|
+
expiresAt: auth.expiresAt
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
async handleGuilds(req, res) {
|
|
2072
|
+
const context = this.createContext(req);
|
|
2073
|
+
if (this.options.ownerIds && this.options.ownerIds.length > 0 && !this.options.ownerIds.includes(context.user.id)) {
|
|
2074
|
+
return res.status(403).json({ message: "You are not allowed to access this dashboard." });
|
|
2075
|
+
}
|
|
2076
|
+
let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
|
|
2077
|
+
if (this.options.guildFilter) {
|
|
2078
|
+
const filtered = [];
|
|
2079
|
+
for (const guild of manageableGuilds) {
|
|
2080
|
+
if (await this.options.guildFilter(guild, context)) filtered.push(guild);
|
|
2081
|
+
}
|
|
2082
|
+
manageableGuilds = filtered;
|
|
2083
|
+
}
|
|
2084
|
+
const botGuildIds = await this.fetchBotGuildIds();
|
|
2085
|
+
const enrichedGuilds = manageableGuilds.map((guild) => {
|
|
2086
|
+
const botInGuild = botGuildIds.has(guild.id);
|
|
2087
|
+
return {
|
|
2088
|
+
...guild,
|
|
2089
|
+
iconUrl: getGuildIconUrl(guild),
|
|
2090
|
+
botInGuild,
|
|
2091
|
+
inviteUrl: botInGuild ? void 0 : this.createGuildInviteUrl(guild.id)
|
|
2092
|
+
};
|
|
2093
|
+
});
|
|
2094
|
+
res.json({ guilds: enrichedGuilds });
|
|
2095
|
+
}
|
|
2096
|
+
async handleOverview(req, res) {
|
|
2097
|
+
const context = this.createContext(req);
|
|
2098
|
+
const cards = await this.resolveOverviewCards(context);
|
|
2099
|
+
res.json({ cards });
|
|
2100
|
+
}
|
|
2101
|
+
async handleHomeCategories(req, res) {
|
|
2102
|
+
const context = this.createContext(req);
|
|
2103
|
+
const activeScope = this.resolveScope(context);
|
|
2104
|
+
const categories = await this.resolveHomeCategories(context);
|
|
2105
|
+
const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
|
|
2106
|
+
res.json({ categories: visible, activeScope });
|
|
2107
|
+
}
|
|
2108
|
+
async handleHome(req, res) {
|
|
2109
|
+
const context = this.createContext(req);
|
|
2110
|
+
const activeScope = this.resolveScope(context);
|
|
2111
|
+
const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
|
|
2112
|
+
let sections = await this.resolveHomeSections(context);
|
|
2113
|
+
sections = sections.filter((section) => {
|
|
2114
|
+
const sectionScope = section.scope ?? activeScope;
|
|
2115
|
+
if (sectionScope !== "setup" && sectionScope !== activeScope) return false;
|
|
2116
|
+
if (!categoryId) return true;
|
|
2117
|
+
return section.categoryId === categoryId;
|
|
2118
|
+
});
|
|
2119
|
+
res.json({ sections, activeScope });
|
|
2120
|
+
}
|
|
2121
|
+
async handleLookupRoles(req, res) {
|
|
2122
|
+
const context = this.createContext(req);
|
|
2123
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2124
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2125
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2126
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2127
|
+
const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
|
|
2128
|
+
const roles = await context.helpers.searchGuildRoles(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, includeManaged });
|
|
2129
|
+
res.json({ roles });
|
|
2130
|
+
}
|
|
2131
|
+
async handleLookupChannels(req, res) {
|
|
2132
|
+
const context = this.createContext(req);
|
|
2133
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2134
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2135
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2136
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2137
|
+
const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
|
|
2138
|
+
const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
|
|
2139
|
+
const channels = await context.helpers.searchGuildChannels(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, nsfw, channelTypes });
|
|
2140
|
+
res.json({ channels });
|
|
2141
|
+
}
|
|
2142
|
+
async handleLookupMembers(req, res) {
|
|
2143
|
+
const context = this.createContext(req);
|
|
2144
|
+
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2145
|
+
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2146
|
+
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2147
|
+
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2148
|
+
const members = await context.helpers.searchGuildMembers(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0 });
|
|
2149
|
+
res.json({ members });
|
|
2150
|
+
}
|
|
2151
|
+
async handleHomeAction(req, res) {
|
|
2152
|
+
const context = this.createContext(req);
|
|
2153
|
+
const action = this.options.home?.actions?.[req.params.actionId];
|
|
2154
|
+
if (!action) return res.status(404).json({ ok: false, message: "Home action not found" });
|
|
2155
|
+
const payload = req.body;
|
|
2156
|
+
if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
|
|
2157
|
+
return res.status(400).json({ ok: false, message: "Invalid home action payload" });
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
const result = await action(context, { sectionId: payload.sectionId, values: payload.values });
|
|
2161
|
+
res.json(result);
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Home action failed" });
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
async handlePlugins(req, res) {
|
|
2167
|
+
const context = this.createContext(req);
|
|
2168
|
+
const activeScope = context.selectedGuildId ? "guild" : "user";
|
|
2169
|
+
const payload = [];
|
|
2170
|
+
for (const plugin of this.plugins) {
|
|
2171
|
+
const pluginScope = plugin.scope ?? "both";
|
|
2172
|
+
if (pluginScope !== "both" && pluginScope !== activeScope) continue;
|
|
2173
|
+
const panels = await plugin.getPanels(context);
|
|
2174
|
+
payload.push({ id: plugin.id, name: plugin.name, description: plugin.description, panels });
|
|
2175
|
+
}
|
|
2176
|
+
res.json({ plugins: payload });
|
|
2177
|
+
}
|
|
2178
|
+
async handlePluginAction(req, res) {
|
|
2179
|
+
const context = this.createContext(req);
|
|
2180
|
+
const plugin = this.plugins.find((item) => item.id === req.params.pluginId);
|
|
2181
|
+
if (!plugin) return res.status(404).json({ ok: false, message: "Plugin not found" });
|
|
2182
|
+
const action = plugin.actions?.[req.params.actionId];
|
|
2183
|
+
if (!action) return res.status(404).json({ ok: false, message: "Action not found" });
|
|
2184
|
+
try {
|
|
2185
|
+
const result = await action(context, req.body);
|
|
2186
|
+
res.json(result);
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Plugin action failed" });
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
// --- Internal Dashboard Resolvers & Helpers ---
|
|
2192
|
+
ensureAuthenticated = (req, res, next) => {
|
|
2193
|
+
if (!req.session.discordAuth) {
|
|
2194
|
+
res.status(401).json({ authenticated: false, message: "Authentication required" });
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
next();
|
|
2198
|
+
};
|
|
2199
|
+
createContext(req) {
|
|
2200
|
+
const auth = req.session.discordAuth;
|
|
2201
|
+
if (!auth) throw new Error("Not authenticated");
|
|
2202
|
+
return {
|
|
2203
|
+
user: auth.user,
|
|
2204
|
+
guilds: auth.guilds,
|
|
2205
|
+
accessToken: auth.accessToken,
|
|
2206
|
+
selectedGuildId: typeof req.query.guildId === "string" ? req.query.guildId : void 0,
|
|
2207
|
+
helpers: this.helpers
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
async exchangeCodeForToken(code) {
|
|
2211
|
+
const response = await fetch(`${DISCORD_API}/oauth2/token`, {
|
|
2212
|
+
method: "POST",
|
|
2213
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2214
|
+
body: toQuery({
|
|
2215
|
+
client_id: this.options.clientId,
|
|
2216
|
+
client_secret: this.options.clientSecret,
|
|
2217
|
+
grant_type: "authorization_code",
|
|
2218
|
+
code,
|
|
2219
|
+
redirect_uri: this.options.redirectUri
|
|
2220
|
+
})
|
|
2221
|
+
});
|
|
2222
|
+
if (!response.ok) throw new Error(`Failed token exchange: ${response.status} ${await response.text()}`);
|
|
2223
|
+
return await response.json();
|
|
2224
|
+
}
|
|
2225
|
+
async resolveOverviewCards(context) {
|
|
2226
|
+
if (this.options.getOverviewCards) return await this.options.getOverviewCards(context);
|
|
2227
|
+
const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2228
|
+
return [
|
|
2229
|
+
{ id: "user", title: "Logged-in User", value: context.user.global_name || context.user.username, subtitle: `ID: ${context.user.id}`, intent: "info" },
|
|
2230
|
+
{ id: "guilds", title: "Manageable Guilds", value: manageableGuildCount, subtitle: "Owner or Manage Server permissions", intent: "success" },
|
|
2231
|
+
{ id: "plugins", title: "Plugins Loaded", value: this.plugins.length, subtitle: "Dynamic server modules", intent: "neutral" }
|
|
2232
|
+
];
|
|
2233
|
+
}
|
|
2234
|
+
async resolveHomeSections(context) {
|
|
2235
|
+
const customSections = this.options.home?.getSections ? await this.options.home.getSections(context) : [];
|
|
2236
|
+
const overviewSections = this.options.home?.getOverviewSections ? await this.options.home.getOverviewSections(context) : [];
|
|
2237
|
+
if (customSections.length > 0 || overviewSections.length > 0) {
|
|
2238
|
+
const normalizedOverview = overviewSections.map((section) => ({ ...section, categoryId: section.categoryId ?? "overview" }));
|
|
2239
|
+
return [...normalizedOverview, ...customSections];
|
|
2240
|
+
}
|
|
2241
|
+
const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
|
|
2242
|
+
return [
|
|
2243
|
+
{
|
|
2244
|
+
id: "setup",
|
|
2245
|
+
title: "Setup Details",
|
|
2246
|
+
description: "Core dashboard setup information",
|
|
2247
|
+
scope: "setup",
|
|
2248
|
+
categoryId: "setup",
|
|
2249
|
+
fields: [
|
|
2250
|
+
{ id: "dashboardName", label: "Dashboard Name", type: "text", value: this.options.dashboardName ?? "Discord Dashboard", readOnly: true },
|
|
2251
|
+
{ id: "basePath", label: "Base Path", type: "text", value: this.basePath, readOnly: true }
|
|
2252
|
+
]
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
id: "context",
|
|
2256
|
+
title: "Dashboard Context",
|
|
2257
|
+
description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
|
|
2258
|
+
scope: this.resolveScope(context),
|
|
2259
|
+
categoryId: "overview",
|
|
2260
|
+
fields: [
|
|
2261
|
+
{ id: "mode", label: "Mode", type: "text", value: selectedGuild ? "Guild" : "User", readOnly: true },
|
|
2262
|
+
{ id: "target", label: "Target", type: "text", value: selectedGuild ? selectedGuild.name : context.user.username, readOnly: true }
|
|
2263
|
+
]
|
|
2264
|
+
}
|
|
2265
|
+
];
|
|
2266
|
+
}
|
|
2267
|
+
resolveScope(context) {
|
|
2268
|
+
return context.selectedGuildId ? "guild" : "user";
|
|
2269
|
+
}
|
|
2270
|
+
async resolveHomeCategories(context) {
|
|
2271
|
+
if (this.options.home?.getCategories) {
|
|
2272
|
+
const categories = await this.options.home.getCategories(context);
|
|
2273
|
+
return [...categories].sort((a, b) => a.id === "overview" ? -1 : b.id === "overview" ? 1 : 0);
|
|
2274
|
+
}
|
|
2275
|
+
return [
|
|
2276
|
+
{ id: "overview", label: "Overview", scope: this.resolveScope(context) },
|
|
2277
|
+
{ id: "setup", label: "Setup", scope: "setup" }
|
|
2278
|
+
];
|
|
2279
|
+
}
|
|
2280
|
+
createGuildInviteUrl(guildId) {
|
|
2281
|
+
const scopes = this.options.botInviteScopes && this.options.botInviteScopes.length > 0 ? this.options.botInviteScopes : ["bot", "applications.commands"];
|
|
2282
|
+
return `https://discord.com/oauth2/authorize?${toQuery({
|
|
2283
|
+
client_id: this.options.clientId,
|
|
2284
|
+
scope: scopes.join(" "),
|
|
2285
|
+
permissions: this.options.botInvitePermissions ?? "8",
|
|
2286
|
+
guild_id: guildId,
|
|
2287
|
+
disable_guild_select: "true"
|
|
2288
|
+
})}`;
|
|
2289
|
+
}
|
|
2290
|
+
async fetchBotGuildIds() {
|
|
2291
|
+
const response = await fetch(`${DISCORD_API}/users/@me/guilds`, { headers: { Authorization: `Bot ${this.options.botToken}` } });
|
|
2292
|
+
if (!response.ok) return /* @__PURE__ */ new Set();
|
|
2293
|
+
const guilds = await response.json();
|
|
2294
|
+
return new Set(guilds.map((guild) => guild.id));
|
|
2295
|
+
}
|
|
2296
|
+
resolveTemplateRenderer() {
|
|
2297
|
+
const selectedTemplate = this.options.uiTemplate ?? "default";
|
|
2298
|
+
const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
|
|
2299
|
+
const customRenderer = this.options.uiTemplates?.[selectedTemplate];
|
|
2300
|
+
if (customRenderer) return customRenderer;
|
|
2301
|
+
const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
|
|
2302
|
+
if (builtinRenderer) return builtinRenderer;
|
|
2303
|
+
if (selectedTemplate !== "default") throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
|
|
2304
|
+
return defaultRenderer;
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2445
2307
|
export {
|
|
2446
2308
|
DashboardDesigner,
|
|
2309
|
+
DiscordDashboard,
|
|
2310
|
+
DiscordHelpers,
|
|
2447
2311
|
builtinTemplateRenderers,
|
|
2448
|
-
createDashboard,
|
|
2449
|
-
createDashboardDesigner,
|
|
2450
|
-
createDiscordHelpers,
|
|
2451
2312
|
getBuiltinTemplateRenderer
|
|
2452
2313
|
};
|
|
2453
2314
|
//# sourceMappingURL=index.js.map
|