CVE Patch + Post stream rubgram + tl improvements

- Switch to using generated columns for rubgram's paid
- Few text changes
- Isolate badges to their type
- Use Redis to cache health checks
- Remove confusing logging and add actionLog stdout logging
- Blacklist RSC/prefetch related requests from nginx logging
- Fix nginx upload size limit
This commit is contained in:
Dreamgineer
2025-12-15 21:15:55 +07:00 Unverified
parent 42ee896468
commit 2c7f18bf56
15 changed files with 187 additions and 118 deletions
+16 -3
View File
@@ -1,4 +1,5 @@
import { import {
ArrowLeftRight,
Badge, Badge,
BookUser, BookUser,
Columns3Cog, Columns3Cog,
@@ -59,8 +60,12 @@ export default async function AdminLayout({
.select() .select()
.from(versions) .from(versions)
.catch(() => [{ id: "..", name: "Error Fetching List." }]); .catch(() => [{ id: "..", name: "Error Fetching List." }]);
const health: { database: boolean; enka: boolean; amber: boolean } = const health: {
await fetch("http://localhost:3000/api/health").then((r) => r.json()); database: boolean;
enka: boolean;
amber: boolean;
red: boolean;
} = await fetch("http://localhost:3000/api/health").then((r) => r.json());
return ( return (
<SidebarProvider> <SidebarProvider>
@@ -116,10 +121,18 @@ export default async function AdminLayout({
} }
/> />
</SimpleTooltip> </SimpleTooltip>
{/* Cache */}
<SimpleTooltip text="Redis Cache/SSE">
<ArrowLeftRight
className={
health.red ? "text-emerald-400" : "text-red-400"
}
/>
</SimpleTooltip>
</div> </div>
<span> <span>
{Object.values(health).some((x) => !x) {Object.values(health).some((x) => !x)
? `${{ database: "ฐานข้อมูล", enka: "Enka ", amber: "Amber " }[Object.entries(health).find(([, v]) => !v)![0]]}มีปัญหา` ? `${{ database: "ฐานข้อมูล", enka: "Enka ", amber: "Amber ", red: "SSE " }[Object.entries(health).find(([, v]) => !v)![0]]}มีปัญหา`
: "ปกติทุกอย่าง"} : "ปกติทุกอย่าง"}
</span> </span>
</div> </div>
+38 -12
View File
@@ -18,6 +18,7 @@ export default function AuditLogViewer({
}) { }) {
const [logs, updateLogs] = useState(initialLogs); const [logs, updateLogs] = useState(initialLogs);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const entryRefs = useRef(new Map<string, HTMLDivElement>());
useEffect(() => { useEffect(() => {
const es = new EventSource("/api/ev/log"); const es = new EventSource("/api/ev/log");
@@ -26,18 +27,9 @@ export default function AuditLogViewer({
return () => es.close(); return () => es.close();
}, []); }, []);
useEffect(() => {
if (containerRef.current)
containerRef.current.scrollTo({
top: 999999,
left: 0,
behavior: "smooth",
});
});
const filters: Filters = { const filters: Filters = {
user: () => users.map((u) => u.email), user: () => users.map((u) => u.email),
has: () => ["artifact", "tierlist", "file"], has: () => ["artifact", "tierlist", "rubgram", "file"],
}; };
const [query, setQuery] = useState<ParsedQuery>(); const [query, setQuery] = useState<ParsedQuery>();
@@ -58,6 +50,36 @@ export default function AuditLogViewer({
}, [query, logs, users]); }, [query, logs, users]);
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set()); const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const focusedIds = useMemo(() => Array.from(expandedIds), [expandedIds]);
// biome-ignore lint/correctness/useExhaustiveDependencies: need to update
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// exactly one focused → scroll to that entry
if (focusedIds.length === 1) {
const el = entryRefs.current.get(focusedIds[0]);
if (el) {
el.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
return;
}
// more than one focused → do nothing
if (focusedIds.length > 1) {
return;
}
// none focused → always follow bottom
container.scrollTo({
top: container.scrollHeight,
behavior: "smooth",
});
}, [logs, focusedIds, query]);
const toggleExpand = (id: string) => { const toggleExpand = (id: string) => {
setExpandedIds((prev) => { setExpandedIds((prev) => {
@@ -117,12 +139,16 @@ export default function AuditLogViewer({
return ( return (
<div <div
key={log.id} key={log.id}
ref={(el) => {
if (el) entryRefs.current.set(log.id, el);
else entryRefs.current.delete(log.id);
}}
className="group hover:bg-[#2225] transition-colors" className="group hover:bg-[#2225] transition-colors"
> >
<div <div
className={`flex gap-4 px-4 ${isGrouped ? "py-0.5" : "py-3.5"} pb-0.5`} className={`flex gap-4 px-4 ${isGrouped ? "py-0.5" : "py-3.5"} pb-0.5`}
> >
<div className="w-min flex-shrink-0 text-right pt-0.5"> <div className="w-min shrink-0 text-right pt-0.5">
<span <span
className={`text-xs whitespace-nowrap invisible ${isGrouped ? "group-hover:visible" : ""}`} className={`text-xs whitespace-nowrap invisible ${isGrouped ? "group-hover:visible" : ""}`}
> >
@@ -154,7 +180,7 @@ export default function AuditLogViewer({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="flex-shrink-0 h-6 w-6 text-gray-400 hover:text-gray-200 hover:bg-[#404249]" className="shrink-0 h-6 w-6 text-gray-400 hover:text-gray-200 hover:bg-[#404249]"
> >
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
+1 -1
View File
@@ -85,7 +85,7 @@ export function CallButton({ user }: { user: string }) {
) : ( ) : (
<MessageSquareWarning /> <MessageSquareWarning />
)} )}
กดเรียกผ่านดิสคอร์ด เรียกในดิสคอร์ด
</Button> </Button>
); );
} }
+1 -5
View File
@@ -1,4 +1,3 @@
import { eq, isNotNull, or, type SQL } from "drizzle-orm";
import { Dice3, Trash2 } from "lucide-react"; import { Dice3, Trash2 } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import {
@@ -42,10 +41,7 @@ export default async function AdminLayout({
name: endgameSubmissions.name, name: endgameSubmissions.name,
checked: endgameSubmissions.checked, checked: endgameSubmissions.checked,
queue: endgameSubmissions.queue, queue: endgameSubmissions.queue,
paid: or( paid: endgameSubmissions.paid,
eq(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
) as SQL<boolean>,
}) })
.from(endgameSubmissions) .from(endgameSubmissions)
.orderBy(endgameSubmissions.id); .orderBy(endgameSubmissions.id);
+5 -22
View File
@@ -1,17 +1,6 @@
"use server"; "use server";
import { import { and, desc, eq, gt, lt, not, sql } from "drizzle-orm";
and,
desc,
eq,
gt,
isNotNull,
isNull,
lt,
not,
or,
sql,
} from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import image from "next/image"; import image from "next/image";
@@ -57,13 +46,8 @@ export async function random() {
const [sub] = await db const [sub] = await db
.select() .select()
.from(endgameSubmissions) .from(endgameSubmissions)
.where( .where(endgameSubmissions.paid.getSQL())
and( .orderBy(sql`${endgameSubmissions.checked} ASC, RANDOM()`)
not(endgameSubmissions.checked),
or(eq(endgameSubmissions.price, 0), isNotNull(endgameSubmissions.slip)),
),
)
.orderBy(sql`RANDOM()`)
.limit(1); .limit(1);
if (sub) redirect(`/rubgram/admin/${sub.id}`); if (sub) redirect(`/rubgram/admin/${sub.id}`);
else throw "ไม่พบผู้ลงทะเบียนที่ยังไม่ตรวจสอบ"; else throw "ไม่พบผู้ลงทะเบียนที่ยังไม่ตรวจสอบ";
@@ -348,8 +332,7 @@ export async function calcPrice(service: string[]) {
async function removeExpiredSubmissions() { async function removeExpiredSubmissions() {
// First, get count of items to be removed // First, get count of items to be removed
const expiredCond = and( const expiredCond = and(
not(eq(endgameSubmissions.price, 0)), not(endgameSubmissions.paid),
isNull(endgameSubmissions.slip),
lt(endgameSubmissions.expires, new Date()), lt(endgameSubmissions.expires, new Date()),
); );
const toRemove = await db const toRemove = await db
@@ -420,7 +403,7 @@ export async function discordCall(id: string) {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}).then(async (r) => { }).then(async (r) => {
console.log(await r.text()); console.log(`called`, await r.text());
return r.ok; return r.ok;
}); });
} }
+1 -1
View File
@@ -214,7 +214,7 @@ export function CancelButton({ sid }: { sid: string }) {
}} }}
disabled={loading} disabled={loading}
> >
{loading && <Spinner />}ยกเลิก {loading && <Spinner />}ใช่
</Button> </Button>
); );
} }
+14 -17
View File
@@ -1,13 +1,9 @@
import { import {
and, and,
eq, eq,
gt, gt, lt,
isNotNull,
isNull,
lt,
not, not,
or, or
type SQL,
} from "drizzle-orm"; } from "drizzle-orm";
import { import {
AlertCircle, AlertCircle,
@@ -89,10 +85,7 @@ export default async function EndgamePage() {
? await db ? await db
.select({ .select({
queue: endgameSubmissions.queue, queue: endgameSubmissions.queue,
paid: or( paid: endgameSubmissions.paid,
eq(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
) as SQL<boolean>,
price: endgameSubmissions.price, price: endgameSubmissions.price,
expires: endgameSubmissions.expires, expires: endgameSubmissions.expires,
}) })
@@ -114,8 +107,7 @@ export default async function EndgamePage() {
.from(endgameSubmissions) .from(endgameSubmissions)
.where( .where(
and( and(
not(eq(endgameSubmissions.price, 0)), not(endgameSubmissions.paid),
isNull(endgameSubmissions.slip),
lt(endgameSubmissions.queue, q.queue), lt(endgameSubmissions.queue, q.queue),
), ),
) )
@@ -293,13 +285,13 @@ export default async function EndgamePage() {
</SimpleTooltip> </SimpleTooltip>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>แน่ใจเหรอ</AlertDialogTitle> <AlertDialogTitle>แน่ใจหรอ</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
ต้องการยกเลิกคิวของคุณหรือไม่ ต้องการยกเลิกการลงคิวของคุณหรือไม่
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>ไม่ใช่</AlertDialogCancel> <AlertDialogCancel>ไม่ยกเลิก</AlertDialogCancel>
<CancelButton sid={sid!.value} /> <CancelButton sid={sid!.value} />
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -330,9 +322,14 @@ export default async function EndgamePage() {
</div> </div>
</CardFooter> </CardFooter>
</Card> </Card>
<span className="backdrop-blur-md p-1 text-xs m-1 border border-dashed rounded-sm"> <span className="backdrop-blur-md p-1 text-xs m-1 border rounded-sm">
หากติดปัญหา โปรดแจ้งผ่านทาง หากติดปัญหา โปรดแจ้งผ่านทาง
<a href="https://discord.gg/HQwDXNhxuK" className="underline">ช่องดิสคอร์ด</a> <a
href="https://discord.gg/HQwDXNhxuK"
className="underline text-green-200"
>
ช่องดิสคอร์ด
</a>
</span> </span>
{sid && !q && <ClearCookie />} {sid && !q && <ClearCookie />}
</div> </div>
+5 -2
View File
@@ -1,4 +1,4 @@
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, or, inArray, isNull } from "drizzle-orm";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { import {
@@ -94,7 +94,10 @@ export default async function TierlistPage({
const badgesList = await tx const badgesList = await tx
.select() .select()
.from(tierlistBadges) .from(tierlistBadges)
.orderBy(tierlistBadges.order); .orderBy(tierlistBadges.order)
.where(
or(isNull(tierlistBadges.type), eq(tierlistBadges.type, version.type)),
);
const badges = badgesList.map((b) => ({ const badges = badgesList.map((b) => ({
...b, ...b,
tier: tiers.filter((t) => t.badges?.includes(b.id)).map((t) => t.id), tier: tiers.filter((t) => t.badges?.includes(b.id)).map((t) => t.id),
+44 -20
View File
@@ -2,29 +2,53 @@ import { sql } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { redis } from "@/lib/db/redis"; import { redis } from "@/lib/db/redis";
export const revalidate = 900;
export async function GET() { export async function GET() {
const database = await db
.execute(sql<boolean>`select true`)
.then(() => true)
.catch(() => false);
const red = await redis const red = await redis
.ping() .ping()
.then(() => true) .then(() => true)
.catch(() => false); .catch(() => false);
const enka = await fetch("https://enka.network/api/uid/888888888/?info", { const res = Object.fromEntries(
cache: "force-cache", await Promise.all(
headers: { "User-Agent": "Buzz Event Platform(Health Checker)" }, Object.entries(checks).map(
signal: AbortSignal.timeout(5000), async ([k, v]) =>
}) [
.then((r) => r.ok) k,
.catch(() => false); red
const amber = await fetch("https://gi.yatta.moe/api/v2/en/avatar", { ? (await redis.exists(`health:${k}`))
cache: "force-cache", ? (await redis.get(`health:${k}`)) === "ok"
signal: AbortSignal.timeout(5000), : await v().then(async (v) => {
}) if (v) await redis.setex(`health:${k}`, 900, "ok");
.then((r) => r.ok) return v;
.catch(() => false); })
return Response.json({ database, red, enka, amber }); : await v(),
] as const,
),
),
);
return Response.json({ red, ...res });
} }
const checks = {
async database() {
return await db
.execute(sql<boolean>`select true`)
.then(() => true)
.catch(() => false);
},
async enka() {
return await fetch("https://enka.network/api/uid/888888888/?info", {
headers: { "User-Agent": "Buzz, https://buzz.sudloh.com/api/health" },
signal: AbortSignal.timeout(5000),
})
.then((r) => r.ok)
.catch(() => false);
},
async amber() {
return await fetch("https://gi.yatta.moe/api/v2/en/avatar", {
headers: { "User-Agent": "Buzz, https://buzz.sudloh.com/api/health" },
signal: AbortSignal.timeout(5000),
})
.then((r) => r.ok)
.catch(() => false);
},
} as const;
-2
View File
@@ -18,8 +18,6 @@ export async function GET(
if (!sub) notFound(); if (!sub) notFound();
if (!sub.slip) notFound(); if (!sub.slip) notFound();
console.log(sub.slip.length);
return new Response(new Uint8Array(sub.slip), { return new Response(new Uint8Array(sub.slip), {
headers: { headers: {
"Content-Type": "image/jpeg", "Content-Type": "image/jpeg",
+11 -11
View File
@@ -52,7 +52,7 @@
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"next": "16.0.7", "next": "16.0.10",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.16.3", "pg": "^8.16.3",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
@@ -363,23 +363,23 @@
"@next/bundle-analyzer": ["@next/[email protected]", "", { "dependencies": { "webpack-bundle-analyzer": "4.10.1" } }, "sha512-zoa/w3TYl6kBcJesvj7JM96fHu1WiHRLTRMgEdLEn2kh1cGJMkIIlOGyekjnccaozd+oIyQ925lKrkCMfgt1xg=="], "@next/bundle-analyzer": ["@next/[email protected]", "", { "dependencies": { "webpack-bundle-analyzer": "4.10.1" } }, "sha512-zoa/w3TYl6kBcJesvj7JM96fHu1WiHRLTRMgEdLEn2kh1cGJMkIIlOGyekjnccaozd+oIyQ925lKrkCMfgt1xg=="],
"@next/env": ["@next/[email protected].7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="], "@next/env": ["@next/[email protected].10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="],
"@next/swc-darwin-arm64": ["@next/[email protected].7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="], "@next/swc-darwin-arm64": ["@next/[email protected].10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg=="],
"@next/swc-darwin-x64": ["@next/[email protected].7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="], "@next/swc-darwin-x64": ["@next/[email protected].10", "", { "os": "darwin", "cpu": "x64" }, "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw=="],
"@next/swc-linux-arm64-gnu": ["@next/[email protected].7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="], "@next/swc-linux-arm64-gnu": ["@next/[email protected].10", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw=="],
"@next/swc-linux-arm64-musl": ["@next/[email protected].7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="], "@next/swc-linux-arm64-musl": ["@next/[email protected].10", "", { "os": "linux", "cpu": "arm64" }, "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw=="],
"@next/swc-linux-x64-gnu": ["@next/[email protected].7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="], "@next/swc-linux-x64-gnu": ["@next/[email protected].10", "", { "os": "linux", "cpu": "x64" }, "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA=="],
"@next/swc-linux-x64-musl": ["@next/[email protected].7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="], "@next/swc-linux-x64-musl": ["@next/[email protected].10", "", { "os": "linux", "cpu": "x64" }, "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g=="],
"@next/swc-win32-arm64-msvc": ["@next/[email protected].7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="], "@next/swc-win32-arm64-msvc": ["@next/[email protected].10", "", { "os": "win32", "cpu": "arm64" }, "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg=="],
"@next/swc-win32-x64-msvc": ["@next/[email protected].7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="], "@next/swc-win32-x64-msvc": ["@next/[email protected].10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="],
"@noble/ciphers": ["@noble/[email protected]", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="], "@noble/ciphers": ["@noble/[email protected]", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="],
@@ -849,7 +849,7 @@
"nanostores": ["[email protected]", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], "nanostores": ["[email protected]", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="],
"next": ["[email protected].7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="], "next": ["[email protected].10", "", { "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="],
"next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "next-themes": ["[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
+7 -4
View File
@@ -285,7 +285,9 @@ export async function cdnify(
export async function actionLog(text: string, details?: unknown) { export async function actionLog(text: string, details?: unknown) {
const session = await adminCheck(); const session = await adminCheck();
const res = await db console.log(` LOG ${session?.name || ""} ${text}`);
const [res] = await db
.insert(auditLog) .insert(auditLog)
.values({ .values({
author: session?.name, author: session?.name,
@@ -293,11 +295,12 @@ export async function actionLog(text: string, details?: unknown) {
details, details,
}) })
.returning() .returning()
.catch(() => .catch(() => {
console.error( console.error(
`Error logging audit log, printing it here:\n${session?.name || "[Unknown User]"} - ${text}`, `Error logging audit log, printing it here:\n${session?.name || "[Unknown User]"} - ${text}`,
), );
); return [];
});
revalidatePath("/admin/log"); revalidatePath("/admin/log");
+14
View File
@@ -1,3 +1,4 @@
import { isNotNull, lte, or, type SQL } from "drizzle-orm";
import { import {
type AnyPgColumn, type AnyPgColumn,
boolean, boolean,
@@ -128,6 +129,15 @@ export const endgameSubmissions = endgame.table("submissions", {
price: integer().notNull(), // calculated on submission price: integer().notNull(), // calculated on submission
slip: text().references(() => endgameSlips.id), // not paid = null slip: text().references(() => endgameSlips.id), // not paid = null
checked: boolean().notNull().default(false), checked: boolean().notNull().default(false),
paid: boolean()
.notNull()
.generatedAlwaysAs(
(): SQL =>
or(
lte(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
)!,
),
}); });
// deleted submissions // deleted submissions
@@ -226,6 +236,10 @@ export const tierlistBadges = tierlist.table("badges", {
onUpdate: "cascade", onUpdate: "cascade",
}), }),
order: integer().notNull(), order: integer().notNull(),
type: text().references(() => tierlistTypes.id, {
onDelete: "set null",
onUpdate: "cascade",
}),
}); });
export const tierlistVersions = tierlist.table("versions", { export const tierlistVersions = tierlist.table("versions", {
+25 -15
View File
@@ -1,29 +1,38 @@
events {} events {}
http { http {
map $uri $loggable { # 1 = log, 0 = don't log (based on path)
default 1; map $uri $loggable_uri {
~^/_next 0; default 1;
~^/_next 0;
=/favicon.webp 0;
=/robots.txt 0;
~^/api/.*/count$ 0;
}
=/favicon.webp 0; # 1 = log, 0 = don't log (based on header)
=/robots.txt 0; map $http_rsc $loggable_prefetch {
default 1;
"" 1; # header not present
~.+ 0; # header exists (any value)
}
~^/api/.*/count$ 0; # final decision: log only if BOTH say yes
} map "$loggable_uri:$loggable_prefetch" $loggable {
default 0;
"1:1" 1;
}
log_format clean ' $request_method $uri $status'; log_format clean ' $request_method $uri $status';
access_log /var/log/nginx/access.log clean if=$loggable; access_log /var/log/nginx/access.log clean if=$loggable;
upstream backend { upstream backend {
# Use the service name defined in docker-compose.yml
server app:3000; server app:3000;
} }
server { server {
listen 80; listen 80;
resolver 127.0.0.11 valid=5s ipv6=off;
# Docker's internal DNS resolver
resolver 127.0.0.11 valid=1s ipv6=off;
location / { location / {
proxy_pass http://backend; proxy_pass http://backend;
@@ -32,11 +41,12 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# SSE support if you want it
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_buffering off; proxy_buffering off;
proxy_read_timeout 100s; proxy_read_timeout 200s;
} }
} }
client_max_body_size 100M;
} }
+4 -2
View File
@@ -11,7 +11,9 @@
"format": "biome format --write", "format": "biome format --write",
"dr": "drizzle-kit", "dr": "drizzle-kit",
"ba": "bunx @better-auth/cli", "ba": "bunx @better-auth/cli",
"up": "docker compose build && docker rollout app && docker compose up -d && docker compose restart -t 0 nginx", "up": "docker compose build && docker rollout app && docker compose up -d backup && bun ngreload",
"logs": "docker compose logs -fn50",
"ngreload": "docker compose restart -t 0 nginx",
"test": "playwright test" "test": "playwright test"
}, },
"dependencies": { "dependencies": {
@@ -62,7 +64,7 @@
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"next": "16.0.7", "next": "16.0.10",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"pg": "^8.16.3", "pg": "^8.16.3",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",