anti stupidity

This commit is contained in:
2026-07-13 16:16:03 -04:00
parent 88e32ebe6c
commit 3c90783a7a
4 changed files with 80 additions and 79 deletions
+10 -13
View File
@@ -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("/");
}
+9 -4
View File
@@ -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<Rating[]>([]);
const [ratingTypes, setRatingTypes] = useState<{ id: number; name: string; hasAttachments: boolean; categories: { id: number; name: string }[] }[]>([]);
@@ -450,7 +455,7 @@ function AdminPage({ user }: Props) {
return (
<div style={{ maxWidth: 320, margin: "40px auto", color: textClr, fontSize: 16, textAlign: "center" }}>
<div style={{ marginBottom: 16 }}>Not authorized</div>
<button onClick={() => { document.cookie = "userId=; path=/; max-age=0"; location.reload(); }} style={{ padding: "6px 14px", fontSize: 13, background: "none", color: textClr, border: `1px solid ${borderClr}`, cursor: "pointer" }}>Refresh Login</button>
<button onClick={() => { document.cookie = "sessionToken=; path=/; max-age=0"; location.reload(); }} style={{ padding: "6px 14px", fontSize: 13, background: "none", color: textClr, border: `1px solid ${borderClr}`, cursor: "pointer" }}>Refresh Login</button>
</div>
);
}
+10
View File
@@ -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())
}
+51 -62
View File
@@ -13,14 +13,10 @@ import crypto from "crypto";
const app = express();
const prisma = new PrismaClient();
const adminSessions = new Map<string, number>();
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<number, RatingCacheEntry>(),
ratingTypes: new Map<number, { id: number; name: string; hasAttachments: boolean; categories: { id: number; name: string }[] }>(),
@@ -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<boolean> {
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<string, unknown> = { 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<string, unknown> = { 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<string, unknown> = { 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) => {