Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m52s

- Remove .prettierrc (dead config, Biome replaces Prettier)
- Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat
- Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit
- Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts
- Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars
- Replace barrel export src/types/index.ts with direct @/types/common imports
- Make trustHost conditional (development only) in auth.ts
- Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations
- Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
This commit is contained in:
openhands committed 2026-07-26 20:28:11 +02:00
1 parent b922f6d49f
commit 17847545dd
292 files changed
+69195 -67915

No files matched your search

+1 -1
View File
@@ -5,11 +5,11 @@ import { redirect } from "next/navigation";
import { z } from "zod";
import { requirePermission } from "@/lib/admin/guard";
import { formPositiveBigInt } from "@/lib/form-data";
import { logger } from "@/lib/logger";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logger } from "@/lib/logger";
import { logStaffActivity } from "@/lib/services/staff-activity";
// CRUD for website advertisements (website_ads). Emulator does not own this
+1 -1
View File
@@ -2,8 +2,8 @@
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function dismissApplication(formData: FormData): Promise<void> {
+2 -4
View File
@@ -3,8 +3,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { slugify } from "@/lib/format";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
async function uniqueSlug(title: string): Promise<string> {
@@ -74,9 +74,7 @@ export async function updateArticle(formData: FormData): Promise<void> {
.normalize("NFC")
.trim()
.slice(0, 255),
...(rawSlug
? { slug: await uniqueSlug(rawSlug) }
: {}),
...(rawSlug ? { slug: await uniqueSlug(rawSlug) } : {}),
shortStory: String(formData.get("shortStory") ?? "")
.normalize("NFC")
.trim()
+1 -1
View File
@@ -2,8 +2,8 @@
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function createEmailTemplate(formData: FormData): Promise<void> {
+9 -5
View File
@@ -3,9 +3,9 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { canonicalize, sanitizeField } from "@/lib/foundation/security";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -26,8 +26,10 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
const imageUrl = sanitizeField(formData.get("imageUrl"));
const buttonText = sanitizeField(formData.get("buttonText"));
const buttonUrl = sanitizeField(formData.get("buttonUrl"));
const buttonColor = sanitizeField(formData.get("buttonColor"), 16) || "#eeb425";
const buttonBorderColor = sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15";
const buttonColor =
sanitizeField(formData.get("buttonColor"), 16) || "#eeb425";
const buttonBorderColor =
sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15";
try {
const entry = await prisma.websiteHelpCenterCategories.create({
@@ -73,8 +75,10 @@ export async function updateHelpQuestion(formData: FormData): Promise<void> {
const imageUrl = sanitizeField(formData.get("imageUrl"));
const buttonText = sanitizeField(formData.get("buttonText"));
const buttonUrl = sanitizeField(formData.get("buttonUrl"));
const buttonColor = sanitizeField(formData.get("buttonColor"), 16) || "#eeb425";
const buttonBorderColor = sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15";
const buttonColor =
sanitizeField(formData.get("buttonColor"), 16) || "#eeb425";
const buttonBorderColor =
sanitizeField(formData.get("buttonBorderColor"), 16) || "#facc15";
try {
await prisma.websiteHelpCenterCategories.update({
+5 -2
View File
@@ -4,8 +4,8 @@ import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { MEDIA_ROOT, resolveMediaPath } from "@/lib/media-storage";
import { PERMS } from "@/lib/permissions";
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -19,7 +19,10 @@ export async function uploadMedia(
if (file.size > MAX_SIZE)
return { ok: false, error: "File too large (max 5MB)" };
if (!ALLOWED.includes(file.type))
return { ok: false, error: "Invalid file type. Allowed: PNG, JPEG, GIF, WebP" };
return {
ok: false,
error: "Invalid file type. Allowed: PNG, JPEG, GIF, WebP",
};
const baseDir = MEDIA_ROOT;
// eslint-disable-next-line security/detect-non-literal-fs-filename
+1 -1
View File
@@ -2,8 +2,8 @@
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function createCategory(formData: FormData): Promise<void> {
+1 -1
View File
@@ -11,8 +11,8 @@ import {
} from "@/lib/habbo-gamedata-hotel";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { clearBadgeCache } from "@/lib/services/habboassets";
import { clearHabboItCache } from "@/lib/services/habbo-furnidata-cache";
import { clearBadgeCache } from "@/lib/services/habboassets";
import { siteSettings } from "@/lib/services/site-settings";
const managedKeySet = new Set(MANAGED_SETTING_KEYS);
+1 -1
View File
@@ -3,8 +3,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { logServerError } from "@/lib/server-log";
import { logStaffActivity } from "@/lib/services/staff-activity";
+5 -2
View File
@@ -7,6 +7,7 @@ import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { ensureReadableThemeColors } from "@/lib/theme-contrast";
import {
deleteCustomThemeStore,
getCustomTheme,
@@ -15,7 +16,6 @@ import {
} from "@/lib/theme-custom-store";
import { FONTS, PRESETS, THEME_COLOR_KEYS } from "@/lib/theme-presets";
import { presetSettings, settingKey } from "@/lib/theme-settings";
import { ensureReadableThemeColors } from "@/lib/theme-contrast";
// Only hex/keyword colour values are accepted (matches ThemeVars' sanitiser).
const COLOR_RE = /^[#a-zA-Z0-9(),.\s%-]+$/;
@@ -46,7 +46,10 @@ export async function saveTheme(formData: FormData): Promise<void> {
}
const fixed = ensureReadableThemeColors(bag);
for (const [key, value] of Object.entries(fixed)) {
await writeSetting(settingKey(key as (typeof THEME_COLOR_KEYS)[number], mode), value);
await writeSetting(
settingKey(key as (typeof THEME_COLOR_KEYS)[number], mode),
value,
);
}
}
const ADMIN_KEYS = [
+2 -2
View File
@@ -1,14 +1,14 @@
"use server";
import { revalidatePath } from "next/cache";
import { positiveBigInt } from "@/lib/api";
import { requirePermission } from "@/lib/admin/guard";
import { positiveBigInt } from "@/lib/api";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import {
type ActionResult,
actionError,
actionOk,
type ActionResult,
} from "@/lib/safe-action-shared";
import { logServerError } from "@/lib/server-log";
+2 -4
View File
@@ -5,9 +5,9 @@ import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import {
type ActionResult,
actionError,
actionOk,
type ActionResult,
} from "@/lib/safe-action-shared";
import { reloadWordFilter } from "@/lib/services/moderation";
import { rcon } from "@/lib/services/rcon";
@@ -33,9 +33,7 @@ export async function addWord(input: {
}
}
export async function deleteWord(input: {
id: string;
}): Promise<ActionResult> {
export async function deleteWord(input: { id: string }): Promise<ActionResult> {
await requirePermission(PERMS.WORDFILTER_EDIT);
const raw = String(input.id ?? "").normalize("NFC");
if (!raw) return actionError("Missing word id");
+50 -48
View File
@@ -64,63 +64,65 @@ export async function buyBadge(formData: FormData): Promise<void> {
if (!(await rateLimit(`draw-badge-buy:${userId}`, 5, 60_000)).ok) {
outcome = "ratelimit";
} else {
const badge = await prisma.websiteDrawbadges.findUnique({
where: { id: BigInt(rawId) },
select: { id: true, badgePath: true, published: true },
});
const badge = await prisma.websiteDrawbadges.findUnique({
where: { id: BigInt(rawId) },
select: { id: true, badgePath: true, published: true },
});
if (!badge?.published) {
outcome = "invalid";
} else {
const code = badgeCodeFromPath(badge.badgePath);
if (code.length === 0) {
if (!badge?.published) {
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";
const code = badgeCodeFromPath(badge.badgePath);
if (code.length === 0) {
outcome = "invalid";
} 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 price = await resolvePrice();
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 },
});
}
});
// 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 },
});
}
},
);
}
// Grant the badge live so it appears immediately for online users.
await rcon.giveBadge(userId, code).catch(() => {});
outcome = "bought";
boughtCode = code;
}
// Grant the badge live so it appears immediately for online users.
await rcon.giveBadge(userId, code).catch(() => {});
outcome = "bought";
boughtCode = code;
}
}
}
}
} catch {
outcome = "fail";
}
+16 -15
View File
@@ -53,9 +53,9 @@ describe("email verification tokens", () => {
it("rejects legacy forever-valid digests", async () => {
const legacy = "a".repeat(64);
expect(
await isValidVerificationToken("[email protected]", legacy),
).toBe(false);
expect(await isValidVerificationToken("[email protected]", legacy)).toBe(
false,
);
});
it("rejects expired tokens", async () => {
@@ -69,18 +69,19 @@ describe("email verification tokens", () => {
});
it("sends mail with a verify link", async () => {
mockGetTranslations.mockResolvedValue(
((key: string, values?: { hotel?: string }) => {
const map: Record<string, string> = {
subject: `Verify your email · ${values?.hotel}`,
heading: "Verify your email",
body: `Welcome to ${values?.hotel}!`,
button: "Verify email",
fallback: "Paste this link:",
};
return map[key] ?? key;
}) as never,
);
mockGetTranslations.mockResolvedValue(((
key: string,
values?: { hotel?: string },
) => {
const map: Record<string, string> = {
subject: `Verify your email · ${values?.hotel}`,
heading: "Verify your email",
body: `Welcome to ${values?.hotel}!`,
button: "Verify email",
fallback: "Paste this link:",
};
return map[key] ?? key;
}) as never);
await sendVerification("[email protected]");
expect(mockSendMail).toHaveBeenCalledWith(
+1 -1
View File
@@ -3,10 +3,10 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
import { positiveBigInt } from "@/lib/api";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { positiveBigInt } from "@/lib/api";
import { moderateOrThrow } from "@/lib/services/moderation";
import { createOwnedTicketReply } from "@/lib/services/ticket-replies";
+96 -92
View File
@@ -3,8 +3,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { prisma } from "@/lib/prisma";
import { clientIp, rateLimit } from "@/lib/rate-limit";
type FriendOutcome =
| "accepted"
@@ -71,68 +71,72 @@ export async function acceptFriend(formData: FormData): Promise<void> {
if (!(await rateLimit(`friend-accept:${meId}`, 10, 60_000)).ok) {
outcome = "ratelimit";
} else {
const requestId = Number(formData.get("requestId"));
if (!Number.isInteger(requestId) || requestId <= 0) {
outcome = "invalid";
} else {
// The request must exist AND be addressed to the session user.
const request = await prisma.messengerFriendrequests.findUnique({
where: { id: requestId },
select: { id: true, userFromId: true, userToId: true },
});
if (!request) {
outcome = "not_found";
} else if (request.userToId !== meId) {
outcome = "unauthorized";
const requestId = Number(formData.get("requestId"));
if (!Number.isInteger(requestId) || requestId <= 0) {
outcome = "invalid";
} else {
const friendId = request.userFromId;
if (!Number.isInteger(friendId) || friendId <= 0 || friendId === meId) {
// Malformed/self request — clear it and treat as not found.
await prisma.messengerFriendrequests.delete({
where: { id: requestId },
});
// The request must exist AND be addressed to the session user.
const request = await prisma.messengerFriendrequests.findUnique({
where: { id: requestId },
select: { id: true, userFromId: true, userToId: true },
});
if (!request) {
outcome = "not_found";
} else if (request.userToId !== meId) {
outcome = "unauthorized";
} else {
const friendsSince = Math.floor(Date.now() / 1000);
await prisma.$transaction(async (tx) => {
// Don't double-insert if a friendship already exists in either direction.
const existing = await tx.messengerFriendships.findFirst({
where: {
OR: [
{ userOneId: meId, userTwoId: friendId },
{ userOneId: friendId, userTwoId: meId },
],
},
select: { id: true },
const friendId = request.userFromId;
if (
!Number.isInteger(friendId) ||
friendId <= 0 ||
friendId === meId
) {
// Malformed/self request — clear it and treat as not found.
await prisma.messengerFriendrequests.delete({
where: { id: requestId },
});
outcome = "not_found";
} else {
const friendsSince = Math.floor(Date.now() / 1000);
if (!existing) {
await tx.messengerFriendships.createMany({
data: [
{ userOneId: meId, userTwoId: friendId, friendsSince },
{ userOneId: friendId, userTwoId: meId, friendsSince },
],
await prisma.$transaction(async (tx) => {
// Don't double-insert if a friendship already exists in either direction.
const existing = await tx.messengerFriendships.findFirst({
where: {
OR: [
{ userOneId: meId, userTwoId: friendId },
{ userOneId: friendId, userTwoId: meId },
],
},
select: { id: true },
});
}
// Clear this request and any reverse pending request between the pair.
await tx.messengerFriendrequests.deleteMany({
where: {
OR: [
{ id: requestId },
{ userFromId: meId, userToId: friendId },
{ userFromId: friendId, userToId: meId },
],
},
if (!existing) {
await tx.messengerFriendships.createMany({
data: [
{ userOneId: meId, userTwoId: friendId, friendsSince },
{ userOneId: friendId, userTwoId: meId, friendsSince },
],
});
}
// Clear this request and any reverse pending request between the pair.
await tx.messengerFriendrequests.deleteMany({
where: {
OR: [
{ id: requestId },
{ userFromId: meId, userToId: friendId },
{ userFromId: friendId, userToId: meId },
],
},
});
});
});
outcome = "accepted";
outcome = "accepted";
}
}
}
}
}
} catch (e) {
// redirect() throws a NEXT_REDIRECT control-flow signal — re-throw it.
if (
@@ -172,26 +176,26 @@ export async function declineFriendRequest(formData: FormData): Promise<void> {
if (!(await rateLimit(`friend-decline:${meId}`, 10, 60_000)).ok) {
outcome = "ratelimit";
} else {
const requestId = Number(formData.get("requestId"));
if (!Number.isInteger(requestId) || requestId <= 0) {
outcome = "invalid";
} else {
const request = await prisma.messengerFriendrequests.findUnique({
where: { id: requestId },
select: { id: true, userToId: true },
});
if (!request) {
outcome = "not_found";
} else if (request.userToId !== meId) {
outcome = "unauthorized";
const requestId = Number(formData.get("requestId"));
if (!Number.isInteger(requestId) || requestId <= 0) {
outcome = "invalid";
} else {
await prisma.messengerFriendrequests.delete({
const request = await prisma.messengerFriendrequests.findUnique({
where: { id: requestId },
select: { id: true, userToId: true },
});
outcome = "declined";
if (!request) {
outcome = "not_found";
} else if (request.userToId !== meId) {
outcome = "unauthorized";
} else {
await prisma.messengerFriendrequests.delete({
where: { id: requestId },
});
outcome = "declined";
}
}
}
}
} catch (e) {
if (
e &&
@@ -231,34 +235,34 @@ export async function removeFriendship(formData: FormData): Promise<void> {
if (!(await rateLimit(`friend-remove:${meId}`, 10, 60_000)).ok) {
outcome = "ratelimit";
} else {
const friendId = Number(formData.get("friendId"));
if (!Number.isInteger(friendId) || friendId <= 0 || friendId === meId) {
outcome = "invalid";
} else {
const deleted = await prisma.$transaction(async (tx) => {
const result = await tx.messengerFriendships.deleteMany({
where: {
OR: [
{ userOneId: meId, userTwoId: friendId },
{ userOneId: friendId, userTwoId: meId },
],
},
const friendId = Number(formData.get("friendId"));
if (!Number.isInteger(friendId) || friendId <= 0 || friendId === meId) {
outcome = "invalid";
} else {
const deleted = await prisma.$transaction(async (tx) => {
const result = await tx.messengerFriendships.deleteMany({
where: {
OR: [
{ userOneId: meId, userTwoId: friendId },
{ userOneId: friendId, userTwoId: meId },
],
},
});
await tx.messengerFriendrequests.deleteMany({
where: {
OR: [
{ userFromId: meId, userToId: friendId },
{ userFromId: friendId, userToId: meId },
],
},
});
return result.count;
});
await tx.messengerFriendrequests.deleteMany({
where: {
OR: [
{ userFromId: meId, userToId: friendId },
{ userFromId: friendId, userToId: meId },
],
},
});
return result.count;
});
outcome = deleted > 0 ? "removed" : "not_found";
}
outcome = deleted > 0 ? "removed" : "not_found";
}
}
} catch (e) {
if (
+1 -5
View File
@@ -12,11 +12,7 @@ const NAME_MAX = 255;
const TEXT_MAX = 5000;
const STYLE_MAX = 5000;
type ApplyOutcome =
| "submitted"
| "invalid"
| "ratelimit"
| "error";
type ApplyOutcome = "submitted" | "invalid" | "ratelimit" | "error";
function applyRedirect(outcome: ApplyOutcome): never {
if (outcome === "submitted") redirect("/radio/apply?submitted=1");
+1 -6
View File
@@ -9,12 +9,7 @@ import { clientIp, rateLimit } from "@/lib/rate-limit";
const SONG_MAX = 255;
const ARTIST_MAX = 255;
type RequestOutcome =
| "posted"
| "empty"
| "invalid"
| "ratelimit"
| "error";
type RequestOutcome = "posted" | "empty" | "invalid" | "ratelimit" | "error";
function requestsRedirect(outcome: RequestOutcome): never {
if (outcome === "posted") redirect("/radio/requests?posted=1");
+1 -6
View File
@@ -12,12 +12,7 @@ const shoutSchema = z.object({
message: z.string().min(1, "Message is required").max(255),
});
type ShoutOutcome =
| "posted"
| "invalid"
| "moderated"
| "ratelimit"
| "error";
type ShoutOutcome = "posted" | "invalid" | "moderated" | "ratelimit" | "error";
function shoutsRedirect(outcome: ShoutOutcome): never {
if (outcome === "posted") redirect("/radio/shouts?posted=1");
+4 -1
View File
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
import { z } from "zod";
import { sendVerification } from "@/actions/email-verify";
import { hashPassword } from "@/lib/auth/password";
import { logger } from "@/lib/logger";
import { prisma } from "@/lib/prisma";
import { clientIp, rateLimit } from "@/lib/rate-limit";
import { captchaConfig, verifyCaptcha } from "@/lib/services/captcha";
@@ -100,6 +101,7 @@ export async function register(
});
if (existing) return "That username is already taken";
} catch {
logger.warn("Username uniqueness check failed during registration");
return "Registration is temporarily unavailable";
}
@@ -122,10 +124,11 @@ export async function register(
try {
await sendVerification(mail);
} catch {
// No-op: account is created; user can request a new link later.
logger.warn("Failed to send verification email after registration");
}
}
} catch {
logger.warn("Account creation failed");
return "Could not create the account (is the username unique?)";
}
+1 -1
View File
@@ -3,9 +3,9 @@
import { mkdir, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { revalidatePath } from "next/cache";
import { resolveMediaPath } from "@/lib/media-storage";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { resolveMediaPath } from "@/lib/media-storage";
const FAVICON_DIR = resolveMediaPath("favicon");
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
+1 -1
View File
@@ -3,9 +3,9 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { revalidatePath } from "next/cache";
import { resolveMediaPath } from "@/lib/media-storage";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { resolveMediaPath } from "@/lib/media-storage";
const MEDIA_DIR = resolveMediaPath("logo");
+7 -4
View File
@@ -37,10 +37,13 @@ export async function signOutEverywhere(): Promise<void> {
where: personalTokenScope(userId),
});
} catch (err) {
logger.warn("Failed to revoke personal access tokens during sign-out-everywhere", {
userId,
error: err instanceof Error ? err.message : "Unknown",
});
logger.warn(
"Failed to revoke personal access tokens during sign-out-everywhere",
{
userId,
error: err instanceof Error ? err.message : "Unknown",
},
);
}
await signOut({ redirectTo: "/login?signedOutAll=1" });
+1 -6
View File
@@ -28,12 +28,7 @@ function parseBadgeCodes(raw: string | null | undefined): string[] {
.filter((s) => s.length > 0 && s.length <= 32);
}
type BuyOutcome =
| "bought"
| "invalid"
| "credits"
| "ratelimit"
| "error";
type BuyOutcome = "bought" | "invalid" | "credits" | "ratelimit" | "error";
function shopRedirect(
categoryId: string,
+1 -6
View File
@@ -22,12 +22,7 @@ type FriendRequestOutcome =
| "ratelimit"
| "error";
type ThreadOutcome =
| "posted"
| "invalid"
| "not_found"
| "ratelimit"
| "error";
type ThreadOutcome = "posted" | "invalid" | "not_found" | "ratelimit" | "error";
type ReplyOutcome =
| "replied"
+2
View File
@@ -388,6 +388,7 @@ export const muteUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: muteSchema },
async (ctx) => {
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank);
void _target;
const success = await rcon.muteUser(ctx.data.userId, ctx.data.duration);
if (!success)
throw new ActionError("Failed to mute. Is the emulator running?");
@@ -440,6 +441,7 @@ export const sendCredits = adminAction(
{ permission: PERMS.USERS_EDIT, schema: sendCreditsSchema },
async (ctx) => {
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank);
void _target;
const success = await rcon.giveCredits(ctx.data.userId, ctx.data.amount);
if (!success)
throw new ActionError("Failed to send credits. Is the emulator running?");