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:
@@ -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) {
|
||||
// Sign the SID to prevent tampering
|
||||
const secret = getSessionSecret();
|
||||
@@ -92,51 +139,10 @@ function clearSessionCookie(res: ServerResponse) {
|
||||
const secure = isProd ? "; Secure" : "";
|
||||
res.setHeader(
|
||||
"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) {
|
||||
// Evict oldest if over cap
|
||||
if (SESSIONS.size >= MAX_SESSIONS) {
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
|
||||
<div id="cardList" class="max-h-72 overflow-auto rounded">
|
||||
<% 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 => { %>
|
||||
<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">
|
||||
<% if (g.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="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 class="flex items-center gap-2">
|
||||
@@ -64,7 +64,7 @@
|
||||
<% if (withoutBot && withoutBot.length) { %>
|
||||
<div class="border-t border-white/6 my-2"></div>
|
||||
<% 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">
|
||||
<% if (g.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 bot = el.getAttribute('data-bot');
|
||||
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');
|
||||
return;
|
||||
}
|
||||
window.location.href = `/dashboard/${id}/overview`;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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>
|
||||
</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);">
|
||||
<% if (guilds && guilds.length) { %>
|
||||
<% const withBot = guilds.filter(g=> g.botInGuild !== false); const withoutBot = guilds.filter(g=> g.botInGuild === false); %>
|
||||
<% if (guilds && guilds.length) { %>
|
||||
<% const withBot = guilds.filter(g=> g.botInGuild === true); const withoutBot = guilds.filter(g=> g.botInGuild !== true); %>
|
||||
<% 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) { %>
|
||||
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon">
|
||||
<% } 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="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 -->
|
||||
</div>
|
||||
<% }) %>
|
||||
<% if (withoutBot && withoutBot.length) { %>
|
||||
<div class="border-t border-white/6 my-1"></div>
|
||||
<% 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) { %>
|
||||
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-5 h-5 rounded-full" alt="icon">
|
||||
<% } else { %>
|
||||
@@ -130,25 +137,26 @@
|
||||
openDropdown(btn, list);
|
||||
}
|
||||
});
|
||||
Array.from(list.querySelectorAll('.guild-item')).forEach(it=>{
|
||||
it.addEventListener('click', ()=>{
|
||||
const id = it.getAttribute('data-id');
|
||||
const bot = it.getAttribute('data-bot');
|
||||
const invite = it.getAttribute('data-invite');
|
||||
if (bot === '0' && invite) {
|
||||
// open invite in a new tab
|
||||
window.open(invite, '_blank', 'noopener');
|
||||
return;
|
||||
}
|
||||
if (id) window.location.href = `/dashboard/${id}/overview`;
|
||||
});
|
||||
// keyboard activation (Enter / Space)
|
||||
it.addEventListener('keydown', (ev)=>{
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.preventDefault();
|
||||
it.click();
|
||||
}
|
||||
});
|
||||
// Use event delegation on the list so clicks on children are handled reliably
|
||||
list.addEventListener('click', (e)=>{
|
||||
let el = e.target;
|
||||
while (el && !el.dataset?.id && el !== list) el = el.parentElement;
|
||||
if (!el || el === list) return;
|
||||
const id = el.getAttribute('data-id');
|
||||
const bot = el.getAttribute('data-bot');
|
||||
const invite = el.getAttribute('data-invite');
|
||||
const isDim = el.classList && el.classList.contains('opacity-60');
|
||||
if (bot === '1') {
|
||||
closeDropdown(btn, list);
|
||||
window.location.href = `/dashboard/${id}/overview`;
|
||||
return;
|
||||
}
|
||||
// if not explicitly '1', treat as missing and open invite if available
|
||||
if ((bot === '0' || isDim) && invite) {
|
||||
window.open(invite, '_blank', 'noopener');
|
||||
closeDropdown(btn, list);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (userBtn && userMenu) {
|
||||
@@ -211,21 +219,27 @@
|
||||
</div>
|
||||
<div id="mobileGuildList" class="space-y-2">
|
||||
<% 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 => { %>
|
||||
<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) { %>
|
||||
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
|
||||
<% } else { %>
|
||||
<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>
|
||||
<% }) %>
|
||||
<% if (withoutBot && withoutBot.length) { %>
|
||||
<div class="border-t border-white/6 mt-2 pt-2"></div>
|
||||
<% 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) { %>
|
||||
<img src="https://cdn.discordapp.com/icons/<%= g.id %>/<%= g.icon %>.webp" class="w-8 h-8 rounded-full" alt="icon">
|
||||
<% } else { %>
|
||||
@@ -289,10 +303,11 @@
|
||||
let el = e.target;
|
||||
while (el && !el.dataset?.id) el = el.parentElement;
|
||||
if (el && el.dataset && el.dataset.id) {
|
||||
const bot = el.getAttribute('data-bot');
|
||||
const invite = el.getAttribute('data-invite');
|
||||
if (bot === '0' && invite) { window.open(invite, '_blank', 'noopener'); return; }
|
||||
window.location.href = `/dashboard/${el.dataset.id}/overview`;
|
||||
const bot = el.getAttribute('data-bot');
|
||||
const invite = el.getAttribute('data-invite');
|
||||
const isDim = el.classList && el.classList.contains('opacity-60');
|
||||
if (bot === '1') { window.location.href = `/dashboard/${el.dataset.id}/overview`; return; }
|
||||
if ((bot === '0' || isDim) && invite) { window.open(invite, '_blank', 'noopener'); return; }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user