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 {
ArrowLeftRight,
Badge,
BookUser,
Columns3Cog,
@@ -59,8 +60,12 @@ export default async function AdminLayout({
.select()
.from(versions)
.catch(() => [{ id: "..", name: "Error Fetching List." }]);
const health: { database: boolean; enka: boolean; amber: boolean } =
await fetch("http://localhost:3000/api/health").then((r) => r.json());
const health: {
database: boolean;
enka: boolean;
amber: boolean;
red: boolean;
} = await fetch("http://localhost:3000/api/health").then((r) => r.json());
return (
<SidebarProvider>
@@ -116,10 +121,18 @@ export default async function AdminLayout({
}
/>
</SimpleTooltip>
{/* Cache */}
<SimpleTooltip text="Redis Cache/SSE">
<ArrowLeftRight
className={
health.red ? "text-emerald-400" : "text-red-400"
}
/>
</SimpleTooltip>
</div>
<span>
{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>
</div>
+38 -12
View File
@@ -18,6 +18,7 @@ export default function AuditLogViewer({
}) {
const [logs, updateLogs] = useState(initialLogs);
const containerRef = useRef<HTMLDivElement>(null);
const entryRefs = useRef(new Map<string, HTMLDivElement>());
useEffect(() => {
const es = new EventSource("/api/ev/log");
@@ -26,18 +27,9 @@ export default function AuditLogViewer({
return () => es.close();
}, []);
useEffect(() => {
if (containerRef.current)
containerRef.current.scrollTo({
top: 999999,
left: 0,
behavior: "smooth",
});
});
const filters: Filters = {
user: () => users.map((u) => u.email),
has: () => ["artifact", "tierlist", "file"],
has: () => ["artifact", "tierlist", "rubgram", "file"],
};
const [query, setQuery] = useState<ParsedQuery>();
@@ -58,6 +50,36 @@ export default function AuditLogViewer({
}, [query, logs, users]);
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) => {
setExpandedIds((prev) => {
@@ -117,12 +139,16 @@ export default function AuditLogViewer({
return (
<div
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"
>
<div
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
className={`text-xs whitespace-nowrap invisible ${isGrouped ? "group-hover:visible" : ""}`}
>
@@ -154,7 +180,7 @@ export default function AuditLogViewer({
<Button
variant="ghost"
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 ? (
<ChevronDown className="h-4 w-4" />
+1 -1
View File
@@ -85,7 +85,7 @@ export function CallButton({ user }: { user: string }) {
) : (
<MessageSquareWarning />
)}
กดเรียกผ่านดิสคอร์ด
เรียกในดิสคอร์ด
</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 { redirect } from "next/navigation";
import {
@@ -42,10 +41,7 @@ export default async function AdminLayout({
name: endgameSubmissions.name,
checked: endgameSubmissions.checked,
queue: endgameSubmissions.queue,
paid: or(
eq(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
) as SQL<boolean>,
paid: endgameSubmissions.paid,
})
.from(endgameSubmissions)
.orderBy(endgameSubmissions.id);
+5 -22
View File
@@ -1,17 +1,6 @@
"use server";
import {
and,
desc,
eq,
gt,
isNotNull,
isNull,
lt,
not,
or,
sql,
} from "drizzle-orm";
import { and, desc, eq, gt, lt, not, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import image from "next/image";
@@ -57,13 +46,8 @@ export async function random() {
const [sub] = await db
.select()
.from(endgameSubmissions)
.where(
and(
not(endgameSubmissions.checked),
or(eq(endgameSubmissions.price, 0), isNotNull(endgameSubmissions.slip)),
),
)
.orderBy(sql`RANDOM()`)
.where(endgameSubmissions.paid.getSQL())
.orderBy(sql`${endgameSubmissions.checked} ASC, RANDOM()`)
.limit(1);
if (sub) redirect(`/rubgram/admin/${sub.id}`);
else throw "ไม่พบผู้ลงทะเบียนที่ยังไม่ตรวจสอบ";
@@ -348,8 +332,7 @@ export async function calcPrice(service: string[]) {
async function removeExpiredSubmissions() {
// First, get count of items to be removed
const expiredCond = and(
not(eq(endgameSubmissions.price, 0)),
isNull(endgameSubmissions.slip),
not(endgameSubmissions.paid),
lt(endgameSubmissions.expires, new Date()),
);
const toRemove = await db
@@ -420,7 +403,7 @@ export async function discordCall(id: string) {
"Content-Type": "application/json",
},
}).then(async (r) => {
console.log(await r.text());
console.log(`called`, await r.text());
return r.ok;
});
}
+1 -1
View File
@@ -214,7 +214,7 @@ export function CancelButton({ sid }: { sid: string }) {
}}
disabled={loading}
>
{loading && <Spinner />}ยกเลิก
{loading && <Spinner />}ใช่
</Button>
);
}
+14 -17
View File
@@ -1,13 +1,9 @@
import {
and,
eq,
gt,
isNotNull,
isNull,
lt,
gt, lt,
not,
or,
type SQL,
or
} from "drizzle-orm";
import {
AlertCircle,
@@ -89,10 +85,7 @@ export default async function EndgamePage() {
? await db
.select({
queue: endgameSubmissions.queue,
paid: or(
eq(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
) as SQL<boolean>,
paid: endgameSubmissions.paid,
price: endgameSubmissions.price,
expires: endgameSubmissions.expires,
})
@@ -114,8 +107,7 @@ export default async function EndgamePage() {
.from(endgameSubmissions)
.where(
and(
not(eq(endgameSubmissions.price, 0)),
isNull(endgameSubmissions.slip),
not(endgameSubmissions.paid),
lt(endgameSubmissions.queue, q.queue),
),
)
@@ -293,13 +285,13 @@ export default async function EndgamePage() {
</SimpleTooltip>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>แน่ใจเหรอ</AlertDialogTitle>
<AlertDialogTitle>แน่ใจหรอ</AlertDialogTitle>
<AlertDialogDescription>
ต้องการยกเลิกคิวของคุณหรือไม่
ต้องการยกเลิกการลงคิวของคุณหรือไม่
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>ไม่ใช่</AlertDialogCancel>
<AlertDialogCancel>ไม่ยกเลิก</AlertDialogCancel>
<CancelButton sid={sid!.value} />
</AlertDialogFooter>
</AlertDialogContent>
@@ -330,9 +322,14 @@ export default async function EndgamePage() {
</div>
</CardFooter>
</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>
{sid && !q && <ClearCookie />}
</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 { db } from "@/lib/db";
import {
@@ -94,7 +94,10 @@ export default async function TierlistPage({
const badgesList = await tx
.select()
.from(tierlistBadges)
.orderBy(tierlistBadges.order);
.orderBy(tierlistBadges.order)
.where(
or(isNull(tierlistBadges.type), eq(tierlistBadges.type, version.type)),
);
const badges = badgesList.map((b) => ({
...b,
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 { redis } from "@/lib/db/redis";
export const revalidate = 900;
export async function GET() {
const database = await db
.execute(sql<boolean>`select true`)
.then(() => true)
.catch(() => false);
const red = await redis
.ping()
.then(() => true)
.catch(() => false);
const enka = await fetch("https://enka.network/api/uid/888888888/?info", {
cache: "force-cache",
headers: { "User-Agent": "Buzz Event Platform(Health Checker)" },
signal: AbortSignal.timeout(5000),
})
.then((r) => r.ok)
.catch(() => false);
const amber = await fetch("https://gi.yatta.moe/api/v2/en/avatar", {
cache: "force-cache",
signal: AbortSignal.timeout(5000),
})
.then((r) => r.ok)
.catch(() => false);
return Response.json({ database, red, enka, amber });
const res = Object.fromEntries(
await Promise.all(
Object.entries(checks).map(
async ([k, v]) =>
[
k,
red
? (await redis.exists(`health:${k}`))
? (await redis.get(`health:${k}`)) === "ok"
: await v().then(async (v) => {
if (v) await redis.setex(`health:${k}`, 900, "ok");
return v;
})
: 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.slip) notFound();
console.log(sub.slip.length);
return new Response(new Uint8Array(sub.slip), {
headers: {
"Content-Type": "image/jpeg",
+11 -11
View File
@@ -52,7 +52,7 @@
"framer-motion": "^12.23.26",
"input-otp": "^1.4.2",
"lucide-react": "^0.544.0",
"next": "16.0.7",
"next": "16.0.10",
"next-themes": "^0.4.6",
"pg": "^8.16.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/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=="],
@@ -849,7 +849,7 @@
"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=="],
+7 -4
View File
@@ -285,7 +285,9 @@ export async function cdnify(
export async function actionLog(text: string, details?: unknown) {
const session = await adminCheck();
const res = await db
console.log(` LOG ${session?.name || ""} ${text}`);
const [res] = await db
.insert(auditLog)
.values({
author: session?.name,
@@ -293,11 +295,12 @@ export async function actionLog(text: string, details?: unknown) {
details,
})
.returning()
.catch(() =>
.catch(() => {
console.error(
`Error logging audit log, printing it here:\n${session?.name || "[Unknown User]"} - ${text}`,
),
);
);
return [];
});
revalidatePath("/admin/log");
+14
View File
@@ -1,3 +1,4 @@
import { isNotNull, lte, or, type SQL } from "drizzle-orm";
import {
type AnyPgColumn,
boolean,
@@ -128,6 +129,15 @@ export const endgameSubmissions = endgame.table("submissions", {
price: integer().notNull(), // calculated on submission
slip: text().references(() => endgameSlips.id), // not paid = null
checked: boolean().notNull().default(false),
paid: boolean()
.notNull()
.generatedAlwaysAs(
(): SQL =>
or(
lte(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
)!,
),
});
// deleted submissions
@@ -226,6 +236,10 @@ export const tierlistBadges = tierlist.table("badges", {
onUpdate: "cascade",
}),
order: integer().notNull(),
type: text().references(() => tierlistTypes.id, {
onDelete: "set null",
onUpdate: "cascade",
}),
});
export const tierlistVersions = tierlist.table("versions", {
+26 -16
View File
@@ -1,29 +1,38 @@
events {}
http {
map $uri $loggable {
default 1;
~^/_next 0;
# 1 = log, 0 = don't log (based on path)
map $uri $loggable_uri {
default 1;
~^/_next 0;
=/favicon.webp 0;
=/robots.txt 0;
~^/api/.*/count$ 0;
}
=/favicon.webp 0;
=/robots.txt 0;
~^/api/.*/count$ 0;
}
# 1 = log, 0 = don't log (based on header)
map $http_rsc $loggable_prefetch {
default 1;
"" 1; # header not present
~.+ 0; # header exists (any value)
}
log_format clean ' $request_method $uri $status';
access_log /var/log/nginx/access.log clean if=$loggable;
# 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';
access_log /var/log/nginx/access.log clean if=$loggable;
upstream backend {
# Use the service name defined in docker-compose.yml
server app:3000;
}
server {
listen 80;
# Docker's internal DNS resolver
resolver 127.0.0.11 valid=1s ipv6=off;
resolver 127.0.0.11 valid=5s ipv6=off;
location / {
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-Proto $scheme;
# SSE support if you want it
proxy_http_version 1.1;
proxy_set_header Connection "";
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",
"dr": "drizzle-kit",
"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"
},
"dependencies": {
@@ -62,7 +64,7 @@
"framer-motion": "^12.23.26",
"input-otp": "^1.4.2",
"lucide-react": "^0.544.0",
"next": "16.0.7",
"next": "16.0.10",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"radix-ui": "^1.4.3",