anti stupidity
This commit is contained in:
+10
-13
@@ -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("/");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user