From 3c90783a7a0776ae13768232325d2d0a32aabca8 Mon Sep 17 00:00:00 2001 From: johnruina Date: Mon, 13 Jul 2026 16:16:03 -0400 Subject: [PATCH] anti stupidity --- client/src/App.tsx | 23 +++---- client/src/pages/AdminPage.tsx | 13 ++-- server/prisma/schema.prisma | 10 +++ server/src/index.ts | 113 +++++++++++++++------------------ 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 9a9b345..82feab6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,7 @@ import TellDevPage from "./pages/TellDevPage"; import Sidebar from "./components/Sidebar"; import { ThemeProvider } from "./context/ThemeContext"; -type User = { id: number; email: string; adminToken?: string }; +type User = { id: number; email: string }; function getCookie(name: string) { const match = document.cookie.match(`(?:^|;\\s*)${name}=([^;]*)`); @@ -38,17 +38,14 @@ function AppInner() { }, []); useEffect(() => { - const userId = getCookie("userId"); - if (!userId) { setLoading(false); return; } + const sessionToken = getCookie("sessionToken"); + if (!sessionToken) { setLoading(false); return; } - fetch(`/api/user-by-id/${userId}`).then((r) => { - if (!r.ok) { document.cookie = "userId=; path=/; max-age=0"; return null; } + fetch(`/api/session?sessionToken=${encodeURIComponent(sessionToken)}`).then((r) => { + if (!r.ok) { document.cookie = "sessionToken=; path=/; max-age=0"; return null; } return r.json(); }).then((u) => { - if (u) { - const savedToken = localStorage.getItem("adminToken"); - setUser(savedToken ? { ...u, adminToken: savedToken } : u); - } + if (u) setUser(u); }).finally(() => setLoading(false)); }, []); @@ -66,8 +63,7 @@ function AppInner() { return r.json(); }).then((u) => { if (u) { - document.cookie = `userId=${encodeURIComponent(String(u.id))}; path=/; max-age=31536000`; - if (u.adminToken) localStorage.setItem("adminToken", u.adminToken); + document.cookie = `sessionToken=${encodeURIComponent(u.sessionToken)}; path=/; max-age=${30 * 24 * 60 * 60}`; setUser(u); setShowLoginModal(false); } @@ -75,8 +71,9 @@ function AppInner() { } function handleLogout() { - document.cookie = "userId=; path=/; max-age=0"; - localStorage.removeItem("adminToken"); + const sessionToken = getCookie("sessionToken"); + if (sessionToken) fetch(`/api/session?sessionToken=${encodeURIComponent(sessionToken)}`, { method: "DELETE" }).catch(() => {}); + document.cookie = "sessionToken=; path=/; max-age=0"; setUser(null); navigate("/"); } diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index 03161ca..1ea8fb3 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -1,6 +1,11 @@ import { FormEvent, useEffect, useRef, useState } from "react"; import { useTheme } from "../context/ThemeContext"; +function getCookie(name: string) { + const match = document.cookie.match(`(?:^|;\\s*)${name}=([^;]*)`); + return match ? decodeURIComponent(match[1]) : null; +} + type Tag = { id: number; name: string; @@ -58,15 +63,15 @@ type SiteUpdate = { }; type Props = { - user: { id: number; email: string; adminToken?: string } | null; + user: { id: number; email: string } | null; }; function AdminPage({ user }: Props) { const { dark } = useTheme(); - const adminLoggedIn = !!user?.adminToken && user?.email === "invoictoan@gmail.com"; + const adminLoggedIn = user?.email === "invoictoan@gmail.com"; - function ua(url: string) { const sep = url.includes("?") ? "&" : "?"; return `${url}${sep}adminToken=${user?.adminToken}`; } + function ua(url: string) { const sep = url.includes("?") ? "&" : "?"; return `${url}${sep}sessionToken=${getCookie("sessionToken")}`; } const [tab, setTab] = useState<"ratings" | "reports" | "polls" | "assets" | "updates">("ratings"); const [ratings, setRatings] = useState([]); const [ratingTypes, setRatingTypes] = useState<{ id: number; name: string; hasAttachments: boolean; categories: { id: number; name: string }[] }[]>([]); @@ -450,7 +455,7 @@ function AdminPage({ user }: Props) { return (
Not authorized
- +
); } diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index a0c12e4..f539d3f 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -16,6 +16,7 @@ model User { reports Report[] userPolls UserPoll[] ratingStars RatingStar[] + sessions Session[] } model Tag { @@ -196,3 +197,12 @@ model UserPoll { @@unique([userId, pollEntryId]) } + +model Session { + id Int @id @default(autoincrement()) + token String @unique + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + expiresAt DateTime + createdAt DateTime @default(now()) +} diff --git a/server/src/index.ts b/server/src/index.ts index 2d408a3..131102d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -13,14 +13,10 @@ import crypto from "crypto"; const app = express(); const prisma = new PrismaClient(); -const adminSessions = new Map(); - type RatingCacheEntry = { id: number; ratingTypeId: number; name: string; picture: string; description: string; icon: string; attachments: string; tags: { id: number; name: string; color: string }[] }; type PendingVoteOp = { userId?: number; ip?: string; ratingId: number; ratingCategoryId: number; score: number | null }; type PendingStarOp = { userId: number; ratingId: number; ratingCategoryId: number; starred: boolean }; -let adminUserId: number | null = null; - const cache = { ratings: new Map(), ratingTypes: new Map(), @@ -91,8 +87,6 @@ async function rebuildCache() { prisma.userPoll.findMany(), prisma.anonymousPollVote.findMany(), ]); - const adminUser = await prisma.user.findUnique({ where: { email: "invoictoan@gmail.com" }, select: { id: true } }); - adminUserId = adminUser?.id ?? null; cache.ratings.clear(); for (const r of ratings) cache.ratings.set(r.id, { ...r, tags: r.tags.map(rt => ({ id: rt.tag.id, name: rt.tag.name, color: rt.tag.color })) }); cache.ratingTypes.clear(); @@ -197,12 +191,14 @@ setInterval(() => { } }, 60000); -app.use((req, res, next) => { - const adminToken = (req.body?.adminToken as string) || (req.query?.adminToken as string); - if (adminToken && adminSessions.has(adminToken)) { next(); return; } +app.use(async (req, res, next) => { const rawUserId = req.body?.userId || (req.query?.userId as string) || undefined; const userId = rawUserId ? Number(rawUserId) : undefined; - if (userId && userId === adminUserId) { next(); return; } + const sessionToken = (req.body?.sessionToken as string) || (req.query?.sessionToken as string) || getCookie(req, "sessionToken"); + if (sessionToken) { + const session = await prisma.session.findUnique({ where: { token: sessionToken }, include: { user: true } }).catch(() => null); + if (session && session.expiresAt >= new Date() && session.user.email === "invoictoan@gmail.com") { next(); return; } + } const key = userId ? String(userId) : getIp(req); const now = Date.now(); const cutoff = now - 60000; @@ -239,14 +235,22 @@ function getIp(req: express.Request): string { return (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() || req.ip || "unknown"; } +function getCookie(req: express.Request, name: string): string | null { + const match = req.headers.cookie?.match(`(?:^|;\\s*)${name}=([^;]*)`); + return match ? decodeURIComponent(match[1]) : null; +} + async function requireAdmin(req: express.Request, res: express.Response): Promise { - const token = (req.body?.adminToken as string) || (req.query?.adminToken as string); + const token = (req.body?.sessionToken as string) || (req.query?.sessionToken as string); if (!token) { res.status(401).json({ error: "unauthorized" }); return false; } - const expiresAt = adminSessions.get(token); - if (!expiresAt || Date.now() > expiresAt) { - if (expiresAt) adminSessions.delete(token); + const session = await prisma.session.findUnique({ where: { token }, include: { user: true } }); + if (!session || session.expiresAt < new Date()) { + if (session) await prisma.session.delete({ where: { id: session.id } }).catch(() => {}); res.status(401).json({ error: "session expired" }); return false; } + if (session.user.email !== "invoictoan@gmail.com") { + res.status(403).json({ error: "forbidden" }); return false; + } return true; } @@ -274,66 +278,51 @@ app.post("/api/google-login", async (req, res) => { const email = payload.email; - const existing = await prisma.user.findUnique({ where: { email } }); - if (existing) { - const resp: Record = { id: existing.id, email: existing.email }; - if (email === "invoictoan@gmail.com") { - const token = crypto.randomBytes(32).toString("hex"); - adminSessions.set(token, Date.now() + 3600000); - resp.adminToken = token; - } - res.json(resp); - return; + let user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + user = await prisma.user.create({ data: { email } }); + + fetch("https://discord.com/api/webhooks/1525698155012952247/1Mo-j3U0_OPRsozpx656ZCpYfl-C2jeLW3gv0_FUUTeIby2FXHCAqYkvP7jAdeHBJeK3", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: `New account: ${email}` }), + }).catch(() => {}); + + const ip = getIp(req); + await prisma.ipAccount.upsert({ + where: { ip }, + create: { ip, count: 1 }, + update: { count: { increment: 1 } }, + }); } - const user = await prisma.user.create({ - data: { email }, + const sessionToken = crypto.randomBytes(32).toString("hex"); + await prisma.session.create({ + data: { token: sessionToken, userId: user.id, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }, }); - fetch("https://discord.com/api/webhooks/1525698155012952247/1Mo-j3U0_OPRsozpx656ZCpYfl-C2jeLW3gv0_FUUTeIby2FXHCAqYkvP7jAdeHBJeK3", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: `New account: ${email}` }), - }).catch(() => {}); - - const ip = getIp(req); - await prisma.ipAccount.upsert({ - where: { ip }, - create: { ip, count: 1 }, - update: { count: { increment: 1 } }, - }); - - const resp: Record = { id: user.id, email: user.email }; - if (email === "invoictoan@gmail.com") { - const token = crypto.randomBytes(32).toString("hex"); - adminSessions.set(token, Date.now() + 3600000); - resp.adminToken = token; - } + const resp: Record = { id: user.id, email: user.email, sessionToken }; res.json(resp); } catch (e) { res.status(401).json({ error: "Invalid Google token" }); } }); -app.get("/api/user-by-id/:id", async (req, res) => { - const id = Number(req.params.id); - const user = await prisma.user.findUnique({ where: { id } }); - if (!user) { res.status(404).json({ error: "not found" }); return; } - res.json({ id: user.id, email: user.email }); +app.get("/api/session", async (req, res) => { + const token = req.query.sessionToken as string; + if (!token) { res.status(401).json({ error: "no session" }); return; } + const session = await prisma.session.findUnique({ where: { token }, include: { user: true } }); + if (!session || session.expiresAt < new Date()) { + if (session) await prisma.session.delete({ where: { id: session.id } }).catch(() => {}); + res.status(401).json({ error: "session expired" }); return; + } + res.json({ id: session.user.id, email: session.user.email }); }); -app.put("/api/user", async (req, res) => { - const id = Number(req.body.id); - - if (!id) { - res.status(400).json({ error: "User ID is required" }); - return; - } - - const user = await prisma.user.findUnique({ where: { id } }); - if (!user) { res.status(404).json({ error: "not found" }); return; } - - res.json({ id: user.id, email: user.email }); +app.delete("/api/session", async (req, res) => { + const token = req.query.sessionToken as string; + if (token) await prisma.session.delete({ where: { token } }).catch(() => {}); + res.json({ ok: true }); }); app.get("/api/ratings", (req, res) => {