feat: Agregar API para obtener roles de un gremio y recargar roles en el panel de control
This commit is contained in:
@@ -1069,6 +1069,89 @@ export const server = createServer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard routes
|
// Dashboard routes
|
||||||
|
// Runtime API to fetch roles for a guild (used by client-side fallback)
|
||||||
|
// GET /api/dashboard/:guildId/roles
|
||||||
|
if (
|
||||||
|
url.pathname.startsWith("/api/dashboard/") &&
|
||||||
|
url.pathname.endsWith("/roles") &&
|
||||||
|
req.method === "GET"
|
||||||
|
) {
|
||||||
|
// path like /api/dashboard/<guildId>/roles
|
||||||
|
const parts = url.pathname.split("/").filter(Boolean);
|
||||||
|
// parts[0] === 'api', parts[1] === 'dashboard', parts[2] === '<guildId>', parts[3] === 'roles'
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
const gid = parts[2];
|
||||||
|
const botToken = process.env.DISCORD_BOT_TOKEN ?? process.env.TOKEN;
|
||||||
|
if (!botToken) {
|
||||||
|
res.writeHead(
|
||||||
|
403,
|
||||||
|
applySecurityHeadersForRequest(req, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({ ok: false, error: "no bot token configured" })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rolesRes = await fetch(
|
||||||
|
`https://discord.com/api/guilds/${encodeURIComponent(
|
||||||
|
String(gid)
|
||||||
|
)}/roles`,
|
||||||
|
{ headers: { Authorization: `Bot ${botToken}` } }
|
||||||
|
);
|
||||||
|
if (!rolesRes.ok) {
|
||||||
|
res.writeHead(
|
||||||
|
rolesRes.status,
|
||||||
|
applySecurityHeadersForRequest(req, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
status: rolesRes.status,
|
||||||
|
statusText: rolesRes.statusText,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rolesJson = await rolesRes.json();
|
||||||
|
const mapped = Array.isArray(rolesJson)
|
||||||
|
? rolesJson.map((r: any) => ({
|
||||||
|
id: String(r.id),
|
||||||
|
name: String(r.name || r.id),
|
||||||
|
color:
|
||||||
|
typeof r.color !== "undefined" && r.color !== null
|
||||||
|
? "#" + ("000000" + r.color.toString(16)).slice(-6)
|
||||||
|
: r.colorHex || r.hex
|
||||||
|
? "#" + String(r.colorHex || r.hex).replace(/^#?/, "")
|
||||||
|
: "#8b95a0",
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
res.writeHead(
|
||||||
|
200,
|
||||||
|
applySecurityHeadersForRequest(req, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({ ok: true, count: mapped.length, roles: mapped })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(
|
||||||
|
500,
|
||||||
|
applySecurityHeadersForRequest(req, {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
res.end(JSON.stringify({ ok: false, error: String(err) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Dev-only helper: fetch roles for a guild (requires COLLAB_TEST=1 and DISCORD_BOT_TOKEN)
|
// Dev-only helper: fetch roles for a guild (requires COLLAB_TEST=1 and DISCORD_BOT_TOKEN)
|
||||||
if (url.pathname.startsWith("/__dev/fetch-roles")) {
|
if (url.pathname.startsWith("/__dev/fetch-roles")) {
|
||||||
if (process.env.COLLAB_TEST !== "1") {
|
if (process.env.COLLAB_TEST !== "1") {
|
||||||
@@ -1230,7 +1313,7 @@ export const server = createServer(
|
|||||||
if (url.pathname === "/dashboard" || url.pathname === "/dashboard/") {
|
if (url.pathname === "/dashboard" || url.pathname === "/dashboard/") {
|
||||||
// determine whether bot is in each guild (if we have a bot token)
|
// determine whether bot is in each guild (if we have a bot token)
|
||||||
try {
|
try {
|
||||||
const botToken = process.env.TOKEN; // No existe env var DISCORD_BOT_TOKEN si no TOKEN (compatibilidad)
|
const botToken = process.env.DISCORD_BOT_TOKEN ?? process.env.TOKEN; // prefer DISCORD_BOT_TOKEN, fallback to TOKEN
|
||||||
if (botToken && Array.isArray(guilds) && guilds.length) {
|
if (botToken && Array.isArray(guilds) && guilds.length) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
guilds.map(async (g: any) => {
|
guilds.map(async (g: any) => {
|
||||||
@@ -1295,7 +1378,7 @@ export const server = createServer(
|
|||||||
// Attempt to fetch guild roles via Discord Bot API if token available
|
// Attempt to fetch guild roles via Discord Bot API if token available
|
||||||
let guildRoles: Array<{ id: string; name: string }> = [];
|
let guildRoles: Array<{ id: string; name: string }> = [];
|
||||||
try {
|
try {
|
||||||
const botToken = process.env.TOKEN; // No existe env var DISCORD_BOT_TOKEN si no TOKEN (compatibilidad)
|
const botToken = process.env.DISCORD_BOT_TOKEN ?? process.env.TOKEN; // prefer DISCORD_BOT_TOKEN, fallback to TOKEN
|
||||||
if (botToken) {
|
if (botToken) {
|
||||||
const rolesRes = await fetch(
|
const rolesRes = await fetch(
|
||||||
`https://discord.com/api/guilds/${encodeURIComponent(
|
`https://discord.com/api/guilds/${encodeURIComponent(
|
||||||
@@ -1309,6 +1392,12 @@ export const server = createServer(
|
|||||||
guildRoles = rolesJson.map((r: any) => ({
|
guildRoles = rolesJson.map((r: any) => ({
|
||||||
id: String(r.id),
|
id: String(r.id),
|
||||||
name: String(r.name || r.id),
|
name: String(r.name || r.id),
|
||||||
|
color:
|
||||||
|
typeof r.color !== "undefined" && r.color !== null
|
||||||
|
? "#" + ("000000" + r.color.toString(16)).slice(-6)
|
||||||
|
: r.colorHex || r.hex
|
||||||
|
? "#" + String(r.colorHex || r.hex).replace(/^#?/, "")
|
||||||
|
: "#8b95a0",
|
||||||
}));
|
}));
|
||||||
// Debug: log number of roles fetched for observability in dev
|
// Debug: log number of roles fetched for observability in dev
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,22 +18,23 @@
|
|||||||
<% const selectedStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
|
<% const selectedStaff = (guildConfig && Array.isArray(guildConfig.staff) ? guildConfig.staff.map(String) : (guildConfig && guildConfig.staff ? String(guildConfig.staff).split(',') : [])) || []; %>
|
||||||
<% if (typeof guildRoles !== 'undefined' && guildRoles && guildRoles.length) { %>
|
<% if (typeof guildRoles !== 'undefined' && guildRoles && guildRoles.length) { %>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div id="staffTagsContainer" class="w-full rounded p-3 flex items-center gap-2 flex-wrap">
|
<div id="staffTagsContainer" class="w-full rounded p-3 flex flex-col sm:flex-row items-start sm:items-center gap-2">
|
||||||
<div id="staffChips" class="flex items-center gap-2 flex-wrap"></div>
|
<div id="staffChips" class="flex-1 min-w-0 flex items-center gap-2 flex-wrap"></div>
|
||||||
<button id="openRolePicker" type="button" class="ml-2 self-stretch inline-flex items-center justify-center px-3 py-1 rounded bg-white/6 hover:bg-white/10">
|
<button id="openRolePicker" type="button" class="ml-0 sm:ml-2 inline-flex items-center justify-center px-3 py-1 rounded bg-white/6 hover:bg-white/10">
|
||||||
<!-- plus svg -->
|
<!-- plus svg -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<input type="hidden" id="staffHidden" name="staff" value="<%= selectedStaff.join(',') %>" />
|
<input type="hidden" id="staffHidden" name="staff" value="<%= selectedStaff.join(',') %>" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-300 mt-1">Pulsa + para seleccionar roles del servidor.</div>
|
|
||||||
|
|
||||||
<!-- Role picker modal (hidden by default) -->
|
<!-- Role picker modal (hidden by default) -->
|
||||||
<div id="rolePickerModal" class="fixed inset-0 flex items-center justify-center z-50 hidden">
|
<div id="rolePickerModal" class="fixed inset-0 flex items-end sm:items-center justify-center z-50 hidden">
|
||||||
<div class="absolute inset-0" id="rolePickerBackdrop"></div>
|
<div class="absolute inset-0 bg-black/50" id="rolePickerBackdrop"></div>
|
||||||
<div class="relative w-full max-w-md bg-gray-800 rounded p-4 shadow-lg glass-card">
|
<div class="relative w-full sm:w-auto max-w-md bg-gray-800 rounded-t-lg sm:rounded p-4 shadow-lg glass-card mx-2 sm:mx-0" style="max-height:85vh; overflow:auto;">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<strong class="text-sm">Seleccionar roles</strong>
|
<strong class="text-sm">Seleccionar roles</strong>
|
||||||
|
<button id="closeRolePicker" type="button" class="text-slate-300">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<input id="rolePickerSearch" class="w-full rounded p-2 bg-transparent border-2 border-dashed mb-2" placeholder="Filtrar roles..." />
|
<input id="rolePickerSearch" class="w-full rounded p-2 bg-transparent border-2 border-dashed mb-2" placeholder="Filtrar roles..." />
|
||||||
<div id="rolePickerList" class="max-h-60 overflow-auto rounded bg-white/4 p-1"></div>
|
<div id="rolePickerList" class="max-h-60 overflow-auto rounded bg-white/4 p-1"></div>
|
||||||
@@ -128,13 +129,47 @@
|
|||||||
return { id: String(r.id), name: String(r.name || r.id), color: c };
|
return { id: String(r.id), name: String(r.name || r.id), color: c };
|
||||||
});
|
});
|
||||||
%>
|
%>
|
||||||
const ROLE_OPTIONS = <%- JSON.stringify(_roles) %>;
|
let ROLE_OPTIONS = <%- JSON.stringify(_roles) %>;
|
||||||
// Initial selected staff IDs (strings)
|
// Initial selected staff IDs (strings)
|
||||||
const INITIAL_STAFF = <%- JSON.stringify(selectedStaff || []) %>;
|
const INITIAL_STAFF = <%- JSON.stringify(selectedStaff || []) %>;
|
||||||
|
|
||||||
let selectedIds = Array.isArray(INITIAL_STAFF) ? INITIAL_STAFF.slice() : [];
|
let selectedIds = Array.isArray(INITIAL_STAFF) ? INITIAL_STAFF.slice() : [];
|
||||||
|
|
||||||
|
// Fallback: if server didn't provide ROLE_OPTIONS (empty), try to read from any
|
||||||
|
// existing <select> on the page or request roles from server API at runtime.
|
||||||
|
(async function ensureRoleOptions(){
|
||||||
|
try {
|
||||||
|
if (!Array.isArray(ROLE_OPTIONS) || ROLE_OPTIONS.length === 0) {
|
||||||
|
// try to read an existing select element first
|
||||||
|
const sel = document.getElementById('staffSelect') || document.querySelector('select[name="staff"]');
|
||||||
|
if (sel && sel.options && sel.options.length) {
|
||||||
|
ROLE_OPTIONS = Array.from(sel.options).map(o => ({ id: String(o.value), name: String(o.textContent || o.innerText || o.label || o.value).trim(), color: (o.dataset && o.dataset.color) ? o.dataset.color : '#8b95a0' }));
|
||||||
|
renderChips();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// otherwise, try fetching roles from server at runtime
|
||||||
|
try {
|
||||||
|
const guildId = '<%= selectedGuild %>';
|
||||||
|
if (guildId) {
|
||||||
|
const resp = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/roles`, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
||||||
|
if (resp.ok) {
|
||||||
|
const j = await resp.json();
|
||||||
|
if (j && Array.isArray(j.roles) && j.roles.length) {
|
||||||
|
ROLE_OPTIONS = j.roles.map(r=>({ id: String(r.id), name: String(r.name||r.id), color: r.color || '#8b95a0' }));
|
||||||
|
renderChips();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e){ /* ignore fetch errors */ }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function renderChips(){
|
function renderChips(){
|
||||||
|
if (!staffChips) return;
|
||||||
staffChips.innerHTML = '';
|
staffChips.innerHTML = '';
|
||||||
for(const id of selectedIds){
|
for(const id of selectedIds){
|
||||||
const role = ROLE_OPTIONS.find(r=>r.id===String(id));
|
const role = ROLE_OPTIONS.find(r=>r.id===String(id));
|
||||||
@@ -146,7 +181,7 @@
|
|||||||
staffChips.appendChild(chip);
|
staffChips.appendChild(chip);
|
||||||
}
|
}
|
||||||
// update hidden input
|
// update hidden input
|
||||||
if(staffHidden) staffHidden.value = selectedIds.join(',');
|
if(typeof staffHidden !== 'undefined' && staffHidden) staffHidden.value = selectedIds.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSuggestions(query){
|
function showSuggestions(query){
|
||||||
@@ -175,6 +210,17 @@
|
|||||||
// initial render of chips
|
// initial render of chips
|
||||||
renderChips();
|
renderChips();
|
||||||
|
|
||||||
|
// Listen for runtime roles loaded events (from reload button in sidebar)
|
||||||
|
window.addEventListener('roles:loaded', (ev)=>{
|
||||||
|
try{
|
||||||
|
const data = ev && ev.detail && ev.detail.roles ? ev.detail.roles : null;
|
||||||
|
if(!data || !Array.isArray(data)) return;
|
||||||
|
ROLE_OPTIONS = data.map(r=>({ id: String(r.id), name: String(r.name||r.id), color: r.color || '#8b95a0' }));
|
||||||
|
renderChips();
|
||||||
|
if(typeof populateRolePicker === 'function') populateRolePicker('');
|
||||||
|
}catch(e){ /* ignore */ }
|
||||||
|
});
|
||||||
|
|
||||||
// Role picker modal logic
|
// Role picker modal logic
|
||||||
const openRolePicker = document.getElementById('openRolePicker');
|
const openRolePicker = document.getElementById('openRolePicker');
|
||||||
const rolePickerModal = document.getElementById('rolePickerModal');
|
const rolePickerModal = document.getElementById('rolePickerModal');
|
||||||
@@ -194,12 +240,16 @@
|
|||||||
function openPicker(){
|
function openPicker(){
|
||||||
if(!rolePickerModal) return;
|
if(!rolePickerModal) return;
|
||||||
rolePickerModal.classList.remove('hidden');
|
rolePickerModal.classList.remove('hidden');
|
||||||
|
// prevent body scroll on mobile when modal open
|
||||||
|
try{ document.documentElement.style.overflow = 'hidden'; }catch(e){}
|
||||||
populateRolePicker('');
|
populateRolePicker('');
|
||||||
setTimeout(()=>{ if(rolePickerSearch) rolePickerSearch.focus(); },50);
|
setTimeout(()=>{ if(rolePickerSearch) rolePickerSearch.focus(); },50);
|
||||||
}
|
}
|
||||||
function closePicker(){
|
function closePicker(){
|
||||||
if(!rolePickerModal) return;
|
if(!rolePickerModal) return;
|
||||||
rolePickerModal.classList.add('hidden');
|
rolePickerModal.classList.add('hidden');
|
||||||
|
// restore body scroll
|
||||||
|
try{ document.documentElement.style.overflow = ''; }catch(e){}
|
||||||
if(rolePickerSearch) rolePickerSearch.value = '';
|
if(rolePickerSearch) rolePickerSearch.value = '';
|
||||||
if(rolePickerList) rolePickerList.innerHTML = '';
|
if(rolePickerList) rolePickerList.innerHTML = '';
|
||||||
}
|
}
|
||||||
@@ -224,6 +274,13 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close modal on Escape globally (only when open)
|
||||||
|
document.addEventListener('keydown', (ev)=>{
|
||||||
|
if(ev.key === 'Escape' && rolePickerModal && !rolePickerModal.classList.contains('hidden')){
|
||||||
|
closePicker();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if(rolePickerList){
|
if(rolePickerList){
|
||||||
rolePickerList.addEventListener('click', (ev)=>{
|
rolePickerList.addEventListener('click', (ev)=>{
|
||||||
const it = ev.target.closest && ev.target.closest('.role-item');
|
const it = ev.target.closest && ev.target.closest('.role-item');
|
||||||
|
|||||||
@@ -2,7 +2,33 @@
|
|||||||
<nav class="bg-transparent rounded-xl p-4 sticky top-20 h-[80vh] overflow-auto">
|
<nav class="bg-transparent rounded-xl p-4 sticky top-20 h-[80vh] overflow-auto">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<div class="mb-4 flex items-center gap-2">
|
||||||
<a href="/dashboard/<%= selectedGuild %>/overview" class="px-3 py-1 rounded-full bg-white/5 text-white text-xs">Home</a>
|
<a href="/dashboard/<%= selectedGuild %>/overview" class="px-3 py-1 rounded-full bg-white/5 text-white text-xs">Home</a>
|
||||||
|
<button id="reloadRolesBtn" type="button" title=":" class="px-2 py-1 rounded-full bg-white/6 hover:bg-white/10 text-xs" aria-label="Recargar roles">
|
||||||
|
<!-- minimal two-dot icon -->
|
||||||
|
<span aria-hidden="true" class="text-sm">··</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const btn = document.getElementById('reloadRolesBtn');
|
||||||
|
if(!btn) return;
|
||||||
|
btn.addEventListener('click', async function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
const guildId = '<%= selectedGuild %>';
|
||||||
|
if(!guildId) return;
|
||||||
|
try{
|
||||||
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/roles`, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
||||||
|
if(!res.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const j = await res.json();
|
||||||
|
if(j && Array.isArray(j.roles)){
|
||||||
|
window.dispatchEvent(new CustomEvent('roles:loaded', { detail: { roles: j.roles } }));
|
||||||
|
}
|
||||||
|
}catch(err){ /* ignore errors */ }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<ul class="mt-2 space-y-3 text-slate-200">
|
<ul class="mt-2 space-y-3 text-slate-200">
|
||||||
<li class="text-sm"><a href="/dashboard/<%= selectedGuild %>/settings" class="flex items-center gap-3"><span class="opacity-80">⚙️</span> General Settings</a></li>
|
<li class="text-sm"><a href="/dashboard/<%= selectedGuild %>/settings" class="flex items-center gap-3"><span class="opacity-80">⚙️</span> General Settings</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user