feat: Restaurar y mejorar la lógica de manejo de sesiones y estado, incluyendo la verificación de SID y el almacenamiento de estado

This commit is contained in:
Shni
2025-10-15 03:53:35 -05:00
parent 75b0305261
commit 02db391bfe
3 changed files with 105 additions and 80 deletions

View File

@@ -72,6 +72,53 @@ function parseCookies(req: IncomingMessage) {
}, {}); }, {});
} }
// --- Session and state helpers (restored) ---
function getSessionSecret(): string {
// Prefer explicit env var; fallback to package name + version for a deterministic but non-secret default.
if (process.env.SESSION_SECRET && process.env.SESSION_SECRET.length > 8)
return process.env.SESSION_SECRET;
const name = pkg?.name || "amayo";
const version = pkg?.version || "0";
return createHmac("sha256", "fallback")
.update(name + "@" + version)
.digest("hex");
}
function unsignSid(signed: string | undefined): string | null {
if (!signed) return null;
const parts = String(signed).split(".");
if (parts.length !== 2) return null;
const sid = parts[0];
const sig = parts[1];
try {
const expected = createHmac("sha256", getSessionSecret())
.update(sid)
.digest("base64url");
// timing-safe compare
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length) return null;
if (!timingSafeEqual(a, b)) return null;
return sid;
} catch {
return null;
}
}
function storeState(key: string) {
STATE_STORE.set(key, { ts: Date.now() });
}
function hasState(key: string) {
const v = STATE_STORE.get(key);
if (!v) return false;
if (Date.now() - v.ts > STATE_TTL_MS) {
STATE_STORE.delete(key);
return false;
}
return true;
}
function setSessionCookie(res: ServerResponse, sid: string) { function setSessionCookie(res: ServerResponse, sid: string) {
// Sign the SID to prevent tampering // Sign the SID to prevent tampering
const secret = getSessionSecret(); const secret = getSessionSecret();
@@ -92,51 +139,10 @@ function clearSessionCookie(res: ServerResponse) {
const secure = isProd ? "; Secure" : ""; const secure = isProd ? "; Secure" : "";
res.setHeader( res.setHeader(
"Set-Cookie", "Set-Cookie",
`amayo_sid=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${secure}` `amayo_sid=; HttpOnly; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT${secure}`
); );
} }
function getSessionSecret() {
return (
process.env.SESSION_SECRET ||
process.env.DISCORD_CLIENT_SECRET ||
"dev-session-secret"
);
}
function unsignSid(signed: string | undefined): string | null {
if (!signed) return null;
const decoded = decodeURIComponent(signed);
const parts = decoded.split(".");
if (parts.length !== 2) return null;
const [sid, sig] = parts;
try {
const secret = getSessionSecret();
const expected = createHmac("sha256", secret).update(sid).digest();
const got = Buffer.from(sig, "base64url");
// timing safe compare
if (expected.length !== got.length) return null;
if (!timingSafeEqual(expected, got)) return null;
return sid;
} catch {
return null;
}
}
function storeState(state: string) {
STATE_STORE.set(state, { ts: Date.now() });
}
function hasState(state: string) {
const v = STATE_STORE.get(state);
if (!v) return false;
if (Date.now() - v.ts > STATE_TTL_MS) {
STATE_STORE.delete(state);
return false;
}
return true;
}
function createSession(data: any) { function createSession(data: any) {
// Evict oldest if over cap // Evict oldest if over cap
if (SESSIONS.size >= MAX_SESSIONS) { if (SESSIONS.size >= MAX_SESSIONS) {

View File

@@ -43,9 +43,9 @@
<div id="cardList" class="max-h-72 overflow-auto rounded"> <div id="cardList" class="max-h-72 overflow-auto rounded">
<% if (guilds && guilds.length) { %> <% if (guilds && guilds.length) { %>
<% const sorted = guilds.slice().sort((a,b)=> a.name.localeCompare(b.name)); const withBot = sorted.filter(g=> g.botInGuild !== false); const withoutBot = sorted.filter(g=> g.botInGuild === false); %> <% const sorted = guilds.slice().sort((a,b)=> a.name.localeCompare(b.name)); const withBot = sorted.filter(g=> g.botInGuild === true); const withoutBot = sorted.filter(g=> g.botInGuild !== true); %>
<% withBot.forEach(g => { %> <% withBot.forEach(g => { %>
<div class="p-3 cursor-pointer hover:bg-white/5 flex items-center justify-between text-white" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="location.href='/dashboard/<%= g.id %>/overview'" role="button" tabindex="0" class="p-3 cursor-pointer hover:bg-white/5 flex items-center justify-between text-white" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
@@ -53,7 +53,7 @@
<div class="w-8 h-8 rounded-full bg-white/8 flex items-center justify-center text-xs text-white">S</div> <div class="w-8 h-8 rounded-full bg-white/8 flex items-center justify-center text-xs text-white">S</div>
<% } %> <% } %>
<div class="text-left"> <div class="text-left">
<div class="font-medium"><%= g.name %></div> <div class="font-medium"><%= g.name %> <% if (g.botInGuild === true) { %><span class="text-xs text-emerald-300">(Bot)</span><% } else { %><span class="text-xs text-sky-300">(Invitar)</span><% } %></div>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -64,7 +64,7 @@
<% if (withoutBot && withoutBot.length) { %> <% if (withoutBot && withoutBot.length) { %>
<div class="border-t border-white/6 my-2"></div> <div class="border-t border-white/6 my-2"></div>
<% withoutBot.forEach(g => { %> <% withoutBot.forEach(g => { %>
<div class="p-3 cursor-pointer hover:bg-white/5 flex items-center justify-between text-white opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="window.open('https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>', '_blank', 'noopener')" role="button" tabindex="0" class="p-3 cursor-pointer hover:bg-white/5 flex items-center justify-between text-white opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
@@ -110,11 +110,15 @@
const id = el.dataset.id; const id = el.dataset.id;
const bot = el.getAttribute('data-bot'); const bot = el.getAttribute('data-bot');
const invite = el.getAttribute('data-invite'); const invite = el.getAttribute('data-invite');
if (bot === '0' && invite) { const isDim = el.classList && el.classList.contains('opacity-60');
if (bot === '1') {
window.location.href = `/dashboard/${id}/overview`;
return;
}
if ((bot === '0' || isDim) && invite) {
window.open(invite, '_blank', 'noopener'); window.open(invite, '_blank', 'noopener');
return; return;
} }
window.location.href = `/dashboard/${id}/overview`;
} }
}); });
})(); })();

View File

@@ -22,23 +22,30 @@
<svg class="w-3 h-3 text-white/80" viewBox="0 0 20 20" fill="none"><path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg class="w-3 h-3 text-white/80" viewBox="0 0 20 20" fill="none"><path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button> </button>
<div id="miniGuildList" class="origin-top-left absolute top-full left-0 mt-2 w-56 bg-white/80 backdrop-blur-md rounded-lg p-1 hidden transition-transform duration-150 z-50" style="max-height:18rem; overflow:auto; background-color: rgba(12,15,18,0.92);"> <div id="miniGuildList" class="origin-top-left absolute top-full left-0 mt-2 w-56 bg-white/80 backdrop-blur-md rounded-lg p-1 hidden transition-transform duration-150 z-50" style="max-height:18rem; overflow:auto; background-color: rgba(12,15,18,0.92);">
<% if (guilds && guilds.length) { %> <% if (guilds && guilds.length) { %>
<% const withBot = guilds.filter(g=> g.botInGuild !== false); const withoutBot = guilds.filter(g=> g.botInGuild === false); %> <% const withBot = guilds.filter(g=> g.botInGuild === true); const withoutBot = guilds.filter(g=> g.botInGuild !== true); %>
<% withBot.forEach(g => { %> <% withBot.forEach(g => { %>
<div class="p-1 rounded-md hover:bg-white/5 cursor-pointer text-white guild-item flex items-center gap-3" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="location.href='/dashboard/<%= g.id %>/overview'" role="button" tabindex="0" class="p-1 rounded-md hover:bg-white/5 cursor-pointer text-white guild-item flex items-center gap-3" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon">
<% } else { %> <% } else { %>
<div class="w-5 h-5 rounded-full bg-white/8 flex items-center justify-center text-xs text-white">S</div> <div class="w-5 h-5 rounded-full bg-white/8 flex items-center justify-center text-xs text-white">S</div>
<% } %> <% } %>
<div class="flex-1 text-sm truncate pl-1"><%= g.name %></div> <div class="flex-1 text-sm truncate pl-1">
<%= g.name %>
<% if (g.botInGuild === true) { %>
<span class="ml-2 text-xs text-emerald-300">(Bot)</span>
<% } else { %>
<span class="ml-2 text-xs text-sky-300">(Invitar)</span>
<% } %>
</div>
<!-- invite button removed from inline list; rows without bot are dimmed and clicking them opens the invite --> <!-- invite button removed from inline list; rows without bot are dimmed and clicking them opens the invite -->
</div> </div>
<% }) %> <% }) %>
<% if (withoutBot && withoutBot.length) { %> <% if (withoutBot && withoutBot.length) { %>
<div class="border-t border-white/6 my-1"></div> <div class="border-t border-white/6 my-1"></div>
<% withoutBot.forEach(g => { %> <% withoutBot.forEach(g => { %>
<div class="p-1 rounded-md hover:bg-white/5 cursor-pointer text-white guild-item flex items-center gap-3 opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="window.open('https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>', '_blank', 'noopener')" role="button" tabindex="0" class="p-1 rounded-md hover:bg-white/5 cursor-pointer text-white guild-item flex items-center gap-3 opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon">
<% } else { %> <% } else { %>
@@ -130,25 +137,26 @@
openDropdown(btn, list); openDropdown(btn, list);
} }
}); });
Array.from(list.querySelectorAll('.guild-item')).forEach(it=>{ // Use event delegation on the list so clicks on children are handled reliably
it.addEventListener('click', ()=>{ list.addEventListener('click', (e)=>{
const id = it.getAttribute('data-id'); let el = e.target;
const bot = it.getAttribute('data-bot'); while (el && !el.dataset?.id && el !== list) el = el.parentElement;
const invite = it.getAttribute('data-invite'); if (!el || el === list) return;
if (bot === '0' && invite) { const id = el.getAttribute('data-id');
// open invite in a new tab const bot = el.getAttribute('data-bot');
window.open(invite, '_blank', 'noopener'); const invite = el.getAttribute('data-invite');
return; const isDim = el.classList && el.classList.contains('opacity-60');
} if (bot === '1') {
if (id) window.location.href = `/dashboard/${id}/overview`; closeDropdown(btn, list);
}); window.location.href = `/dashboard/${id}/overview`;
// keyboard activation (Enter / Space) return;
it.addEventListener('keydown', (ev)=>{ }
if (ev.key === 'Enter' || ev.key === ' ') { // if not explicitly '1', treat as missing and open invite if available
ev.preventDefault(); if ((bot === '0' || isDim) && invite) {
it.click(); window.open(invite, '_blank', 'noopener');
} closeDropdown(btn, list);
}); return;
}
}); });
} }
if (userBtn && userMenu) { if (userBtn && userMenu) {
@@ -211,21 +219,27 @@
</div> </div>
<div id="mobileGuildList" class="space-y-2"> <div id="mobileGuildList" class="space-y-2">
<% if (guilds && guilds.length) { %> <% if (guilds && guilds.length) { %>
<% const withBot = guilds.filter(g=> g.botInGuild !== false); const withoutBot = guilds.filter(g=> g.botInGuild === false); %> <% const withBot = guilds.filter(g=> g.botInGuild === true); const withoutBot = guilds.filter(g=> g.botInGuild !== true); %>
<% withBot.forEach(g => { %> <% withBot.forEach(g => { %>
<div class="flex items-center gap-3 p-2 rounded-md hover:bg-white/5 cursor-pointer" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="location.href='/dashboard/<%= g.id %>/overview'" role="button" tabindex="0" class="flex items-center gap-3 p-2 rounded-md hover:bg-white/5 cursor-pointer" data-id="<%= g.id %>" data-bot="1" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
<% } else { %> <% } else { %>
<div class="w-8 h-8 rounded-full bg-white/8"></div> <div class="w-8 h-8 rounded-full bg-white/8"></div>
<% } %> <% } %>
<div class="text-white"><%= g.name %></div> <div class="text-white"><%= g.name %>
<% if (g.botInGuild === true) { %>
<span class="ml-2 text-xs text-emerald-300">(Bot)</span>
<% } else { %>
<span class="ml-2 text-xs text-sky-300">(Invitar)</span>
<% } %>
</div>
</div> </div>
<% }) %> <% }) %>
<% if (withoutBot && withoutBot.length) { %> <% if (withoutBot && withoutBot.length) { %>
<div class="border-t border-white/6 mt-2 pt-2"></div> <div class="border-t border-white/6 mt-2 pt-2"></div>
<% withoutBot.forEach(g => { %> <% withoutBot.forEach(g => { %>
<div class="flex items-center gap-3 p-2 rounded-md hover:bg-white/5 cursor-pointer opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>"> <div onclick="window.open('https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>', '_blank', 'noopener')" role="button" tabindex="0" class="flex items-center gap-3 p-2 rounded-md hover:bg-white/5 cursor-pointer opacity-60" data-id="<%= g.id %>" data-bot="0" data-invite="https://discord.com/oauth2/authorize?client_id=991062751633883136&permissions=2416176272&integration_type=0&scope=bot&guild_id=<%= g.id %>">
<% if (g.icon) { %> <% if (g.icon) { %>
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon"> <img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
<% } else { %> <% } else { %>
@@ -289,10 +303,11 @@
let el = e.target; let el = e.target;
while (el && !el.dataset?.id) el = el.parentElement; while (el && !el.dataset?.id) el = el.parentElement;
if (el && el.dataset && el.dataset.id) { if (el && el.dataset && el.dataset.id) {
const bot = el.getAttribute('data-bot'); const bot = el.getAttribute('data-bot');
const invite = el.getAttribute('data-invite'); const invite = el.getAttribute('data-invite');
if (bot === '0' && invite) { window.open(invite, '_blank', 'noopener'); return; } const isDim = el.classList && el.classList.contains('opacity-60');
window.location.href = `/dashboard/${el.dataset.id}/overview`; if (bot === '1') { window.location.href = `/dashboard/${el.dataset.id}/overview`; return; }
if ((bot === '0' || isDim) && invite) { window.open(invite, '_blank', 'noopener'); return; }
} }
}); });