@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/dist/index.js CHANGED
@@ -1,88 +1,90 @@
1
- // src/dashboard.ts
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/discord-helpers.ts
10
- var DISCORD_API = "https://discord.com/api/v10";
11
- async function fetchDiscordWithBot(botToken, path) {
12
- const response = await fetch(`${DISCORD_API}${path}`, {
13
- headers: {
14
- Authorization: `Bot ${botToken}`
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
- return await response.json();
21
- }
22
- function createDiscordHelpers(botToken) {
23
- return {
24
- async getChannel(channelId) {
25
- return await fetchDiscordWithBot(botToken, `/channels/${channelId}`);
26
- },
27
- async getGuildChannels(guildId) {
28
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/channels`) ?? [];
29
- },
30
- async searchGuildChannels(guildId, query, options) {
31
- const channels = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/channels`) ?? [];
32
- const normalizedQuery = query.trim().toLowerCase();
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
- return roles.find((role) => role.id === roleId) ?? null;
53
- },
54
- async getGuildRoles(guildId) {
55
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`) ?? [];
56
- },
57
- async searchGuildRoles(guildId, query, options) {
58
- const roles = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`) ?? [];
59
- const normalizedQuery = query.trim().toLowerCase();
60
- const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
61
- return roles.filter((role) => {
62
- if (!options?.includeManaged && role.managed) {
63
- return false;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
@@ -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
- button.className = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
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
- button.className = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
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; cursor: grab; font-size: .9rem; }
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/dashboard.ts
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
- if (input.ownerIds) this.partialOptions.ownerIds = input.ownerIds;
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
- function createDashboardDesigner(baseOptions) {
2443
- return new DashboardDesigner(baseOptions);
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