feat(admin): ban appeals, photos polish, economy adjust, staff smoke
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m17s
CI / check (push) Successful in 25s
CI / deploy (push) Failing after 11s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-30 20:23:03 +02:00
1 parent fe46bd0544
commit 3ad3f9512c
36 files changed
+1185 -293

No files matched your search

+136
View File
@@ -181,3 +181,139 @@ export async function bulkGiveBadge({
data: { given, total: userIds.length, failedIds },
};
}
export async function bulkAdjustCurrency({
userIds,
amount,
type,
}: {
userIds: number[];
/** Positive = give, negative = take. Balances clamped at 0. */
amount: number;
type: "credits" | "pixels" | "points";
}): Promise<
ActionResult<{
adjusted: number;
total: number;
failedIds: Array<{ userId: number; reason: string }>;
}>
> {
const staff = await requirePermission(PERMS.USERS_EDIT);
if (!Number.isFinite(amount) || amount === 0) {
return { ok: false as const, error: "Amount must be a non-zero number" };
}
if (amount > 0) {
const given = await bulkGiveCurrency({ userIds, amount, type });
if (!given.ok) return given;
if (!given.data) {
return { ok: false as const, error: "Currency adjustment failed" };
}
return {
ok: true as const,
data: {
adjusted: given.data.given,
total: given.data.total,
failedIds: given.data.failedIds,
},
};
}
const take = Math.abs(Math.trunc(amount));
let adjusted = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
for (const userId of userIds) {
try {
if (type === "credits") {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { credits: true },
});
if (!user) {
failedIds.push({ userId, reason: "Not found" });
continue;
}
const next = Math.max(0, user.credits - take);
await prisma.user.update({
where: { id: userId },
data: { credits: next },
});
} else {
const currencyType = type === "pixels" ? 0 : 101;
const row = await prisma.usersCurrency.findUnique({
where: { userId_type: { userId, type: currencyType } },
});
const current = row?.amount ?? 0;
const next = Math.max(0, current - take);
await prisma.usersCurrency.upsert({
where: { userId_type: { userId, type: currencyType } },
update: { amount: next },
create: { userId, type: currencyType, amount: next },
});
}
adjusted++;
} catch {
failedIds.push({ userId, reason: "Database error" });
}
}
await logStaffActivity({
staffId: staff.id,
action: "bulk_adjust_currency",
description: `Adjusted ${amount} ${type} for ${adjusted} user(s) (DB-only take; no RCON debit)`,
targetType: "user",
});
return {
ok: true as const,
data: { adjusted, total: userIds.length, failedIds },
};
}
/**
* Persist trade lock on `sanctions.trade_locked_until`.
* No first-class RCON trade-lock helper in this CMS — DB only.
*/
export async function setTradeLock({
userId,
untilUnix,
}: {
userId: number;
/** Unix seconds; 0 clears the lock. */
untilUnix: number;
}): Promise<ActionResult<{ userId: number; untilUnix: number }>> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const until = Math.max(0, Math.trunc(untilUnix));
const existing = await prisma.sanctions.findFirst({
where: { habboId: userId },
select: { id: true },
});
if (existing) {
await prisma.sanctions.update({
where: { id: existing.id },
data: { tradeLockedUntil: until },
});
} else {
await prisma.sanctions.create({
data: {
habboId: userId,
tradeLockedUntil: until,
reason: until > 0 ? "Trade lock (CMS)" : "",
},
});
}
await logStaffActivity({
staffId: staff.id,
action: until > 0 ? "trade_lock" : "trade_unlock",
description:
until > 0
? `Trade-locked user #${userId} until ${until}`
: `Cleared trade lock for user #${userId}`,
targetType: "user",
targetId: userId,
});
return { ok: true as const, data: { userId, untilUnix: until } };
}