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
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const findMany = vi.hoisted(() => vi.fn());
const create = vi.hoisted(() => vi.fn());
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const createFn = vi.hoisted(() => vi.fn());
const sendMailFn = vi.hoisted(() => vi.fn().mockResolvedValue(true));
-1
View File
@@ -16,7 +16,6 @@ import { sendMail } from "@/lib/services/email";
// fetch (Node 18+/Next 16) — no extra packages.
//
export type AlertSeverity =
| "info"
| "notice"
+41 -6
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const create = vi.hoisted(() => vi.fn());
const findMany = vi.hoisted(() => vi.fn());
@@ -14,7 +14,7 @@ vi.mock("@/lib/prisma", () => ({
vi.mock("@/env", () => ({ env: {} }));
import { logAudit, getAuditLogs } from "./audit";
import { getAuditLogs, logAudit } from "./audit";
beforeEach(() => {
vi.clearAllMocks();
@@ -87,8 +87,28 @@ describe("logAudit", () => {
describe("getAuditLogs", () => {
it("returns paginated logs with usernames", async () => {
findMany.mockResolvedValue([
{ id: 1, userId: 1, action: "test", target: "user", targetId: null, before: null, after: null, diff: null, createdAt: "2024-01-01" },
{ id: 2, userId: 2, action: "test2", target: "room", targetId: null, before: null, after: null, diff: null, createdAt: "2024-01-02" },
{
id: 1,
userId: 1,
action: "test",
target: "user",
targetId: null,
before: null,
after: null,
diff: null,
createdAt: "2024-01-01",
},
{
id: 2,
userId: 2,
action: "test2",
target: "room",
targetId: null,
before: null,
after: null,
diff: null,
createdAt: "2024-01-02",
},
]);
count.mockResolvedValue(2);
userFindMany.mockResolvedValue([
@@ -110,14 +130,29 @@ describe("getAuditLogs", () => {
await getAuditLogs({ search: "test" });
expect(findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { OR: [{ action: { contains: "test" } }, { target: { contains: "test" } }] },
where: {
OR: [
{ action: { contains: "test" } },
{ target: { contains: "test" } },
],
},
}),
);
});
it("falls back to User #id for unknown users", async () => {
findMany.mockResolvedValue([
{ id: 1, userId: 99, action: "x", target: "y", targetId: null, before: null, after: null, diff: null, createdAt: "2024-01-01" },
{
id: 1,
userId: 99,
action: "x",
target: "y",
targetId: null,
before: null,
after: null,
diff: null,
createdAt: "2024-01-01",
},
]);
count.mockResolvedValue(1);
userFindMany.mockResolvedValue([]);
+65 -7
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const findUnique = vi.hoisted(() => vi.fn());
const findMany = vi.hoisted(() => vi.fn());
@@ -11,7 +11,14 @@ const catalogUpdateMany = vi.hoisted(() => vi.fn());
vi.mock("@/lib/prisma", () => ({
prisma: {
catalogPages: { findUnique, findMany, update, create, delete: catalogDelete, updateMany: catalogUpdateMany },
catalogPages: {
findUnique,
findMany,
update,
create,
delete: catalogDelete,
updateMany: catalogUpdateMany,
},
$queryRaw: queryRaw,
$executeRaw: executeRaw,
},
@@ -19,8 +26,15 @@ vi.mock("@/lib/prisma", () => ({
vi.mock("@/env", () => ({ env: {} }));
import { buildNestedTree, toInt, movePage, deletePage, getAncestors, createPage } from "./catalog-tree";
import type { TreeNode } from "@/types/catalog";
import {
buildNestedTree,
createPage,
deletePage,
getAncestors,
movePage,
toInt,
} from "./catalog-tree";
beforeEach(() => {
vi.clearAllMocks();
@@ -44,9 +58,48 @@ describe("toInt", () => {
describe("buildNestedTree", () => {
const flat: TreeNode[] = [
{ id: 1, caption: "Root", parentId: -1, depth: 0, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 2, itemCount: 0 },
{ id: 2, caption: "Child", parentId: 1, depth: 1, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 1, itemCount: 5 },
{ id: 3, caption: "Grandchild", parentId: 2, depth: 2, orderNum: 1, enabled: "1", visible: "1", iconImage: 0, iconColor: 0, pageLayout: "default_3x3", childCount: 0, itemCount: 0 },
{
id: 1,
caption: "Root",
parentId: -1,
depth: 0,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 2,
itemCount: 0,
},
{
id: 2,
caption: "Child",
parentId: 1,
depth: 1,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 1,
itemCount: 5,
},
{
id: 3,
caption: "Grandchild",
parentId: 2,
depth: 2,
orderNum: 1,
enabled: "1",
visible: "1",
iconImage: 0,
iconColor: 0,
pageLayout: "default_3x3",
childCount: 0,
itemCount: 0,
},
];
it("builds nested tree from flat nodes", () => {
@@ -89,7 +142,12 @@ describe("createPage", () => {
it("converts boolean visible/enabled to string", async () => {
create.mockResolvedValue({ id: 1 });
await createPage({ parentId: 0, caption: "x", visible: false, enabled: false });
await createPage({
parentId: 0,
caption: "x",
visible: false,
enabled: false,
});
expect(create.mock.calls[0][0].data.visible).toBe("0");
expect(create.mock.calls[0][0].data.enabled).toBe("0");
});
+12 -12
View File
@@ -234,10 +234,10 @@ export async function cloneSingleFurni(params: {
// Rollback: remove both downloaded files so we don't leave orphaned assets.
await fs.unlink(/*turbopackIgnore: true*/ nitroPath).catch(() => {});
await fs.unlink(/*turbopackIgnore: true*/ iconPath).catch(() => {});
logger.warn(
"[clone-import] items_base insert failed for",
{ classname, error: (err as Error).message },
);
logger.warn("[clone-import] items_base insert failed for", {
classname,
error: (err as Error).message,
});
return {
ok: false,
classname,
@@ -254,10 +254,10 @@ export async function cloneSingleFurni(params: {
itemType,
);
} catch (err) {
logger.warn(
"[clone-import] FurnitureData append failed for",
{ classname, error: (err as Error).message },
);
logger.warn("[clone-import] FurnitureData append failed for", {
classname,
error: (err as Error).message,
});
warnings.push(`FurnitureData append failed: ${(err as Error).message}`);
}
@@ -272,10 +272,10 @@ export async function cloneSingleFurni(params: {
return nextCatalogId;
});
} catch (err) {
logger.warn(
"[clone-import] catalog entry failed for",
{ classname, error: (err as Error).message },
);
logger.warn("[clone-import] catalog entry failed for", {
classname,
error: (err as Error).message,
});
warnings.push(`catalog entry failed: ${(err as Error).message}`);
}
+9 -13
View File
@@ -436,7 +436,6 @@ export async function importSingleFurni(params: {
const starIdx = classname.indexOf("*");
const hasColor = starIdx !== -1;
const baseClassname = hasColor ? classname.substring(0, starIdx) : classname;
const _colorNum = hasColor ? classname.substring(starIdx + 1) : "2";
const iconFileName = classname.replace(/\*/g, "_");
const safeSwfName = baseClassname;
const safeNitroName = baseClassname;
@@ -536,10 +535,9 @@ export async function importSingleFurni(params: {
warnings.push("SWF not available for Nitro conversion");
}
} catch (err) {
logger.warn(
"[import-furni] SWF to Nitro conversion failed",
{ error: (err as Error).message },
);
logger.warn("[import-furni] SWF to Nitro conversion failed", {
error: (err as Error).message,
});
warnings.push(
`SWF to Nitro conversion failed: ${(err as Error).message}`,
);
@@ -665,10 +663,9 @@ export async function importSingleFurni(params: {
try {
await appendFurniEntry(furniEntry, itemType);
} catch (err) {
logger.warn(
"[import-furni] Failed to update FurnitureData.json",
{ error: (err as Error).message },
);
logger.warn("[import-furni] Failed to update FurnitureData.json", {
error: (err as Error).message,
});
warnings.push("FurnitureData.json update failed");
}
}
@@ -707,10 +704,9 @@ export async function importSingleFurni(params: {
});
}
} catch (err) {
logger.warn(
"[import-furni] Failed to create catalog entry",
{ error: (err as Error).message },
);
logger.warn("[import-furni] Failed to create catalog entry", {
error: (err as Error).message,
});
warnings.push("Catalog entry creation failed");
}
+7 -12
View File
@@ -5,13 +5,13 @@
export type { HabboItFurniEntry } from "@/types/furni";
import { logger } from "@/lib/logger";
import type { HabboItFurniEntry } from "@/types/furni";
import {
habboFurnidataUrl,
type HabboGamedataHotel,
habboFurnidataUrl,
} from "@/lib/habbo-gamedata-hotel";
import { logger } from "@/lib/logger";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
import type { HabboItFurniEntry } from "@/types/furni";
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
@@ -30,11 +30,7 @@ export async function getHabboItFurnidata(): Promise<
const hotel = await getHabboGamedataHotel();
const now = Date.now();
if (
cache &&
cacheHotel === hotel &&
now - cacheTimestamp < CACHE_TTL
) {
if (cache && cacheHotel === hotel && now - cacheTimestamp < CACHE_TTL) {
return cache;
}
@@ -125,10 +121,9 @@ export async function getHabboItFurnidata(): Promise<
cacheTimestamp = Date.now();
return map;
} catch (err) {
logger.warn(
"[habbo-furnidata] Fetch error",
{ error: (err as Error).message },
);
logger.warn("[habbo-furnidata] Fetch error", {
error: (err as Error).message,
});
return cache || new Map();
} finally {
loading = null;
+4 -8
View File
@@ -7,13 +7,13 @@
export type { HabboAssetBadge } from "@/types/furni";
import { logger } from "@/lib/logger";
import type { HabboAssetBadge } from "@/types/furni";
import {
habboExternalTextsUrl,
type HabboGamedataHotel,
habboExternalTextsUrl,
} from "@/lib/habbo-gamedata-hotel";
import { logger } from "@/lib/logger";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
import type { HabboAssetBadge } from "@/types/furni";
const BADGE_IMAGE_BASE = "https://images.habbo.com/c_images/album1584";
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
@@ -39,11 +39,7 @@ async function loadBadges(): Promise<{
}> {
const hotel = await getHabboGamedataHotel();
const now = Date.now();
if (
badgeCache &&
cacheHotel === hotel &&
now - cacheTimestamp < CACHE_TTL
) {
if (badgeCache && cacheHotel === hotel && now - cacheTimestamp < CACHE_TTL) {
return { badges: badgeCache, hotel };
}
if (loadingPromise) {
+8 -3
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const findMany = vi.hoisted(() => vi.fn());
@@ -29,7 +29,10 @@ describe("moderation", () => {
it("blocks text containing a filtered word", async () => {
findMany.mockResolvedValue([{ word: "badword" }]);
const result = await isAllowed("this contains badword here");
expect(result).toEqual({ ok: false, reason: 'Blocked by word filter: "badword"' });
expect(result).toEqual({
ok: false,
reason: 'Blocked by word filter: "badword"',
});
});
it("allows empty text", async () => {
@@ -46,7 +49,9 @@ describe("moderation", () => {
it("moderateOrThrow throws on blocked content", async () => {
findMany.mockResolvedValue([{ word: "bad" }]);
await expect(moderateOrThrow("this is bad")).rejects.toThrow("Blocked by word filter");
await expect(moderateOrThrow("this is bad")).rejects.toThrow(
"Blocked by word filter",
);
});
it("moderateOrThrow resolves on clean content", async () => {
+7 -2
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { describe, expect, it, vi } from "vitest";
vi.mock("@/env", () => ({
env: {
@@ -10,7 +10,12 @@ vi.mock("@/env", () => ({
},
}));
import { isPayPalConfigured, creditsPerUnit, PAYPAL_API, PAYPAL_CURRENCY } from "./paypal";
import {
creditsPerUnit,
isPayPalConfigured,
PAYPAL_API,
PAYPAL_CURRENCY,
} from "./paypal";
describe("paypal", () => {
it("isPayPalConfigured returns true when credentials are set", () => {
+1 -2
View File
@@ -4,8 +4,7 @@
import { env } from "@/env";
export const PAYPAL_API =
env.PAYPAL_API?.replace(/\/+$/, "") ??
"https://api-m.sandbox.paypal.com";
env.PAYPAL_API?.replace(/\/+$/, "") ?? "https://api-m.sandbox.paypal.com";
export const PAYPAL_CURRENCY = env.PAYPAL_CURRENCY.toUpperCase();
+2 -2
View File
@@ -2,9 +2,9 @@ import { describe, expect, it } from "vitest";
import {
getRankPermissionColumn,
toPermissionValue,
splitRankUpdateFields,
RANK_GENERAL_FIELDS,
splitRankUpdateFields,
toPermissionValue,
} from "./permission-ranks";
describe("getRankPermissionColumn", () => {
+13 -3
View File
@@ -19,7 +19,12 @@ describe("sendCurrency", () => {
giveDiamonds: vi.fn(),
givePointsGotw: vi.fn(),
};
const result = await sendCurrency({ rcon, db: {} as any }, 1, "credits", 100);
const result = await sendCurrency(
{ rcon, db: {} as any },
1,
"credits",
100,
);
expect(result).toBe(true);
expect(rcon.giveCredits).toHaveBeenCalledWith(1, 100);
});
@@ -55,7 +60,12 @@ describe("sendCurrency", () => {
giveDiamonds: vi.fn(),
givePointsGotw: vi.fn(),
};
const result = await sendCurrency({ rcon, db: {} as any }, 1, "duckets", 50);
const result = await sendCurrency(
{ rcon, db: {} as any },
1,
"duckets",
50,
);
expect(result).toBe(true);
expect(rcon.giveDuckets).toHaveBeenCalledWith(1, 50);
});
@@ -138,4 +148,4 @@ describe("sendCurrency", () => {
}),
);
});
});
});
+8 -7
View File
@@ -1,6 +1,7 @@
import "server-only";
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
import { logger } from "@/lib/logger";
import { prisma } from "@/lib/prisma";
import { redis } from "@/lib/redis";
@@ -25,6 +26,7 @@ class SiteSettings {
});
return new Map(rows.map((r) => [r.key, r.value]));
} catch {
logger.warn("Failed to load site settings from database, using defaults");
return new Map(Object.entries(DEFAULTS));
}
}
@@ -38,7 +40,7 @@ class SiteSettings {
return new Map(Object.entries(parsed));
}
} catch {
// Redis unavailable — fall through
logger.warn("Redis cache read failed for site settings");
}
}
@@ -56,7 +58,7 @@ class SiteSettings {
JSON.stringify(obj),
);
} catch {
// non-critical
logger.warn("Failed to write site settings to Redis cache");
}
}
@@ -78,13 +80,12 @@ class SiteSettings {
return fallback;
}
async getMany(
keys: string[],
): Promise<Record<string, string | null>> {
async getMany(keys: string[]): Promise<Record<string, string | null>> {
const map = await this.load();
const result: Record<string, string | null> = {};
for (const key of keys) {
result[key] = map.get(key) ?? (key in DEFAULTS ? DEFAULTS[key] as string : null);
result[key] =
map.get(key) ?? (key in DEFAULTS ? (DEFAULTS[key] as string) : null);
}
return result;
}
@@ -111,7 +112,7 @@ class SiteSettings {
try {
await redis.del(REDIS_CACHE_KEY);
} catch {
// non-critical
logger.warn("Failed to invalidate Redis cache for site settings");
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
const create = vi.hoisted(() => vi.fn());
+1 -8
View File
@@ -5,12 +5,7 @@
* then bundles everything into the .nitro format expected
* by the Nitro HTML5 client.
*/
import { gunzipSync, inflateSync } from "node:zlib";
import type {
BatchConversionItem,
BatchConversionResult,
ConversionResult,
} from "@/types/furni";
import type { ConversionResult } from "@/types/furni";
import {
parseBitsJpeg2,
parseBitsJpeg3,
@@ -535,5 +530,3 @@ export function extractIconFromSwf(
return null;
}
}
+12 -3
View File
@@ -35,7 +35,10 @@ export interface UploadParams {
customparams?: string;
}
const MIGRATIONS_DIR = path.resolve(/*turbopackIgnore: true*/ process.cwd(), "prisma/migrations");
const MIGRATIONS_DIR = path.resolve(
/*turbopackIgnore: true*/ process.cwd(),
"prisma/migrations",
);
let itemsBaseIdAllocChain: Promise<unknown> = Promise.resolve();
@@ -99,7 +102,10 @@ async function writeSqlMigration(params: {
const padded = String(num).padStart(4, "0");
const className = params.classname.replace(/[^a-zA-Z0-9_-]/g, "_");
const fileName = `${padded}_import_furni_${className}.sql`;
const filePath = path.join(/*turbopackIgnore: true*/ MIGRATIONS_DIR, fileName);
const filePath = path.join(
/*turbopackIgnore: true*/ MIGRATIONS_DIR,
fileName,
);
const lines: string[] = [
`-- Migration: ${fileName}`,
@@ -192,7 +198,10 @@ export async function uploadSingleFurni(params: {
const { iconDir, nitroDir } = await getFurniAssetDirs();
const nitroFileName = `${classname}.nitro`;
const nitroPath = path.join(/*turbopackIgnore: true*/ nitroDir, nitroFileName);
const nitroPath = path.join(
/*turbopackIgnore: true*/ nitroDir,
nitroFileName,
);
if (existsSync(nitroPath)) {
return {
ok: false,
+1 -1
View File
@@ -1,5 +1,5 @@
import { logger } from "@/lib/logger";
import { env } from "@/env";
import { logger } from "@/lib/logger";
import { prisma } from "../prisma";
export type { WebhookAction } from "@/types/admin";