This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+80
-80
@@ -2,11 +2,11 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import type { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
/**
|
||||
* Buy a published community-drawn badge for the SIGNED-IN user. Faithful to
|
||||
@@ -30,100 +30,100 @@ const DEFAULT_PRICE = 50;
|
||||
// The emulator badge code is the badge_path filename without its directory or
|
||||
// extension, restricted to the code charset the client accepts.
|
||||
function badgeCodeFromPath(badgePath: string): string {
|
||||
const base = badgePath.split(/[\\/]/).pop() ?? badgePath;
|
||||
const noExt = base.replace(/\.[^.]+$/, "");
|
||||
return noExt.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 32);
|
||||
const base = badgePath.split(/[\\/]/).pop() ?? badgePath;
|
||||
const noExt = base.replace(/\.[^.]+$/, "");
|
||||
return noExt.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 32);
|
||||
}
|
||||
|
||||
async function resolvePrice(): Promise<number> {
|
||||
const raw = await siteSettings.get("drawbadge.price", String(DEFAULT_PRICE));
|
||||
const n = Number(raw);
|
||||
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_PRICE;
|
||||
const raw = await siteSettings.get("drawbadge.price", String(DEFAULT_PRICE));
|
||||
const n = Number(raw);
|
||||
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_PRICE;
|
||||
}
|
||||
|
||||
export async function buyBadge(formData: FormData): Promise<void> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/login");
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/login");
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
if (!Number.isFinite(userId)) redirect("/login");
|
||||
const userId = Number(session.user.id);
|
||||
if (!Number.isFinite(userId)) redirect("/login");
|
||||
|
||||
// The form posts the badge row id; everything else (price, code) is resolved
|
||||
// server-side from trusted data — never from the client.
|
||||
const rawId = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!/^\d+$/.test(rawId)) redirect("/draw-badge?error=invalid");
|
||||
// The form posts the badge row id; everything else (price, code) is resolved
|
||||
// server-side from trusted data — never from the client.
|
||||
const rawId = String(formData.get("id") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!/^\d+$/.test(rawId)) redirect("/draw-badge?error=invalid");
|
||||
|
||||
let outcome: "bought" | "invalid" | "credits" | "fail";
|
||||
let boughtCode = "";
|
||||
let outcome: "bought" | "invalid" | "credits" | "fail";
|
||||
let boughtCode = "";
|
||||
|
||||
try {
|
||||
const badge = await prisma.websiteDrawbadges.findUnique({
|
||||
where: { id: BigInt(rawId) },
|
||||
select: { id: true, badgePath: true, published: true },
|
||||
});
|
||||
try {
|
||||
const badge = await prisma.websiteDrawbadges.findUnique({
|
||||
where: { id: BigInt(rawId) },
|
||||
select: { id: true, badgePath: true, published: true },
|
||||
});
|
||||
|
||||
if (!badge || !badge.published) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const code = badgeCodeFromPath(badge.badgePath);
|
||||
if (code.length === 0) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const price = await resolvePrice();
|
||||
if (!badge?.published) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const code = badgeCodeFromPath(badge.badgePath);
|
||||
if (code.length === 0) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const price = await resolvePrice();
|
||||
|
||||
// Re-read the buyer's live credit balance and verify it covers the cost.
|
||||
const buyer = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { credits: true },
|
||||
});
|
||||
if (!buyer || buyer.credits < price) {
|
||||
outcome = "credits";
|
||||
} else {
|
||||
// Atomically deduct credits and persist the badge so a failure
|
||||
// between the two operations cannot orphan the user.
|
||||
if (price > 0) {
|
||||
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
||||
await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: { credits: { decrement: price } },
|
||||
});
|
||||
// Re-read the buyer's live credit balance and verify it covers the cost.
|
||||
const buyer = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { credits: true },
|
||||
});
|
||||
if (!buyer || buyer.credits < price) {
|
||||
outcome = "credits";
|
||||
} else {
|
||||
// Atomically deduct credits and persist the badge so a failure
|
||||
// between the two operations cannot orphan the user.
|
||||
if (price > 0) {
|
||||
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
||||
await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: { credits: { decrement: price } },
|
||||
});
|
||||
|
||||
const existing = await tx.usersBadges.findFirst({
|
||||
where: { userId, badgeCode: code },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) {
|
||||
const max = await tx.usersBadges.aggregate({
|
||||
where: { userId },
|
||||
_max: { slotId: true },
|
||||
});
|
||||
const slotId = (max._max.slotId ?? 0) + 1;
|
||||
await tx.usersBadges.create({
|
||||
data: { userId, slotId, badgeCode: code },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const existing = await tx.usersBadges.findFirst({
|
||||
where: { userId, badgeCode: code },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!existing) {
|
||||
const max = await tx.usersBadges.aggregate({
|
||||
where: { userId },
|
||||
_max: { slotId: true },
|
||||
});
|
||||
const slotId = (max._max.slotId ?? 0) + 1;
|
||||
await tx.usersBadges.create({
|
||||
data: { userId, slotId, badgeCode: code },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Grant the badge live so it appears immediately for online users.
|
||||
await rcon.giveBadge(userId, code).catch(() => {});
|
||||
// Grant the badge live so it appears immediately for online users.
|
||||
await rcon.giveBadge(userId, code).catch(() => {});
|
||||
|
||||
outcome = "bought";
|
||||
boughtCode = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
outcome = "fail";
|
||||
}
|
||||
outcome = "bought";
|
||||
boughtCode = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
outcome = "fail";
|
||||
}
|
||||
|
||||
revalidatePath("/draw-badge");
|
||||
revalidatePath("/draw-badge");
|
||||
|
||||
// redirect() throws — it must live OUTSIDE the try/catch.
|
||||
if (outcome === "bought") {
|
||||
redirect(`/draw-badge?bought=${encodeURIComponent(boughtCode)}`);
|
||||
}
|
||||
redirect(`/draw-badge?error=${outcome}`);
|
||||
// redirect() throws — it must live OUTSIDE the try/catch.
|
||||
if (outcome === "bought") {
|
||||
redirect(`/draw-badge?bought=${encodeURIComponent(boughtCode)}`);
|
||||
}
|
||||
redirect(`/draw-badge?error=${outcome}`);
|
||||
}
|
||||
Reference in new issue
Block a user