muitas melhorias pra descrever
This commit is contained in:
1 parent
89b08a19ef
commit
c8fba4d240
21 files changed
+971
-17
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
// Painel de admin de uso pessoal (um único usuário) — token assinado com
|
||||
// HMAC via módulo nativo `crypto`, sem dependência extra de JWT. Mesmo
|
||||
// padrão de "segredo em variável de ambiente com default" já usado pelas
|
||||
// credenciais do Postgres neste projeto (ver docker-compose.yml).
|
||||
const SECRET = process.env.ADMIN_JWT_SECRET ?? "troque-este-segredo-em-producao";
|
||||
const TOKEN_TTL_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
export function signAdminToken(email: string): string {
|
||||
const payload = JSON.stringify({ email, exp: Date.now() + TOKEN_TTL_MS });
|
||||
const body = Buffer.from(payload).toString("base64url");
|
||||
const sig = crypto.createHmac("sha256", SECRET).update(body).digest("base64url");
|
||||
return `${body}.${sig}`;
|
||||
}
|
||||
|
||||
function isValidToken(token: string | undefined): boolean {
|
||||
if (!token) return false;
|
||||
const [body, sig] = token.split(".");
|
||||
if (!body || !sig) return false;
|
||||
const expected = crypto.createHmac("sha256", SECRET).update(body).digest("base64url");
|
||||
if (sig.length !== expected.length) return false;
|
||||
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return false;
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(body, "base64url").toString());
|
||||
return typeof payload.exp === "number" && payload.exp > Date.now();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAdmin(req: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const auth = req.headers.authorization;
|
||||
const token = auth?.startsWith("Bearer ") ? auth.slice("Bearer ".length) : undefined;
|
||||
if (!isValidToken(token)) {
|
||||
reply.code(401).send({ error: "unauthorized" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Contagem de "usuários online" — em memória, sem persistir no banco (é um
|
||||
// número instantâneo, não histórico). O frontend manda um heartbeat a cada
|
||||
// ~20s enquanto a aba está aberta; consideramos "online" quem mandou um
|
||||
// heartbeat nos últimos 45s (tolera uma falha de rede pontual).
|
||||
const heartbeats = new Map<string, number>();
|
||||
const TTL_MS = 45_000;
|
||||
|
||||
export function touchSession(sessionId: string): void {
|
||||
heartbeats.set(sessionId, Date.now());
|
||||
}
|
||||
|
||||
export function getOnlineCount(): number {
|
||||
const now = Date.now();
|
||||
for (const [id, lastSeen] of heartbeats) {
|
||||
if (now - lastSeen > TTL_MS) heartbeats.delete(id);
|
||||
}
|
||||
return heartbeats.size;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { FastifyRequest } from "fastify";
|
||||
import geoip from "geoip-lite";
|
||||
|
||||
// Requer `trustProxy: true` no Fastify (ver server.ts) — aí `req.ip` já
|
||||
// reflete o primeiro IP de X-Forwarded-For, que o nginx interno preenche
|
||||
// (ver nginx/nginx.conf) a partir do que o reverse proxy externo do usuário
|
||||
// mandar. Sem isso, todo mundo apareceria com o IP do container "web".
|
||||
export function resolveIp(req: FastifyRequest): string {
|
||||
return req.ip;
|
||||
}
|
||||
|
||||
// A porta de origem do cliente normalmente NÃO sobrevive a um reverse
|
||||
// proxy (o socket que a API enxerga é o do nginx, não o do navegador do
|
||||
// visitante) — só é possível saber a porta real se algum proxy na cadeia
|
||||
// mandar um cabeçalho explícito (X-Forwarded-Port não costuma carregar a
|
||||
// porta do cliente, mas deixamos o suporte pronto caso o proxy externo do
|
||||
// usuário seja configurado para isso). Na ausência dele, usamos a porta do
|
||||
// socket TCP direto (a porta efêmera de quem conectou nesse hop).
|
||||
export function resolvePort(req: FastifyRequest): number | null {
|
||||
const header = req.headers["x-forwarded-port"];
|
||||
const value = Array.isArray(header) ? header[0] : header;
|
||||
if (value) {
|
||||
const port = Number(value.split(",")[0]);
|
||||
if (Number.isFinite(port)) return port;
|
||||
}
|
||||
return req.socket?.remotePort ?? null;
|
||||
}
|
||||
|
||||
export function resolveGeo(ip: string): { country: string | null; city: string | null } {
|
||||
const geo = geoip.lookup(ip);
|
||||
return { country: geo?.country ?? null, city: geo?.city ?? null };
|
||||
}
|
||||
Reference in new issue
Block a user