fix: harden admin actions, search, sanitization and repo hygiene
CI / check (push) Successful in 1m21s
CI / deploy (push) Successful in 1m25s

- Split approve/dismiss application workflows with distinct audit logs,
  rate-limited guards and real error logging
- Validate article status/date/id input and stop resetting publishedAt
  on every update
- Validate guild updates (state, forum enums, non-empty name) behind
  rate-limited guard
- Fix scheduled-article publishing (ignore NULL dates, set updatedAt,
  type-safe predicates)
- Harden admin search API (LIKE escaping, query cap, per-user
  rate limit, round-robin result cap) and fix search dialog
  abort/res.ok/loading races
- Lock down HTML sanitizer to an allowlist profile and add XSS tests
- Improve mobile nav accessibility (unique id, dialog role, focus
  management, scroll lock, outside close)
- Log swallowed server errors instead of silent catch blocks
- Remove dead eslint config, drop unused dompurify deps, restore knip
  CI step, add Playwright config with smoke spec
This commit is contained in:
openhands committed 2026-09-04 13:04:08 +02:00
1 parent 61769e355b
commit 399c047515
20 files changed
+435 -147

No files matched your search

+53 -11
View File
@@ -2,10 +2,7 @@
import { eq, inArray } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import {
requirePermission,
requirePermissionRateLimited,
} from "@/lib/admin/guard";
import { requirePermissionRateLimited } from "@/lib/admin/guard";
import {
db,
GuildForumViews,
@@ -19,6 +16,33 @@ import {
import { PERMS } from "@/lib/permissions";
import { logStaffActivity } from "@/lib/services/staff-activity";
const GUILD_STATES = [0, 1, 2] as const;
const GUILD_FORUM = ["0", "1"] as const;
const GUILD_FORUM_ACCESS = [
"EVERYONE",
"OWNER",
"ADMIN",
"MEMBER",
"NONE",
] as const;
const GUILD_MOD_ACCESS = ["ADMINS", "OWNER", "MEMBER", "NONE"] as const;
function parseGuildState(raw: unknown): number | null {
const value = Number(raw);
return (GUILD_STATES as readonly number[]).includes(value) ? value : null;
}
function parseEnum<T extends string>(
raw: unknown,
allowed: readonly T[],
fallback: T,
): T {
const value = String(raw ?? fallback).trim();
return (allowed as readonly string[]).includes(value)
? (value as T)
: fallback;
}
/** Disband a guild and clean related membership/forum rows. */
export async function disbandGuild(formData: FormData): Promise<void> {
const staff = await requirePermissionRateLimited(PERMS.USERS_EDIT);
@@ -68,22 +92,40 @@ export async function disbandGuild(formData: FormData): Promise<void> {
}
export async function updateGuild(formData: FormData): Promise<void> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const staff = await requirePermissionRateLimited(PERMS.USERS_EDIT);
const id = Number(formData.get("id"));
if (!(id > 0)) return;
const name = String(formData.get("name") ?? "")
.trim()
.slice(0, 50);
if (!name) return;
const description = String(formData.get("description") ?? "")
.trim()
.slice(0, 250);
const state = Number(formData.get("state"));
const forum = String(formData.get("forum") ?? "0");
const readForum = String(formData.get("readForum") ?? "EVERYONE");
const postMessages = String(formData.get("postMessages") ?? "EVERYONE");
const postThreads = String(formData.get("postThreads") ?? "EVERYONE");
const modForum = String(formData.get("modForum") ?? "ADMINS");
const state = parseGuildState(formData.get("state"));
if (state === null) return;
const forum = parseEnum(formData.get("forum"), GUILD_FORUM, "0");
const readForum = parseEnum(
formData.get("readForum"),
GUILD_FORUM_ACCESS,
"EVERYONE",
);
const postMessages = parseEnum(
formData.get("postMessages"),
GUILD_FORUM_ACCESS,
"EVERYONE",
);
const postThreads = parseEnum(
formData.get("postThreads"),
GUILD_FORUM_ACCESS,
"EVERYONE",
);
const modForum = parseEnum(
formData.get("modForum"),
GUILD_MOD_ACCESS,
"ADMINS",
);
await db
.update(Guilds)