feat(housekeeping): deliver system vertical

This commit is contained in:
Simo committed 2026-08-28 23:31:47 +02:00
1 parent 0117b45d74
commit 3788ecd9f1
33 files changed
+4548 -423

No files matched your search

@@ -0,0 +1,94 @@
# Task 10 report: System vertical
## Outcome
Delivered the real System access, configuration, observability, and operations vertical from base `0117b45d74450510187f8f860ee927a193c45a3c` on `codex/housekeeping-complete`.
- Registered the exact 17 System route IDs, canonical `/ase/system/*` hrefs, labels, and read capabilities from `migration/system.ts`.
- Added injected access, configuration, observability, and operations queries with forbidden, partial, and dependency-unavailable results.
- Extracted redirect-free, server-only, capability-guarded mutation services from the six legacy action modules while retaining their existing permission checks and `/admin` revalidation behavior.
- Registered 29 sensitive System commands through deterministic bootstrap, dispatcher authorization, confirmation, rate limiting, and audit. Reasons are mandatory for ACL/permission changes, global settings, alert broadcast, every RCON operation, and maintenance/global availability.
- Added four query-backed server workflow page modules with loading, empty, partial, error, forbidden, and ready states.
- Added the exact 17 System handlers to the global aggregate. The manifest contains real routes and deliberately keeps providers, search, inbox, and widgets empty for Task 19.
No database operation, deployment, push, pull request update, Task 11 work, `/admin` or `/mod` cutover, rank-threshold authorization, or placeholder workflow was performed. `.remember/remember.md` remained untracked and untouched.
## TDD evidence
All Vitest commands used `--coverage.enabled=false` so each RED/GREEN cycle exercised only the named boundary.
| Phase | Exact command | RED | GREEN |
| --- | --- | --- | --- |
| Routes | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/routes.test.ts` | 1 file failed before tests: missing `./routes`. | 1 file, 3 tests passed. |
| Injected queries | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/queries/system-queries.test.ts` | 1 file failed before tests: missing `./access`. | 1 file, 6 tests passed. |
| Commands and guarded service | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/commands/system-commands.test.ts` | 1 file failed before tests: missing `../services/mutations`. | 1 file, 6 tests passed at the first command boundary. |
| Legacy alert and maintenance wrappers | `pnpm exec vitest run --coverage.enabled=false src/actions/admin-alerts.test.ts src/actions/admin-maintenance.test.ts` | 1 of 7 tests failed because the existing alert mock granted `notifications.edit` instead of the canonical `admin.notifications.edit`. | 2 files, 7 tests passed after correcting only the stale mock permission. |
| ACL wrapper extraction | `pnpm exec vitest run --coverage.enabled=false src/lib/admin/acl-management-contract.test.ts` | 1 of 2 tests failed before `access.permissions.update` was present. | 1 file, 2 tests passed after the wrapper delegated to the guarded service. |
| Workflow pages | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/pages/system-pages.test.tsx` | 1 file failed before tests: missing `./access`. | 1 file, 8 tests passed. |
| Handler/bootstrap integration | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/commands/bootstrap.test.ts` | 2 tests failed: System had 0 handlers instead of 17 and no 29-command bootstrap registration. | 2 files, 3 tests passed. |
| Review regressions | `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/domains/system/commands/system-commands.test.ts src/features/housekeeping/domains/system/services/mutations-production.test.ts src/lib/admin/acl-management-contract.test.ts` | 3 files failed; 11 tests failed and 6 passed. Failures proved missing alert reason, six whitespace-only inputs accepted, three alert dependency failures swallowed, and the public/non-strict production boundary. | 3 files, 17 tests passed after the minimal corrections. |
The first integrated target run passed 17 files and 179 tests. `pnpm typecheck` then exposed four integration-only type errors (a heterogeneous test tuple, a bigint alert identifier, and result-union narrowing); the corrected run passed. The first `pnpm test:housekeeping` exposed exactly three obsolete foundation assertions (40 files/372 tests otherwise passed). The three directly obsolete contracts were updated with strict positive System assertions without relaxing another domain; the focused rerun passed 2 files/38 tests and the then-current full suite passed 42 files/376 tests.
## Final verification
- `pnpm exec vitest run --coverage.enabled=false src/actions/admin-alerts.test.ts src/actions/admin-maintenance.test.ts src/lib/admin/acl-management-contract.test.ts src/features/housekeeping/domains/system/routes.test.ts src/features/housekeeping/domains/system/queries/system-queries.test.ts src/features/housekeeping/domains/system/commands/system-commands.test.ts src/features/housekeeping/domains/system/services/mutations-production.test.ts src/features/housekeeping/domains/system/pages/system-pages.test.tsx src/features/housekeeping/migration/system.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/foundation/commands/bootstrap.test.ts src/features/housekeeping/foundation/commands/registry.test.ts src/features/housekeeping/foundation/commands/dispatcher.test.ts src/features/housekeeping/foundation/commands/confirmation.test.ts src/features/housekeeping/foundation/commands/audit-envelope.test.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/registry.test.ts`  17 files, 185 tests passed.
- `pnpm exec vitest run --coverage.enabled=false src/lib/admin-operations-contract.test.ts src/lib/staff-smoke-contract.test.ts src/lib/admin/authorization-contract.test.ts`  3 files, 97 tests passed.
- `pnpm test:housekeeping`  43 files, 385 tests passed.
- `pnpm typecheck`  passed (`tsc --noEmit`).
- `pnpm exec biome check --formatter-enabled=false src/actions/admin-alerts.test.ts src/actions/admin-alerts.ts src/actions/admin-emulator.ts src/actions/admin-maintenance.ts src/actions/admin-settings.ts src/actions/commandocentrum.ts src/actions/permissions.ts src/features/housekeeping/domains/system src/features/housekeeping/foundation/commands/bootstrap.test.ts src/features/housekeeping/foundation/commands/bootstrap.ts src/features/housekeeping/foundation/foundation-source-contract.test.ts src/features/housekeeping/foundation/registry.test.ts src/features/housekeeping/route-handlers.test.ts src/features/housekeeping/route-handlers.ts src/lib/admin/acl-management-contract.test.ts`  checked 32 files; no fixes applied.
- `git diff --check`  exit 0; only expected Git autocrlf warnings.
- `git diff --cached --check`  exit 0 before staging and rerun after exact staging.
- Independent read-only re-review  0 Critical, 0 Important, 0 Minor; ready verdict.
Node/pnpm emitted this non-blocking warning during pnpm gates:
```text
[WARN] Unsupported engine: wanted: {"node":">=26.8.1 <27"} (current: {"node":"v26.7.0","pnpm":"11.24.0"})
```
## Architectural decisions
- The migration matrix remains the single source of route truth. The System route array is materialized from its exact identifiers and values, and tests assert ordered route/handler equality rather than set-only coverage.
- Query factories accept narrow adapters; production adapters reuse existing ACL, settings, emulator, health, online-user, analytics, log, alert, and maintenance services. This avoided unnecessary edits to `ops-health.ts` and `ops-online-users.ts`.
- `systemMutationService` is the only public production mutation boundary. It is server-only and repeats capability enforcement even when called by an already-guarded legacy action or an authorized dispatcher. The unguarded production adapter is module-private.
- Adapter exceptions and unsuccessful RCON sends become typed `DEPENDENCY_UNAVAILABLE` failures. Rank-delete conflict metadata travels in the standard `fieldErrors` shape; the legacy wrapper reconstructs the prior human-readable `ActionError`, keeping the dispatcher result schema strict.
- Command string schemas use a non-transforming `\S` check to reject whitespace-only values; normalization and trimming remain at the guarded service boundary. This preserves the foundation registry rule that command schemas contain no executable transforms.
- Existing semantic label keys were reused where they matched; missing System labels use stable functional keys without changing i18n catalogs outside the tested scope.
- Foundation source-boundary changes are a narrow source-to-import allowlist for the new System integration edges. Existing forbidden directions for every other domain remain asserted.
## Changed files
- `.superpowers/sdd/2026-08-26-housekeeping-completion/task-10-report.md`
- `src/actions/admin-alerts.test.ts`
- `src/actions/admin-alerts.ts`
- `src/actions/admin-emulator.ts`
- `src/actions/admin-maintenance.ts`
- `src/actions/admin-settings.ts`
- `src/actions/commandocentrum.ts`
- `src/actions/permissions.ts`
- `src/features/housekeeping/domains/system/commands/system-commands.test.ts`
- `src/features/housekeeping/domains/system/commands/system-commands.ts`
- `src/features/housekeeping/domains/system/manifest.ts`
- `src/features/housekeeping/domains/system/pages/access.tsx`
- `src/features/housekeeping/domains/system/pages/configuration.tsx`
- `src/features/housekeeping/domains/system/pages/observability.tsx`
- `src/features/housekeeping/domains/system/pages/operations.tsx`
- `src/features/housekeeping/domains/system/pages/system-pages.test.tsx`
- `src/features/housekeeping/domains/system/queries/access.ts`
- `src/features/housekeeping/domains/system/queries/configuration.ts`
- `src/features/housekeeping/domains/system/queries/observability.ts`
- `src/features/housekeeping/domains/system/queries/operations.ts`
- `src/features/housekeeping/domains/system/queries/system-queries.test.ts`
- `src/features/housekeeping/domains/system/route-handlers.ts`
- `src/features/housekeeping/domains/system/routes.test.ts`
- `src/features/housekeeping/domains/system/routes.ts`
- `src/features/housekeeping/domains/system/services/mutations-production.test.ts`
- `src/features/housekeeping/domains/system/services/mutations.ts`
- `src/features/housekeeping/foundation/commands/bootstrap.test.ts`
- `src/features/housekeeping/foundation/commands/bootstrap.ts`
- `src/features/housekeeping/foundation/foundation-source-contract.test.ts`
- `src/features/housekeeping/foundation/registry.test.ts`
- `src/features/housekeeping/route-handlers.test.ts`
- `src/features/housekeeping/route-handlers.ts`
- `src/lib/admin/acl-management-contract.test.ts`
+1 -1
View File
@@ -7,7 +7,7 @@ import { sendHotelAlert } from "./admin-alerts";
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
vi.mock("@/lib/permissions", () => ({
PERMS: { NOTIFICATIONS_EDIT: "notifications.edit" },
PERMS: { NOTIFICATIONS_EDIT: "admin.notifications.edit" },
}));
vi.mock("@/lib/db", () => ({
db: {
+34 -10
View File
@@ -1,11 +1,30 @@
"use server";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import {
type SystemMutationContext,
systemMutationService,
} from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import { createCorrelationId } from "@/features/housekeeping/foundation/correlation";
import { requirePermission } from "@/lib/admin/guard";
import { AlertLogs, db } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { rcon } from "@/lib/services/rcon";
function grantedMutationContext(
staff: { id: number; rank: number; username: string },
permission: string,
): SystemMutationContext {
const matches = (slug: string) => slug === permission;
return {
capability: createHousekeepingCapabilityContext(staff, {
isSuperAdmin: false,
has: matches,
hasAny: (...slugs) => slugs.some(matches),
hasAll: (...slugs) => slugs.every(matches),
}),
correlationId: createCorrelationId(),
};
}
/**
* Broadcast a hotel-wide alert to every online user via RCON.
@@ -14,7 +33,7 @@ import { rcon } from "@/lib/services/rcon";
* `message` payload. Staff-gated; the message is trimmed/bounded before send.
*/
export async function sendHotelAlert(formData: FormData): Promise<void> {
await requirePermission(PERMS.NOTIFICATIONS_EDIT);
const staff = await requirePermission(PERMS.NOTIFICATIONS_EDIT);
const message = String(formData.get("message") ?? "")
.normalize("NFC")
@@ -23,7 +42,11 @@ export async function sendHotelAlert(formData: FormData): Promise<void> {
if (!message) return;
try {
await rcon.send("hotelalert", { message });
await systemMutationService.execute(
grantedMutationContext(staff, PERMS.NOTIFICATIONS_EDIT),
"operations.alert.broadcast",
{ message },
);
} catch {
// Best-effort delivery (dead socket / emulator offline) — never 500 the
// admin page. The emulator writes its own alert_logs row on receipt.
@@ -34,12 +57,13 @@ export async function sendHotelAlert(formData: FormData): Promise<void> {
/** Mark every unread ops alert as read. */
export async function markAllAlertsRead(): Promise<void> {
await requirePermission(PERMS.NOTIFICATIONS_VIEW);
const staff = await requirePermission(PERMS.NOTIFICATIONS_VIEW);
try {
await db
.update(AlertLogs)
.set({ isRead: true, updatedAt: new Date() })
.where(eq(AlertLogs.isRead, false));
await systemMutationService.execute(
grantedMutationContext(staff, PERMS.NOTIFICATIONS_VIEW),
"operations.alerts.mark-read",
{},
);
} catch {
/* ignore */
}
+45 -11
View File
@@ -1,8 +1,10 @@
"use server";
import { revalidatePath } from "next/cache";
import { systemMutationService } from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import { createCorrelationId } from "@/features/housekeeping/foundation/correlation";
import { requirePermission } from "@/lib/admin/guard";
import { db, EmulatorSettings, EmulatorTexts } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
// emulator_settings: PK is the string column `key`, payload is `value` (VarChar 512).
@@ -10,8 +12,40 @@ import { PERMS } from "@/lib/permissions";
// Both tables are emulator-owned; we only ever read/update existing rows or add new
// keys via upsert. We never migrate or drop them.
function mutationContext(staff: {
id: number;
rank: number;
username: string;
}) {
const matches = (slug: string) => slug === PERMS.SETTINGS_EDIT;
return {
capability: createHousekeepingCapabilityContext(staff, {
isSuperAdmin: false,
has: matches,
hasAny: (...slugs: string[]) => slugs.some(matches),
hasAll: (...slugs: string[]) => slugs.every(matches),
}),
correlationId: createCorrelationId(),
};
}
async function requireMutation(
staff: { id: number; rank: number; username: string },
operation:
| "configuration.emulator-setting.update"
| "configuration.emulator-text.update",
input: { key: string; value: string },
): Promise<void> {
const result = await systemMutationService.execute(
mutationContext(staff),
operation,
input,
);
if (!result.ok) throw new Error(result.error.messageKey);
}
export async function updateEmulatorSetting(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
const key = String(formData.get("key") ?? "")
.normalize("NFC")
.trim()
@@ -20,15 +54,15 @@ export async function updateEmulatorSetting(formData: FormData): Promise<void> {
.normalize("NFC")
.slice(0, 512);
if (!key) return;
await db
.insert(EmulatorSettings)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } });
await requireMutation(staff, "configuration.emulator-setting.update", {
key,
value,
});
revalidatePath("/admin/emulator");
}
export async function updateEmulatorText(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
const key = String(formData.get("key") ?? "")
.normalize("NFC")
.trim()
@@ -37,9 +71,9 @@ export async function updateEmulatorText(formData: FormData): Promise<void> {
.normalize("NFC")
.slice(0, 4096);
if (!key) return;
await db
.insert(EmulatorTexts)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } });
await requireMutation(staff, "configuration.emulator-text.update", {
key,
value,
});
revalidatePath("/admin/emulator");
}
+29 -30
View File
@@ -1,10 +1,11 @@
"use server";
import { revalidatePath } from "next/cache";
import { systemMutationService } from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import { createCorrelationId } from "@/features/housekeeping/foundation/correlation";
import { requirePermission } from "@/lib/admin/guard";
import { db, WebsiteSetting } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
// Maintenance mode lives in three CMS-owned website_settings rows (mirrors
// AtomCMS's MaintenanceToggle Livewire component):
@@ -14,32 +15,25 @@ import { siteSettings } from "@/lib/services/site-settings";
// The Laravel login flow reads these via setting() to gate non-staff logins
// while maintenance is on, so the website_settings keys are the source of truth.
const KEY_ENABLED = "maintenance_enabled";
const KEY_MESSAGE = "maintenance_message";
const KEY_MIN_RANK = "min_maintenance_login_rank";
const COMMENTS: Record<string, string> = {
[KEY_ENABLED]: "Determines whether maintenance is enabled or not",
[KEY_MESSAGE]:
"The maintenance message displayed to users while maintenance is activated",
[KEY_MIN_RANK]:
"The minimum rank required to login to the hotel during maintenance",
};
async function upsertSetting(key: string, value: string): Promise<void> {
await db
.insert(WebsiteSetting)
.values({
key,
value,
// eslint-disable-next-line security/detect-object-injection -- key is one of 3 known const values
comment: COMMENTS[key] ?? null,
})
.onDuplicateKeyUpdate({ set: { value } });
function mutationContext(staff: {
id: number;
rank: number;
username: string;
}) {
const matches = (slug: string) => slug === PERMS.SETTINGS_EDIT;
return {
capability: createHousekeepingCapabilityContext(staff, {
isSuperAdmin: false,
has: matches,
hasAny: (...slugs: string[]) => slugs.some(matches),
hasAll: (...slugs: string[]) => slugs.every(matches),
}),
correlationId: createCorrelationId(),
};
}
export async function saveMaintenance(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
// Checkbox: present only when ticked. Normalise to the '1'/'0' string the
// emulator/Laravel side expects.
@@ -56,10 +50,15 @@ export async function saveMaintenance(formData: FormData): Promise<void> {
const minRank =
Number.isFinite(parsedRank) && parsedRank >= 0 ? parsedRank : 5;
await upsertSetting(KEY_ENABLED, enabled);
await upsertSetting(KEY_MESSAGE, message);
await upsertSetting(KEY_MIN_RANK, String(minRank));
siteSettings.reload();
const result = await systemMutationService.execute(
mutationContext(staff),
"operations.maintenance.update",
{
enabled: enabled === "1",
message,
minimumLoginRank: minRank,
},
);
if (!result.ok) throw new Error(result.error.messageKey);
revalidatePath("/admin/maintenance");
}
+73 -42
View File
@@ -1,20 +1,23 @@
"use server";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config";
import {
type SystemMutationContext,
type SystemMutationOperation,
systemMutationService,
} from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import { createCorrelationId } from "@/features/housekeeping/foundation/correlation";
import { requirePermissionRateLimited } from "@/lib/admin/guard";
import { db, WebsiteSetting } from "@/lib/db";
import { actionOk, adminAction } from "@/lib/foundation/action";
import type { AdminActionContext } from "@/lib/foundation/types";
import {
HABBO_GAMEDATA_HOTEL_SETTING_KEY,
normalizeHabboGamedataHotel,
} from "@/lib/habbo-gamedata-hotel";
import { PERMS } from "@/lib/permissions";
import { clearOfficialHabboFurnidataCache } 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);
@@ -25,11 +28,47 @@ function normalizeSettingValue(key: string, value: string): string {
return value;
}
function bustGamedataCachesIfNeeded(key: string): void {
if (key === HABBO_GAMEDATA_HOTEL_SETTING_KEY) {
clearOfficialHabboFurnidataCache();
clearBadgeCache();
}
function actionMutationContext(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
): SystemMutationContext {
return {
capability: createHousekeepingCapabilityContext(
{
id: Number(ctx.session.user.id),
rank: Number(ctx.session.user.rank),
username: ctx.session.user.username,
},
ctx.permissions,
),
correlationId: String(ctx.requestId),
};
}
function checkedMutationContext(staff: {
id: number;
rank: number;
username: string;
}): SystemMutationContext {
const matches = (slug: string) => slug === PERMS.SETTINGS_EDIT;
return {
capability: createHousekeepingCapabilityContext(staff, {
isSuperAdmin: false,
has: matches,
hasAny: (...slugs) => slugs.some(matches),
hasAll: (...slugs) => slugs.every(matches),
}),
correlationId: createCorrelationId(),
};
}
async function runMutation(
context: SystemMutationContext,
operation: SystemMutationOperation,
input: unknown,
): Promise<unknown> {
const result = await systemMutationService.execute(context, operation, input);
if (!result.ok) throw new Error(result.error.messageKey);
return result.data;
}
const saveManagedSchema = z.object({
@@ -47,27 +86,19 @@ export const saveManagedSettings = adminAction(
const entries = Object.entries(ctx.data.settings)
.filter(([key]) => managedKeySet.has(key))
.map(([key, value]) => [key, normalizeSettingValue(key, value)] as const);
await Promise.all(
entries.map(([key, value]) =>
db
.insert(WebsiteSetting)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } }),
),
);
await siteSettings.reload();
if (entries.some(([key]) => key === HABBO_GAMEDATA_HOTEL_SETTING_KEY)) {
clearOfficialHabboFurnidataCache();
clearBadgeCache();
}
const mutation = (await runMutation(
actionMutationContext(ctx),
"configuration.settings.save",
{ settings: Object.fromEntries(entries) },
)) as { saved: number };
revalidatePath("/admin/settings");
revalidatePath("/admin/catalog");
return actionOk({ saved: entries.length });
return actionOk({ saved: mutation.saved });
},
);
export async function updateSetting(formData: FormData): Promise<void> {
await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const key = String(formData.get("key") ?? "")
.normalize("NFC")
.trim();
@@ -76,17 +107,16 @@ export async function updateSetting(formData: FormData): Promise<void> {
String(formData.get("value") ?? "").normalize("NFC"),
);
if (!key) return;
await db
.insert(WebsiteSetting)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } });
await siteSettings.reload();
bustGamedataCachesIfNeeded(key);
await runMutation(
checkedMutationContext(staff),
"configuration.setting.update",
{ key, value },
);
revalidatePath("/admin/settings");
}
export async function createSetting(formData: FormData): Promise<void> {
await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const key = String(formData.get("key") ?? "")
.normalize("NFC")
.trim()
@@ -100,23 +130,24 @@ export async function createSetting(formData: FormData): Promise<void> {
.trim()
.slice(0, 255);
if (!key) return;
await db
.insert(WebsiteSetting)
.values({ key, value, comment: comment || null })
.onDuplicateKeyUpdate({ set: { value } });
await siteSettings.reload();
bustGamedataCachesIfNeeded(key);
await runMutation(
checkedMutationContext(staff),
"configuration.setting.create",
{ key, value, comment },
);
revalidatePath("/admin/settings");
}
export async function deleteSetting(formData: FormData): Promise<void> {
await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const staff = await requirePermissionRateLimited(PERMS.SETTINGS_EDIT);
const key = String(formData.get("key") ?? "")
.normalize("NFC")
.trim();
if (!key) return;
await db.delete(WebsiteSetting).where(eq(WebsiteSetting.key, key));
await siteSettings.reload();
bustGamedataCachesIfNeeded(key);
await runMutation(
checkedMutationContext(staff),
"configuration.setting.delete",
{ key },
);
revalidatePath("/admin/settings");
}
+114 -106
View File
@@ -1,48 +1,96 @@
"use server";
import { eq, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { db, User } from "@/lib/db";
import {
type SystemMutationContext,
type SystemMutationOperation,
systemMutationService,
} from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import type { AdminActionContext } from "@/lib/foundation/types";
import { PERMS } from "@/lib/permissions";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { rcon } from "@/lib/services/rcon";
const PATH = "/admin/commandocentrum";
const RCON_FAIL = "RCON command failed. Is the emulator running?";
async function requireRconOk(ok: boolean): Promise<void> {
if (!ok) throw new ActionError(RCON_FAIL);
function mutationContext(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
): SystemMutationContext {
return {
capability: createHousekeepingCapabilityContext(
{
id: Number(ctx.session.user.id),
rank: Number(ctx.session.user.rank),
username: ctx.session.user.username,
},
ctx.permissions,
),
correlationId: String(ctx.requestId),
};
}
async function runRconMutation(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
operation: SystemMutationOperation,
input: unknown,
): Promise<void> {
const result = await systemMutationService.execute(
mutationContext(ctx),
operation,
input,
);
if (result.ok) return;
if (result.error.messageKey === "errors.housekeeping.system.userNotFound") {
throw new ActionError("User not found");
}
if (result.error.messageKey === "errors.housekeeping.system.rankNotFound") {
throw new ActionError("Rank does not exist");
}
if (
result.error.messageKey ===
"errors.housekeeping.system.cannotChangePeerRank"
) {
throw new ActionError("Cannot change rank of a user at or above your rank");
}
if (
result.error.messageKey ===
"errors.housekeeping.system.cannotAssignPeerRank"
) {
throw new ActionError("Cannot set a rank equal to or above your own");
}
throw new ActionError(RCON_FAIL);
}
function revalidate(): void {
revalidatePath(PATH);
}
/** Rebuild the in-memory catalog on the emulator (rcon: updatecatalog). */
export const updateCatalog = adminAction(
{ permission: PERMS.RCON_EXECUTE },
async () => {
await requireRconOk(await rcon.updateCatalog());
revalidatePath(PATH);
async (ctx) => {
await runRconMutation(ctx, "rcon.update-catalog", {});
revalidate();
return actionOk();
},
);
/** Reload the chat word filter on the emulator (rcon: updatewordfilter). */
export const updateWordFilter = adminAction(
{ permission: PERMS.RCON_EXECUTE },
async () => {
await requireRconOk(await rcon.updateWordFilter());
revalidatePath(PATH);
async (ctx) => {
await runRconMutation(ctx, "rcon.update-word-filter", {});
revalidate();
return actionOk();
},
);
/** Reload navigator data on the emulator (rcon: updatenavigator, no payload). */
export const updateNavigator = adminAction(
{ permission: PERMS.RCON_EXECUTE },
async () => {
await requireRconOk(await rcon.send("updatenavigator", null));
revalidatePath(PATH);
async (ctx) => {
await runRconMutation(ctx, "rcon.update-navigator", {});
revalidate();
return actionOk();
},
);
@@ -51,13 +99,13 @@ const hotelAlertSchema = z.object({
message: z.string().trim().min(1).max(512),
});
/** Broadcast a hotel-wide alert to every connected user (rcon: hotelalert). */
export const hotelAlert = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: hotelAlertSchema },
async (ctx) => {
const message = ctx.data.message.normalize("NFC");
await requireRconOk(await rcon.send("hotelalert", { message }));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.hotel-alert", {
message: ctx.data.message.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -67,13 +115,14 @@ const disconnectSchema = z.object({
username: z.string().trim().min(1),
});
/** Disconnect/kick a user from the hotel (rcon: disconnect). */
export const disconnectUser = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: disconnectSchema },
async (ctx) => {
const username = ctx.data.username.normalize("NFC");
await requireRconOk(await rcon.disconnectUser(ctx.data.userId, username));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.disconnect-user", {
userId: ctx.data.userId,
username: ctx.data.username.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -83,13 +132,14 @@ const alertUserSchema = z.object({
message: z.string().trim().min(1).max(512),
});
/** Send an alert to a specific user (rcon: alertuser). */
export const alertUser = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: alertUserSchema },
async (ctx) => {
const message = ctx.data.message.normalize("NFC");
await requireRconOk(await rcon.alertUser(ctx.data.userId, message));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.alert-user", {
userId: ctx.data.userId,
message: ctx.data.message.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -99,14 +149,11 @@ const forwardUserSchema = z.object({
roomId: z.coerce.number().int().positive(),
});
/** Forward a user to a specific room (rcon: forwarduser). */
export const forwardUser = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: forwardUserSchema },
async (ctx) => {
await requireRconOk(
await rcon.forwardUser(ctx.data.userId, ctx.data.roomId),
);
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.forward-user", ctx.data);
revalidate();
return actionOk();
},
);
@@ -116,14 +163,14 @@ const giveCreditsSchema = z.object({
credits: z.coerce.number().int().positive(),
});
/** Give credits to a user (rcon: givecredits). */
export const giveCredits = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: giveCreditsSchema },
async (ctx) => {
await requireRconOk(
await rcon.giveCredits(ctx.data.userId, ctx.data.credits),
);
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.give-credits", {
userId: ctx.data.userId,
amount: ctx.data.credits,
});
revalidate();
return actionOk();
},
);
@@ -133,26 +180,20 @@ const giveAmountSchema = z.object({
amount: z.coerce.number().int().positive(),
});
/** Give duckets to a user (rcon: givepoints type=duckets). */
export const giveDuckets = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: giveAmountSchema },
async (ctx) => {
await requireRconOk(
await rcon.giveDuckets(ctx.data.userId, ctx.data.amount),
);
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.give-duckets", ctx.data);
revalidate();
return actionOk();
},
);
/** Give diamonds to a user (rcon: givepoints type=diamonds). */
export const giveDiamonds = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: giveAmountSchema },
async (ctx) => {
await requireRconOk(
await rcon.giveDiamonds(ctx.data.userId, ctx.data.amount),
);
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.give-diamonds", ctx.data);
revalidate();
return actionOk();
},
);
@@ -162,13 +203,14 @@ const giveBadgeSchema = z.object({
badge: z.string().trim().min(1).max(32),
});
/** Give a badge to a user (rcon: givebadge). */
export const giveBadge = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: giveBadgeSchema },
async (ctx) => {
const badge = ctx.data.badge.normalize("NFC");
await requireRconOk(await rcon.giveBadge(ctx.data.userId, badge));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.give-badge", {
userId: ctx.data.userId,
badge: ctx.data.badge.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -178,13 +220,14 @@ const setMottoSchema = z.object({
motto: z.string().trim().min(1).max(127),
});
/** Set a user's motto (rcon: setmotto). */
export const setMotto = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: setMottoSchema },
async (ctx) => {
const motto = ctx.data.motto.normalize("NFC");
await requireRconOk(await rcon.setMotto(ctx.data.userId, motto));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.set-motto", {
userId: ctx.data.userId,
motto: ctx.data.motto.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -194,47 +237,11 @@ const setRankSchema = z.object({
rank: z.coerce.number().int().min(1).max(9999),
});
/** Set a user's rank (rcon: setrank). */
export const setRank = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: setRankSchema },
async (ctx) => {
const staffRank = Number(ctx.session.user.rank);
const isSuper = ctx.permissions.isSuperAdmin;
const [target] = await db
.select({ rank: User.rank })
.from(User)
.where(eq(User.id, ctx.data.userId))
.limit(1);
if (!target) throw new ActionError("User not found");
let rankExists: { id: number }[] = [];
try {
const [rows] = await db.execute(
sql`SELECT id FROM permission_ranks WHERE id = ${ctx.data.rank} LIMIT 1`,
);
rankExists = rows as unknown as { id: number }[];
} catch {
rankExists = [];
}
if (rankExists.length === 0) throw new ActionError("Rank does not exist");
if (!isSuper) {
if (target.rank >= staffRank) {
throw new ActionError(
"Cannot change rank of a user at or above your rank",
);
}
if (ctx.data.rank >= staffRank) {
throw new ActionError("Cannot set a rank equal to or above your own");
}
}
await requireRconOk(await rcon.setRank(ctx.data.userId, ctx.data.rank));
await db
.update(User)
.set({ rank: ctx.data.rank })
.where(eq(User.id, ctx.data.userId));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.set-rank", ctx.data);
revalidate();
return actionOk();
},
);
@@ -244,13 +251,14 @@ const executeCommandSchema = z.object({
command: z.string().trim().min(1).max(100),
});
/** Execute a command as a user (rcon: executecommand). */
export const executeCommand = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: executeCommandSchema },
async (ctx) => {
const command = ctx.data.command.normalize("NFC");
await requireRconOk(await rcon.executeCommand(ctx.data.userId, command));
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.execute-command", {
userId: ctx.data.userId,
command: ctx.data.command.normalize("NFC"),
});
revalidate();
return actionOk();
},
);
@@ -261,15 +269,15 @@ const sendGiftSchema = z.object({
message: z.string().trim().max(255).optional().default("Here is a gift."),
});
/** Send a gift to a user (rcon: sendgift). */
export const sendGift = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: sendGiftSchema },
async (ctx) => {
const message = ctx.data.message.trim().slice(0, 255) || "Here is a gift.";
await requireRconOk(
await rcon.sendGift(ctx.data.userId, ctx.data.itemId, message),
);
revalidatePath(PATH);
await runRconMutation(ctx, "rcon.send-gift", {
userId: ctx.data.userId,
itemId: ctx.data.itemId,
message: ctx.data.message.trim().slice(0, 255) || "Here is a gift.",
});
revalidate();
return actionOk();
},
);
+59 -208
View File
@@ -1,27 +1,56 @@
"use server";
import { and, count, eq, inArray, sql } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { revalidateTag } from "next/cache";
import { z } from "zod";
import {
AclModelPermission,
AclModelRole,
AclPermission,
AclRole,
db,
User,
} from "@/lib/db";
type SystemMutationContext,
type SystemMutationOperation,
systemMutationService,
} from "@/features/housekeeping/domains/system/services/mutations";
import { createHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/capability-context";
import type { AdminActionContext } from "@/lib/foundation/types";
import { PERMS } from "@/lib/permission-slugs";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import {
createEmulatorRank,
deleteEmulatorRank,
updateEmulatorRank,
} from "@/lib/services/permission-ranks";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
function mutationContext(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
): SystemMutationContext {
return {
capability: createHousekeepingCapabilityContext(
{
id: Number(ctx.session.user.id),
rank: Number(ctx.session.user.rank),
username: ctx.session.user.username,
},
ctx.permissions,
),
correlationId: String(ctx.requestId),
};
}
async function runAccessMutation(
ctx: Pick<AdminActionContext, "session" | "permissions" | "requestId">,
operation: SystemMutationOperation,
input: unknown,
): Promise<unknown> {
const result = await systemMutationService.execute(
mutationContext(ctx),
operation,
input,
);
if (result.ok) return result.data;
if (result.error.messageKey === "errors.housekeeping.system.rankInUse") {
const users = Number(result.error.fieldErrors?.rank?.[0]);
if (Number.isInteger(users) && users > 0) {
throw new ActionError(`Cannot delete: ${users} users have this rank`);
}
}
if (result.error.messageKey === "errors.housekeeping.system.roleNotFound") {
throw new ActionError("Role not found");
}
throw new ActionError(result.error.messageKey);
}
const createRankSchema = z.object({
rank_name: z.string().trim().min(1).max(25),
@@ -31,25 +60,12 @@ const createRankSchema = z.object({
export const createRank = adminAction(
{ schema: createRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const id = await createEmulatorRank(db, ctx.data);
await db
.insert(AclRole)
.values({
slug: `rank_${id}`,
title: ctx.data.rank_name,
description: "CMS role synchronized from permission_ranks",
})
.onDuplicateKeyUpdate({ set: { title: ctx.data.rank_name } });
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_create",
description: `Created rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
const result = (await runAccessMutation(ctx, "access.rank.create", {
name: ctx.data.rank_name,
level: ctx.data.level,
})) as { id: number };
revalidateTag("permissions", { expire: 0 });
return actionOk({ id });
return actionOk({ id: result.id });
},
);
@@ -58,41 +74,7 @@ const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() });
export const deleteRank = adminAction(
{ schema: deleteRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [userCount] = await db
.select({ total: count() })
.from(User)
.where(eq(User.rank, ctx.data.id));
const users = userCount?.total ?? 0;
if (users > 0)
throw new ActionError(`Cannot delete: ${users} users have this rank`);
const [role] = await db
.select({ id: AclRole.id })
.from(AclRole)
.where(eq(AclRole.slug, `rank_${ctx.data.id}`))
.limit(1);
await deleteEmulatorRank(db, ctx.data.id);
if (role) {
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
await tx.delete(AclModelRole).where(eq(AclModelRole.roleId, role.id));
await tx.delete(AclRole).where(eq(AclRole.id, role.id));
});
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_delete",
description: `Deleted rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
await runAccessMutation(ctx, "access.rank.delete", ctx.data);
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
@@ -106,21 +88,7 @@ const saveRankSchema = z.object({
export const saveRank = adminAction(
{ schema: saveRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
await updateEmulatorRank(db, ctx.data.id, ctx.data.fields);
if (typeof ctx.data.fields.rank_name === "string") {
await db
.update(AclRole)
.set({ title: ctx.data.fields.rank_name })
.where(eq(AclRole.slug, `rank_${ctx.data.id}`));
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_update",
description: `Updated rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
await runAccessMutation(ctx, "access.rank.update", ctx.data);
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
@@ -134,138 +102,21 @@ const setCmsPermsSchema = z.object({
export const setCmsPermissions = adminAction(
{ schema: setCmsPermsSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [role] = await db
.select({ id: AclRole.id, slug: AclRole.slug })
.from(AclRole)
.where(eq(AclRole.id, ctx.data.roleId))
.limit(1);
if (!role) throw new ActionError("Role not found");
const permissions = await db
.select({ id: AclPermission.id })
.from(AclPermission)
.where(inArray(AclPermission.slug, ctx.data.permissionSlugs));
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
if (permissions.length) {
await tx.insert(AclModelPermission).values(
permissions.map((permission) => ({
modelId: role.id,
modelType: "Role",
permissionId: permission.id,
})),
);
}
});
await logStaffActivity({
staffId: ctx.session.user.id,
action: "acl_role_permissions_update",
description: `Updated ${permissions.length} permissions for ${role.slug}`,
targetType: "acl_role",
targetId: role.id,
});
await runAccessMutation(ctx, "access.permissions.update", ctx.data);
revalidateTag("permissions", { expire: 0 });
return actionOk();
},
);
/**
* Re-apply the same grant repair as migration 0018:
* - ranks with admin.dashboard get all admin.*
* - ranks >= 6 get admin.*.view + dashboard
* - ranks >= 7 get edit/manage/execute tools used by the sidebar
*/
export const repairAdminNavAclGrants = adminAction(
{ permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const [dashboardFillResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`acl_roles\` ar
JOIN \`acl_permissions\` ap ON ap.slug LIKE 'admin.%'
WHERE EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
JOIN \`acl_permissions\` apdash ON apdash.id = amp.permission_id
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND apdash.slug = 'admin.dashboard'
)
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp2
WHERE amp2.model_type = 'Role'
AND amp2.model_id = ar.id
AND amp2.permission_id = ap.id
)
`);
const [midRankViewsResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`permission_ranks\` pr
JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN \`acl_permissions\` ap ON (
ap.slug = 'admin.dashboard'
OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view')
)
WHERE pr.id >= 6
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const [highRankToolsResult] = await db.execute(sql`
INSERT INTO \`acl_model_permissions\` (\`model_type\`, \`model_id\`, \`permission_id\`)
SELECT 'Role', ar.id, ap.id
FROM \`permission_ranks\` pr
JOIN \`acl_roles\` ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN \`acl_permissions\` ap ON (
(ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit')
OR ap.slug IN (
'admin.permissions.manage',
'admin.rcon.execute',
'admin.assets.import',
'admin.export',
'admin.analytics.export',
'admin.users.ban',
'admin.users.reset_password',
'admin.room.delete'
)
)
WHERE pr.id >= 7
AND NOT EXISTS (
SELECT 1
FROM \`acl_model_permissions\` amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const inserted =
Number((dashboardFillResult as ResultSetHeader).affectedRows) +
Number((midRankViewsResult as ResultSetHeader).affectedRows) +
Number((highRankToolsResult as ResultSetHeader).affectedRows);
await logStaffActivity({
staffId: ctx.session.user.id,
action: "acl_nav_grants_repair",
description: `Repaired admin nav ACL grants (${inserted} rows inserted)`,
targetType: "acl",
targetId: 0,
});
const result = (await runAccessMutation(
ctx,
"access.permissions.repair",
{},
)) as { inserted: number };
revalidateTag("permissions", { expire: 0 });
return actionOk({ inserted });
return actionOk({ inserted: result.inserted });
},
);
@@ -0,0 +1,228 @@
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import { confirmHousekeepingCommand } from "../../../foundation/commands/confirmation";
import type { HousekeepingCommand } from "../../../foundation/commands/registry";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
import {
createSystemMutationService,
type SystemMutationAdapter,
} from "../services/mutations";
import {
createSystemCommands,
SYSTEM_COMMAND_IDS,
SYSTEM_COMMANDS,
} from "./system-commands";
const expectedCommandIds = [
"system.access.rank.create",
"system.access.rank.delete",
"system.access.rank.update",
"system.access.permissions.update",
"system.access.permissions.repair",
"system.configuration.settings.save",
"system.configuration.setting.create",
"system.configuration.setting.update",
"system.configuration.setting.delete",
"system.configuration.emulator-setting.update",
"system.configuration.emulator-text.update",
"system.operations.alerts.mark-read",
"system.operations.alert.broadcast",
"system.operations.rcon.update-catalog",
"system.operations.rcon.update-word-filter",
"system.operations.rcon.update-navigator",
"system.operations.rcon.hotel-alert",
"system.operations.rcon.disconnect-user",
"system.operations.rcon.alert-user",
"system.operations.rcon.forward-user",
"system.operations.rcon.give-credits",
"system.operations.rcon.give-duckets",
"system.operations.rcon.give-diamonds",
"system.operations.rcon.give-badge",
"system.operations.rcon.set-motto",
"system.operations.rcon.set-rank",
"system.operations.rcon.execute-command",
"system.operations.rcon.send-gift",
"system.operations.maintenance.update",
] as const;
const reasonRequiredIds = expectedCommandIds.filter(
(id) =>
id.startsWith("system.access.") ||
id.startsWith("system.configuration.setting") ||
id === "system.configuration.settings.save" ||
id === "system.operations.alert.broadcast" ||
id.startsWith("system.operations.rcon.") ||
id === "system.operations.maintenance.update",
);
const iterableSystemCommands =
SYSTEM_COMMANDS as unknown as readonly HousekeepingCommand<
unknown,
unknown
>[];
function capabilityContext(
granted: readonly string[],
): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 500 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
describe("SYSTEM_COMMANDS", () => {
it("declares the complete deterministic command list", () => {
expect(SYSTEM_COMMAND_IDS).toEqual(expectedCommandIds);
expect(iterableSystemCommands.map((command) => command.id)).toEqual(
expectedCommandIds,
);
expect(
iterableSystemCommands.every((command) => command.risk === "sensitive"),
).toBe(true);
});
it("requires confirmation reasons for access, global settings, RCON, and maintenance", () => {
expect(
iterableSystemCommands
.filter((command) => command.requiresReason)
.map((command) => command.id),
).toEqual(reasonRequiredIds);
for (const command of iterableSystemCommands.filter(
(command) => command.requiresReason,
)) {
expect(
confirmHousekeepingCommand(command, " ", "correlation-test"),
).toMatchObject({
ok: false,
error: {
code: "VALIDATION",
fieldErrors: { reason: ["errors.validation.required"] },
},
});
}
});
it("uses the authoritative mutation capability per command family", () => {
const byId = new Map(
iterableSystemCommands.map((command) => [command.id, command]),
);
expect(byId.get("system.access.rank.create")?.capability.slugs).toEqual([
PERMS.PERMISSIONS_MANAGE,
]);
expect(
byId.get("system.configuration.setting.update")?.capability.slugs,
).toEqual([PERMS.SETTINGS_EDIT]);
expect(
byId.get("system.operations.alerts.mark-read")?.capability.slugs,
).toEqual([PERMS.NOTIFICATIONS_VIEW]);
expect(
byId.get("system.operations.alert.broadcast")?.capability.slugs,
).toEqual([PERMS.NOTIFICATIONS_EDIT]);
expect(
byId.get("system.operations.rcon.update-catalog")?.capability.slugs,
).toEqual([PERMS.RCON_EXECUTE]);
});
it.each([
["system.access.rank.create", { name: " ", level: 3 }],
["system.operations.alert.broadcast", { message: " " }],
["system.operations.rcon.disconnect-user", { userId: 7, username: " " }],
["system.operations.rcon.give-badge", { userId: 7, badge: " " }],
["system.operations.rcon.set-motto", { userId: 7, motto: " " }],
["system.operations.rcon.execute-command", { userId: 7, command: " " }],
] as const)("rejects whitespace-only text for %s", (commandId, input) => {
const command = iterableSystemCommands.find(
(entry) => entry.id === commandId,
);
if (!command) throw new Error(`command missing: ${commandId}`);
expect(command.input.safeParse(input).success).toBe(false);
});
it("delegates parsed command input to the shared mutation service", async () => {
const service = {
execute: vi.fn(async (context, operation, input) => ({
ok: true as const,
data: { context, operation, input },
correlationId: context.correlationId,
})),
};
const commands = createSystemCommands(
service,
) as unknown as readonly HousekeepingCommand<unknown, unknown>[];
const command = commands.find(
(entry) => entry.id === "system.operations.rcon.give-credits",
);
if (!command) throw new Error("command missing");
const parsed = command.input.parse({ userId: 7, amount: 25 });
const result = await command.execute(
{
capability: capabilityContext([PERMS.RCON_EXECUTE]),
correlationId: "command-correlation",
ipAddress: "198.51.100.8",
},
parsed,
);
expect(result).toMatchObject({
ok: true,
data: {
operation: "rcon.give-credits",
input: { userId: 7, amount: 25 },
},
correlationId: "command-correlation",
});
});
});
describe("System mutation service boundary", () => {
it("returns FORBIDDEN without invoking the adapter when permission is absent", async () => {
const execute = vi.fn(async () => ({ saved: true }));
const service = createSystemMutationService({ execute });
const result = await service.execute(
{
capability: capabilityContext([]),
correlationId: "denied-correlation",
},
"configuration.setting.update",
{ key: "hotel_name", value: "Hotel" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "FORBIDDEN" },
correlationId: "denied-correlation",
});
expect(execute).not.toHaveBeenCalled();
});
it("maps adapter outages to DEPENDENCY_UNAVAILABLE and preserves correlation", async () => {
const adapter: SystemMutationAdapter = {
execute: async () => {
throw new Error("database unavailable");
},
};
const service = createSystemMutationService(adapter);
const result = await service.execute(
{
capability: capabilityContext([PERMS.SETTINGS_EDIT]),
correlationId: "failure-correlation",
},
"configuration.setting.update",
{ key: "hotel_name", value: "Hotel" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
correlationId: "failure-correlation",
});
});
});
@@ -0,0 +1,315 @@
import "server-only";
import { z } from "zod";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCommand } from "../../../foundation/commands/registry";
import { anyCapability } from "../../../foundation/contracts";
import {
type SystemMutationOperation,
type SystemMutationService,
systemMutationService,
} from "../services/mutations";
export const SYSTEM_COMMAND_IDS = [
"system.access.rank.create",
"system.access.rank.delete",
"system.access.rank.update",
"system.access.permissions.update",
"system.access.permissions.repair",
"system.configuration.settings.save",
"system.configuration.setting.create",
"system.configuration.setting.update",
"system.configuration.setting.delete",
"system.configuration.emulator-setting.update",
"system.configuration.emulator-text.update",
"system.operations.alerts.mark-read",
"system.operations.alert.broadcast",
"system.operations.rcon.update-catalog",
"system.operations.rcon.update-word-filter",
"system.operations.rcon.update-navigator",
"system.operations.rcon.hotel-alert",
"system.operations.rcon.disconnect-user",
"system.operations.rcon.alert-user",
"system.operations.rcon.forward-user",
"system.operations.rcon.give-credits",
"system.operations.rcon.give-duckets",
"system.operations.rcon.give-diamonds",
"system.operations.rcon.give-badge",
"system.operations.rcon.set-motto",
"system.operations.rcon.set-rank",
"system.operations.rcon.execute-command",
"system.operations.rcon.send-gift",
"system.operations.maintenance.update",
] as const;
interface CommandOptions<I> {
readonly id: (typeof SYSTEM_COMMAND_IDS)[number];
readonly operation: SystemMutationOperation;
readonly capability: string;
readonly input: z.ZodType<I>;
readonly requiresReason: boolean;
readonly attempts?: number;
}
function systemCommand<I>(
service: Pick<SystemMutationService, "execute">,
options: CommandOptions<I>,
): HousekeepingCommand<I, unknown> {
return {
id: options.id,
owner: "system",
risk: "sensitive",
capability: anyCapability(options.capability),
input: options.input,
requiresReason: options.requiresReason,
rateLimit: { attempts: options.attempts ?? 10, windowMs: 60_000 },
execute: (context, input) =>
service.execute(
{
capability: context.capability,
correlationId: context.correlationId,
},
options.operation,
input,
),
};
}
const empty = z.object({});
const positiveId = z.number().int().positive();
const requiredText = (max: number) => z.string().min(1).max(max).regex(/\S/u);
const settingInput = z.object({
key: requiredText(255),
value: z.string().max(65_535),
});
export function createSystemCommands(
service: Pick<SystemMutationService, "execute">,
) {
return [
systemCommand(service, {
id: "system.access.rank.create",
operation: "access.rank.create",
capability: PERMS.PERMISSIONS_MANAGE,
input: z.object({ name: requiredText(25), level: positiveId }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.access.rank.delete",
operation: "access.rank.delete",
capability: PERMS.PERMISSIONS_MANAGE,
input: z.object({ id: positiveId }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.access.rank.update",
operation: "access.rank.update",
capability: PERMS.PERMISSIONS_MANAGE,
input: z.object({
id: positiveId,
fields: z.record(z.string(), z.union([z.string(), z.number()])),
}),
requiresReason: true,
}),
systemCommand(service, {
id: "system.access.permissions.update",
operation: "access.permissions.update",
capability: PERMS.PERMISSIONS_MANAGE,
input: z.object({
roleId: positiveId,
permissionSlugs: z.array(requiredText(160)).max(500),
}),
requiresReason: true,
}),
systemCommand(service, {
id: "system.access.permissions.repair",
operation: "access.permissions.repair",
capability: PERMS.PERMISSIONS_MANAGE,
input: empty,
requiresReason: true,
}),
systemCommand(service, {
id: "system.configuration.settings.save",
operation: "configuration.settings.save",
capability: PERMS.SETTINGS_EDIT,
input: z.object({ settings: z.record(z.string(), z.string()) }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.configuration.setting.create",
operation: "configuration.setting.create",
capability: PERMS.SETTINGS_EDIT,
input: settingInput.extend({ comment: z.string().max(255).optional() }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.configuration.setting.update",
operation: "configuration.setting.update",
capability: PERMS.SETTINGS_EDIT,
input: settingInput,
requiresReason: true,
}),
systemCommand(service, {
id: "system.configuration.setting.delete",
operation: "configuration.setting.delete",
capability: PERMS.SETTINGS_EDIT,
input: z.object({ key: requiredText(255) }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.configuration.emulator-setting.update",
operation: "configuration.emulator-setting.update",
capability: PERMS.SETTINGS_EDIT,
input: z.object({ key: requiredText(100), value: z.string().max(512) }),
requiresReason: false,
}),
systemCommand(service, {
id: "system.configuration.emulator-text.update",
operation: "configuration.emulator-text.update",
capability: PERMS.SETTINGS_EDIT,
input: z.object({ key: requiredText(100), value: z.string().max(4096) }),
requiresReason: false,
}),
systemCommand(service, {
id: "system.operations.alerts.mark-read",
operation: "operations.alerts.mark-read",
capability: PERMS.NOTIFICATIONS_VIEW,
input: empty,
requiresReason: false,
}),
systemCommand(service, {
id: "system.operations.alert.broadcast",
operation: "operations.alert.broadcast",
capability: PERMS.NOTIFICATIONS_EDIT,
input: z.object({ message: requiredText(1000) }),
requiresReason: true,
}),
systemCommand(service, {
id: "system.operations.rcon.update-catalog",
operation: "rcon.update-catalog",
capability: PERMS.RCON_EXECUTE,
input: empty,
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.update-word-filter",
operation: "rcon.update-word-filter",
capability: PERMS.RCON_EXECUTE,
input: empty,
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.update-navigator",
operation: "rcon.update-navigator",
capability: PERMS.RCON_EXECUTE,
input: empty,
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.hotel-alert",
operation: "rcon.hotel-alert",
capability: PERMS.RCON_EXECUTE,
input: z.object({ message: requiredText(512) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.disconnect-user",
operation: "rcon.disconnect-user",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, username: requiredText(255) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.alert-user",
operation: "rcon.alert-user",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, message: requiredText(512) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.forward-user",
operation: "rcon.forward-user",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, roomId: positiveId }),
requiresReason: true,
attempts: 5,
}),
...[
["give-credits", "rcon.give-credits"],
["give-duckets", "rcon.give-duckets"],
["give-diamonds", "rcon.give-diamonds"],
].map(([suffix, operation]) =>
systemCommand(service, {
id: `system.operations.rcon.${suffix}` as (typeof SYSTEM_COMMAND_IDS)[number],
operation: operation as SystemMutationOperation,
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, amount: positiveId }),
requiresReason: true,
attempts: 5,
}),
),
systemCommand(service, {
id: "system.operations.rcon.give-badge",
operation: "rcon.give-badge",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, badge: requiredText(32) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.set-motto",
operation: "rcon.set-motto",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, motto: requiredText(127) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.set-rank",
operation: "rcon.set-rank",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, rank: positiveId.max(9999) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.execute-command",
operation: "rcon.execute-command",
capability: PERMS.RCON_EXECUTE,
input: z.object({ userId: positiveId, command: requiredText(100) }),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.rcon.send-gift",
operation: "rcon.send-gift",
capability: PERMS.RCON_EXECUTE,
input: z.object({
userId: positiveId,
itemId: positiveId,
message: z.string().max(255).optional(),
}),
requiresReason: true,
attempts: 5,
}),
systemCommand(service, {
id: "system.operations.maintenance.update",
operation: "operations.maintenance.update",
capability: PERMS.SETTINGS_EDIT,
input: z.object({
enabled: z.boolean(),
message: z.string().max(65_535),
minimumLoginRank: z.number().int().min(0),
}),
requiresReason: true,
}),
] as const;
}
export const SYSTEM_COMMANDS = createSystemCommands(systemMutationService);
@@ -3,6 +3,7 @@ import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
import { SYSTEM_ROUTES } from "./routes";
export const systemManifest = {
id: "system",
@@ -21,7 +22,7 @@ export const systemManifest = {
PERMS.SETTINGS_EDIT,
PERMS.NOTIFICATIONS_EDIT,
),
routes: [],
routes: SYSTEM_ROUTES,
searchProviders: [],
inboxSources: [],
widgets: [],
@@ -0,0 +1,193 @@
import type { ReactNode } from "react";
import {
createCorrelationId,
fail,
type HousekeepingResult,
} from "../../../foundation/contracts";
import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell";
import { HousekeepingPageState } from "../../../foundation/page/housekeeping-page-state";
import type { HousekeepingPageInput } from "../../../route-handlers";
import {
type SystemAccessQueryData,
type SystemAccessQueryInput,
systemAccessQuery,
} from "../queries/access";
interface SystemAccessPageProps {
readonly result?: HousekeepingResult<SystemAccessQueryData>;
}
function state(
kind: "loading" | "empty" | "error",
title: string,
description: string,
) {
return (
<div data-housekeeping-state={kind}>
<HousekeepingPageState
state={kind}
title={title}
description={description}
/>
</div>
);
}
function forbidden() {
return (
<section
role="alert"
data-housekeeping-state="forbidden"
className="rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-4"
>
<h2 className="font-medium text-[var(--admin-text)]">Access denied</h2>
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Your account cannot manage permissions.
</p>
</section>
);
}
function accessContent(data: SystemAccessQueryData) {
if (data.kind === "permission-detail") {
return (
<div className="grid gap-4 lg:grid-cols-2">
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">
{data.rank.name}
</h2>
<dl className="mt-3 grid grid-cols-2 gap-2 text-sm text-[var(--admin-text-muted)]">
<dt>Rank ID</dt>
<dd>{data.rank.id}</dd>
<dt>Level</dt>
<dd>{data.rank.level}</dd>
<dt>Users</dt>
<dd>{data.rank.userCount}</dd>
</dl>
</section>
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">
CMS permissions
</h2>
<ul className="mt-3 space-y-1 text-sm text-[var(--admin-text-muted)]">
{data.rank.cmsRole?.permissionSlugs.map((slug) => (
<li key={slug}>{slug}</li>
)) ?? <li>No CMS role is linked.</li>}
</ul>
</section>
</div>
);
}
return (
<div className="grid gap-4 lg:grid-cols-[2fr_1fr]">
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">
Permission ranks
</h2>
<ul className="mt-3 divide-y divide-[var(--admin-border)]">
{data.ranks.map((rank) => (
<li
key={rank.id}
className="flex items-center justify-between py-2 text-sm"
>
<a
href={`/ase/system/access/permissions/${rank.id}`}
className="font-medium text-[var(--admin-text)]"
>
{rank.name}
</a>
<span className="text-[var(--admin-text-muted)]">
{rank.userCount} users
</span>
</li>
))}
</ul>
</section>
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">ACL summary</h2>
<p className="mt-3 text-sm text-[var(--admin-text-muted)]">
{data.acl.roles} roles and {data.acl.permissions} permissions
</p>
</section>
</div>
);
}
export function SystemAccessPage({ result }: SystemAccessPageProps) {
let body: ReactNode;
if (!result) {
body = state(
"loading",
"Loading access data",
"Reading ranks and ACL assignments.",
);
} else if (!result.ok) {
body =
result.error.code === "FORBIDDEN"
? forbidden()
: state(
"error",
"Access data unavailable",
`Request ${result.correlationId} could not be completed.`,
);
} else {
const data = result.data;
const empty =
data.kind === "permissions" &&
data.ranks.length === 0 &&
data.acl.roles === 0 &&
data.acl.permissions === 0;
if (empty) {
body = state(
"empty",
"No access data",
"No ranks or ACL assignments were returned.",
);
} else {
body = (
<div
data-housekeeping-state={
data.partialDependencies.length > 0 ? "partial" : "ready"
}
className="space-y-4"
>
{data.partialDependencies.length > 0 ? (
<HousekeepingPageState
state="partial"
partialLabel="Partial data"
title="Some access data is unavailable"
description={`Unavailable: ${data.partialDependencies.join(", ")}`}
/>
) : null}
{accessContent(data)}
</div>
);
}
}
return (
<HousekeepingPageShell
title="Access control"
description="Review emulator ranks and CMS ACL assignments."
>
{body}
</HousekeepingPageShell>
);
}
export async function renderSystemAccessPage(input: HousekeepingPageInput) {
const queryInput: SystemAccessQueryInput | null =
input.match.routeId === "system.access.permissions"
? { routeId: "system.access.permissions" }
: input.match.routeId === "system.access.permission-detail"
? {
routeId: "system.access.permission-detail",
rankId: input.match.params.id ?? "",
}
: null;
const result = queryInput
? await systemAccessQuery.run(input.context, queryInput)
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
return <SystemAccessPage result={result} />;
}
@@ -0,0 +1,165 @@
import type { ReactNode } from "react";
import {
createCorrelationId,
fail,
type HousekeepingResult,
} from "../../../foundation/contracts";
import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell";
import { HousekeepingPageState } from "../../../foundation/page/housekeeping-page-state";
import type { HousekeepingPageInput } from "../../../route-handlers";
import {
type SystemConfigurationQueryData,
type SystemConfigurationQueryInput,
type SystemSettingRow,
systemConfigurationQuery,
} from "../queries/configuration";
interface SystemConfigurationPageProps {
readonly result?: HousekeepingResult<SystemConfigurationQueryData>;
}
function pageState(
kind: "loading" | "empty" | "error",
title: string,
description: string,
) {
return (
<div data-housekeeping-state={kind}>
<HousekeepingPageState
state={kind}
title={title}
description={description}
/>
</div>
);
}
function forbiddenState() {
return (
<section
role="alert"
data-housekeeping-state="forbidden"
className="rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-4"
>
<h2 className="font-medium text-[var(--admin-text)]">Access denied</h2>
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Your account cannot view system configuration.
</p>
</section>
);
}
function settingList(title: string, rows: readonly SystemSettingRow[]) {
return (
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">{title}</h2>
<dl className="mt-3 divide-y divide-[var(--admin-border)]">
{rows.map((row) => (
<div
key={row.key}
className="grid gap-1 py-2 text-sm md:grid-cols-[minmax(12rem,1fr)_2fr]"
>
<dt className="font-medium text-[var(--admin-text)]">{row.key}</dt>
<dd className="break-words text-[var(--admin-text-muted)]">
{row.value}
</dd>
</div>
))}
</dl>
</section>
);
}
function configurationContent(data: SystemConfigurationQueryData) {
return data.kind === "settings" ? (
settingList("Website settings", data.settings)
) : (
<div className="grid gap-4 xl:grid-cols-2">
{settingList("Emulator settings", data.settings)}
<div className="space-y-2">
{settingList("Emulator texts", data.texts)}
<p className="text-xs text-[var(--admin-text-muted)]">
Showing {data.texts.length} of {data.textsTotal} text entries.
</p>
</div>
</div>
);
}
export function SystemConfigurationPage({
result,
}: SystemConfigurationPageProps) {
let body: ReactNode;
if (!result) {
body = pageState(
"loading",
"Loading configuration",
"Reading CMS and emulator settings.",
);
} else if (!result.ok) {
body =
result.error.code === "FORBIDDEN"
? forbiddenState()
: pageState(
"error",
"Configuration unavailable",
`Request ${result.correlationId} could not be completed.`,
);
} else {
const data = result.data;
const empty =
data.kind === "settings"
? data.settings.length === 0
: data.settings.length === 0 && data.texts.length === 0;
if (empty) {
body = pageState(
"empty",
"No configuration entries",
"The selected configuration source returned no entries.",
);
} else {
body = (
<div
data-housekeeping-state={
data.partialDependencies.length > 0 ? "partial" : "ready"
}
className="space-y-4"
>
{data.partialDependencies.length > 0 ? (
<HousekeepingPageState
state="partial"
partialLabel="Partial data"
title="Some configuration data is unavailable"
description={`Unavailable: ${data.partialDependencies.join(", ")}`}
/>
) : null}
{configurationContent(data)}
</div>
);
}
}
return (
<HousekeepingPageShell
title="System configuration"
description="Review CMS and emulator configuration sources."
>
{body}
</HousekeepingPageShell>
);
}
export async function renderSystemConfigurationPage(
input: HousekeepingPageInput,
) {
const queryInput: SystemConfigurationQueryInput | null =
input.match.routeId === "system.configuration.settings"
? { routeId: "system.configuration.settings" }
: input.match.routeId === "system.configuration.emulator"
? { routeId: "system.configuration.emulator" }
: null;
const result = queryInput
? await systemConfigurationQuery.run(input.context, queryInput)
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
return <SystemConfigurationPage result={result} />;
}
@@ -0,0 +1,212 @@
import type { ReactNode } from "react";
import {
createCorrelationId,
fail,
type HousekeepingResult,
} from "../../../foundation/contracts";
import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell";
import { HousekeepingPageState } from "../../../foundation/page/housekeeping-page-state";
import type { HousekeepingPageInput } from "../../../route-handlers";
import {
type SystemObservabilityQueryData,
type SystemObservabilityQueryInput,
systemObservabilityQuery,
} from "../queries/observability";
interface SystemObservabilityPageProps {
readonly result?: HousekeepingResult<SystemObservabilityQueryData>;
}
function pageState(
kind: "loading" | "empty" | "error",
title: string,
description: string,
) {
return (
<div data-housekeeping-state={kind}>
<HousekeepingPageState
state={kind}
title={title}
description={description}
/>
</div>
);
}
function forbiddenState() {
return (
<section
role="alert"
data-housekeeping-state="forbidden"
className="rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-4"
>
<h2 className="font-medium text-[var(--admin-text)]">Access denied</h2>
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Your account cannot view this observability source.
</p>
</section>
);
}
function observabilityContent(data: SystemObservabilityQueryData) {
if (data.kind === "devops") {
return (
<div className="grid gap-4 lg:grid-cols-2">
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">
Service health
</h2>
{data.health ? (
<dl className="mt-3 grid grid-cols-2 gap-2 text-sm text-[var(--admin-text-muted)]">
<dt>Database</dt>
<dd>{data.health.dbOk ? "Available" : "Unavailable"}</dd>
<dt>Redis</dt>
<dd>
{data.health.redisOk === null
? "Not configured"
: data.health.redisOk
? "Available"
: "Unavailable"}
</dd>
<dt>Emulator</dt>
<dd>{data.health.emulatorOk ? "Available" : "Unavailable"}</dd>
<dt>Online users</dt>
<dd>{data.health.onlineUsers}</dd>
</dl>
) : (
<p className="mt-3 text-sm text-[var(--admin-text-muted)]">
Health data is unavailable.
</p>
)}
</section>
{observationRows("Recent emulator errors", data.errors)}
</div>
);
}
if (data.kind === "devops-errors") {
return observationRows("Emulator errors", data.errors);
}
return (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{data.metrics.map((metric) => (
<section
key={metric.label}
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
>
<p className="text-xs text-[var(--admin-text-muted)]">
{metric.label}
</p>
<p className="mt-1 text-xl font-semibold text-[var(--admin-text)]">
{metric.value}
</p>
</section>
))}
</div>
{observationRows(
data.kind === "logs" ? "Recent entries" : "Details",
data.rows,
)}
</div>
);
}
function observationRows(
title: string,
rows: readonly { id: string; primary: string; secondary?: string }[],
) {
return (
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">{title}</h2>
<ul className="mt-3 divide-y divide-[var(--admin-border)]">
{rows.map((row) => (
<li key={row.id} className="py-2 text-sm">
<p className="font-medium text-[var(--admin-text)]">
{row.primary}
</p>
{row.secondary ? (
<p className="break-words text-[var(--admin-text-muted)]">
{row.secondary}
</p>
) : null}
</li>
))}
</ul>
</section>
);
}
function isEmpty(data: SystemObservabilityQueryData): boolean {
if (data.kind === "devops")
return data.health === null && data.errors.length === 0;
if (data.kind === "devops-errors") return data.errors.length === 0;
return data.metrics.length === 0 && data.rows.length === 0;
}
export function SystemObservabilityPage({
result,
}: SystemObservabilityPageProps) {
let body: ReactNode;
if (!result) {
body = pageState(
"loading",
"Loading observability data",
"Reading metrics and operational logs.",
);
} else if (!result.ok) {
body =
result.error.code === "FORBIDDEN"
? forbiddenState()
: pageState(
"error",
"Observability data unavailable",
`Request ${result.correlationId} could not be completed.`,
);
} else if (isEmpty(result.data)) {
body = pageState(
"empty",
"No observations",
"The selected source returned no records.",
);
} else {
body = (
<div
data-housekeeping-state={
result.data.partialDependencies.length > 0 ? "partial" : "ready"
}
className="space-y-4"
>
{result.data.partialDependencies.length > 0 ? (
<HousekeepingPageState
state="partial"
partialLabel="Partial data"
title="Some observability data is unavailable"
description={`Unavailable: ${result.data.partialDependencies.join(", ")}`}
/>
) : null}
{observabilityContent(result.data)}
</div>
);
}
return (
<HousekeepingPageShell
title="System observability"
description="Review metrics, service health, and operational records."
>
{body}
</HousekeepingPageShell>
);
}
export async function renderSystemObservabilityPage(
input: HousekeepingPageInput,
) {
const routeId = input.match
.routeId as SystemObservabilityQueryInput["routeId"];
const supported = routeId.startsWith("system.observability.");
const result = supported
? await systemObservabilityQuery.run(input.context, { routeId })
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
return <SystemObservabilityPage result={result} />;
}
@@ -0,0 +1,182 @@
import type { ReactNode } from "react";
import {
createCorrelationId,
fail,
type HousekeepingResult,
} from "../../../foundation/contracts";
import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell";
import { HousekeepingPageState } from "../../../foundation/page/housekeeping-page-state";
import type { HousekeepingPageInput } from "../../../route-handlers";
import {
type SystemOperationsQueryData,
type SystemOperationsQueryInput,
systemOperationsQuery,
} from "../queries/operations";
interface SystemOperationsPageProps {
readonly result?: HousekeepingResult<SystemOperationsQueryData>;
}
function pageState(
kind: "loading" | "empty" | "error",
title: string,
description: string,
) {
return (
<div data-housekeeping-state={kind}>
<HousekeepingPageState
state={kind}
title={title}
description={description}
/>
</div>
);
}
function forbiddenState() {
return (
<section
role="alert"
data-housekeeping-state="forbidden"
className="rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-4"
>
<h2 className="font-medium text-[var(--admin-text)]">Access denied</h2>
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
Your account cannot use this system operation.
</p>
</section>
);
}
function maintenanceSummary(data: {
readonly enabled: boolean;
readonly message: string;
readonly minimumLoginRank: number;
}) {
return (
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">Maintenance</h2>
<dl className="mt-3 grid grid-cols-2 gap-2 text-sm text-[var(--admin-text-muted)]">
<dt>Status</dt>
<dd>{data.enabled ? "Enabled" : "Disabled"}</dd>
<dt>Minimum login rank</dt>
<dd>{data.minimumLoginRank}</dd>
<dt>Message</dt>
<dd>{data.message || "No message configured"}</dd>
</dl>
</section>
);
}
function operationsContent(data: SystemOperationsQueryData) {
if (data.kind === "alerts") {
return (
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">
Operational alerts
</h2>
<ul className="mt-3 divide-y divide-[var(--admin-border)]">
{data.alerts.map((alert) => (
<li key={alert.id} className="py-2 text-sm">
<p className="font-medium text-[var(--admin-text)]">
{alert.type} - {alert.severity}
</p>
<p className="text-[var(--admin-text-muted)]">{alert.message}</p>
</li>
))}
</ul>
</section>
);
}
if (data.kind === "maintenance") return maintenanceSummary(data.maintenance);
return (
<div className="grid gap-4 lg:grid-cols-3">
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">Command center</h2>
<p className="mt-3 text-sm text-[var(--admin-text-muted)]">
{data.onlineUsers.count} users online
</p>
<ul className="mt-2 space-y-1 text-sm text-[var(--admin-text-muted)]">
{data.onlineUsers.users.map((user) => (
<li key={user.id}>{user.username}</li>
))}
</ul>
</section>
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
<h2 className="font-medium text-[var(--admin-text)]">Service health</h2>
<p className="mt-3 text-sm text-[var(--admin-text-muted)]">
{data.health
? `Database ${data.health.dbOk ? "available" : "unavailable"}; emulator ${data.health.emulatorOk ? "available" : "unavailable"}.`
: "Health data is unavailable."}
</p>
</section>
{data.maintenance ? maintenanceSummary(data.maintenance) : null}
</div>
);
}
export function SystemOperationsPage({ result }: SystemOperationsPageProps) {
let body: ReactNode;
if (!result) {
body = pageState(
"loading",
"Loading system operations",
"Reading operational status and queues.",
);
} else if (!result.ok) {
body =
result.error.code === "FORBIDDEN"
? forbiddenState()
: pageState(
"error",
"System operations unavailable",
`Request ${result.correlationId} could not be completed.`,
);
} else if (result.data.kind === "alerts" && result.data.alerts.length === 0) {
body = pageState(
"empty",
"No operational alerts",
"There are no alerts to review.",
);
} else {
body = (
<div
data-housekeeping-state={
result.data.partialDependencies.length > 0 ? "partial" : "ready"
}
className="space-y-4"
>
{result.data.partialDependencies.length > 0 ? (
<HousekeepingPageState
state="partial"
partialLabel="Partial data"
title="Some operation data is unavailable"
description={`Unavailable: ${result.data.partialDependencies.join(", ")}`}
/>
) : null}
{operationsContent(result.data)}
</div>
);
}
return (
<HousekeepingPageShell
title="System operations"
description="Review alerts, command-center status, and maintenance availability."
>
{body}
</HousekeepingPageShell>
);
}
export async function renderSystemOperationsPage(input: HousekeepingPageInput) {
const routeId = input.match.routeId as SystemOperationsQueryInput["routeId"];
const supported =
routeId === "system.operations.alerts" ||
routeId === "system.operations.command-center" ||
routeId === "system.operations.maintenance";
const result = supported
? await systemOperationsQuery.run(input.context, { routeId })
: fail("NOT_FOUND", "errors.housekeeping.notFound", createCorrelationId());
return <SystemOperationsPage result={result} />;
}
@@ -0,0 +1,157 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import {
fail,
type HousekeepingResult,
ok,
} from "../../../foundation/contracts";
import { SystemAccessPage } from "./access";
import { SystemConfigurationPage } from "./configuration";
import { SystemObservabilityPage } from "./observability";
import { SystemOperationsPage } from "./operations";
const correlationId = "system-pages-test";
type PageCase = {
readonly name: string;
readonly render: (result?: HousekeepingResult<unknown>) => string;
readonly empty: unknown;
readonly partial: unknown;
readonly ready: unknown;
};
const pageCases: readonly PageCase[] = [
{
name: "access",
render: (result) =>
renderToStaticMarkup(<SystemAccessPage result={result as never} />),
empty: {
kind: "permissions",
ranks: [],
acl: { roles: 0, permissions: 0 },
partialDependencies: [],
},
partial: {
kind: "permissions",
ranks: [{ id: 4, name: "Moderator", level: 4, userCount: 2 }],
acl: { roles: 0, permissions: 0 },
partialDependencies: ["acl"],
},
ready: {
kind: "permissions",
ranks: [{ id: 4, name: "Moderator", level: 4, userCount: 2 }],
acl: { roles: 3, permissions: 18 },
partialDependencies: [],
},
},
{
name: "configuration",
render: (result) =>
renderToStaticMarkup(
<SystemConfigurationPage result={result as never} />,
),
empty: { kind: "settings", settings: [], partialDependencies: [] },
partial: {
kind: "emulator",
settings: [{ key: "hotel.name", value: "Epic" }],
texts: [],
textsTotal: 0,
partialDependencies: ["emulator-texts"],
},
ready: {
kind: "settings",
settings: [{ key: "hotel.name", value: "Epic" }],
partialDependencies: [],
},
},
{
name: "observability",
render: (result) =>
renderToStaticMarkup(
<SystemObservabilityPage result={result as never} />,
),
empty: {
kind: "logs",
metrics: [],
rows: [],
partialDependencies: [],
},
partial: {
kind: "devops",
health: null,
errors: [{ id: "1", primary: "Connection error" }],
partialDependencies: ["health"],
},
ready: {
kind: "analytics",
metrics: [{ label: "Online users", value: 12 }],
rows: [],
partialDependencies: [],
},
},
{
name: "operations",
render: (result) =>
renderToStaticMarkup(<SystemOperationsPage result={result as never} />),
empty: { kind: "alerts", alerts: [], partialDependencies: [] },
partial: {
kind: "command-center",
health: null,
onlineUsers: { count: 0, users: [] },
maintenance: {
enabled: false,
message: "",
minimumLoginRank: 5,
},
partialDependencies: ["health"],
},
ready: {
kind: "alerts",
alerts: [
{
id: 7,
severity: "warning",
type: "emulator",
message: "Connection restored",
isRead: false,
},
],
partialDependencies: [],
},
},
];
describe.each(pageCases)(
"System $name page",
({ render, empty, partial, ready }) => {
it("renders loading, forbidden, and dependency errors explicitly", () => {
expect(render()).toContain('data-housekeeping-state="loading"');
expect(
render(
fail("FORBIDDEN", "errors.housekeeping.forbidden", correlationId),
),
).toContain('data-housekeeping-state="forbidden"');
expect(
render(
fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
correlationId,
),
),
).toContain('data-housekeeping-state="error"');
});
it("renders empty, partial, and ready query results explicitly", () => {
expect(render(ok(empty, correlationId))).toContain(
'data-housekeeping-state="empty"',
);
expect(render(ok(partial, correlationId))).toContain(
'data-housekeeping-state="partial"',
);
expect(render(ok(ready, correlationId))).toContain(
'data-housekeeping-state="ready"',
);
});
},
);
@@ -0,0 +1,257 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../../foundation/authorization";
import {
anyCapability,
fail,
type HousekeepingQuery,
ok,
} from "../../../foundation/contracts";
export interface SystemAccessRank {
readonly id: number;
readonly name: string;
readonly level: number;
readonly userCount: number;
}
export interface SystemAccessRankDetail extends SystemAccessRank {
readonly badge: string;
readonly permissions: Readonly<Record<string, string>>;
readonly cmsRole: {
readonly id: number;
readonly slug: string;
readonly title: string;
readonly permissionSlugs: readonly string[];
} | null;
readonly users: readonly { id: number; username: string }[];
}
export interface SystemAclOverview {
readonly roles: number;
readonly permissions: number;
}
export interface SystemAccessAdapters {
loadRanks(): Promise<readonly SystemAccessRank[]>;
loadRankDetail(rankId: number): Promise<SystemAccessRankDetail | null>;
loadAclOverview(): Promise<SystemAclOverview>;
}
export type SystemAccessQueryInput =
| { readonly routeId: "system.access.permissions" }
| {
readonly routeId: "system.access.permission-detail";
readonly rankId: string;
};
export type SystemAccessQueryData =
| {
readonly kind: "permissions";
readonly ranks: readonly SystemAccessRank[];
readonly acl: SystemAclOverview;
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "permission-detail";
readonly rank: SystemAccessRankDetail;
readonly partialDependencies: readonly string[];
};
const accessCapability = anyCapability(PERMS.PERMISSIONS_MANAGE);
function dependencyUnavailable(correlationId: string) {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
correlationId,
);
}
export function createSystemAccessQuery(
adapters: SystemAccessAdapters,
): HousekeepingQuery<SystemAccessQueryInput, SystemAccessQueryData> {
return {
id: "system.access.query",
owner: "system",
capability: accessCapability,
async run(context, input) {
const authorization = authorizeHousekeeping(context, accessCapability);
if (!authorization.ok) return authorization;
const correlationId = authorization.correlationId;
if (input.routeId === "system.access.permission-detail") {
const rankId = Number(input.rankId);
if (!Number.isInteger(rankId) || rankId <= 0) {
return fail(
"VALIDATION",
"errors.housekeeping.validation",
correlationId,
{ rankId: ["errors.validation.invalid"] },
);
}
try {
const rank = await adapters.loadRankDetail(rankId);
return rank
? ok(
{
kind: "permission-detail" as const,
rank,
partialDependencies: [],
},
correlationId,
)
: fail("NOT_FOUND", "errors.housekeeping.notFound", correlationId);
} catch {
return dependencyUnavailable(correlationId);
}
}
const [ranksResult, aclResult] = await Promise.allSettled([
adapters.loadRanks(),
adapters.loadAclOverview(),
]);
if (
ranksResult.status === "rejected" &&
aclResult.status === "rejected"
) {
return dependencyUnavailable(correlationId);
}
const partialDependencies: string[] = [];
if (ranksResult.status === "rejected") partialDependencies.push("ranks");
if (aclResult.status === "rejected") partialDependencies.push("acl");
return ok(
{
kind: "permissions",
ranks: ranksResult.status === "fulfilled" ? ranksResult.value : [],
acl:
aclResult.status === "fulfilled"
? aclResult.value
: { roles: 0, permissions: 0 },
partialDependencies,
},
correlationId,
);
},
};
}
export const systemAccessAdapters: SystemAccessAdapters = {
async loadRanks() {
const [{ db }, { sql }, { fetchEmulatorRankSummaries }] = await Promise.all(
[
import("@/lib/db"),
import("drizzle-orm"),
import("@/lib/services/permission-ranks"),
],
);
const [ranks, countResult] = await Promise.all([
fetchEmulatorRankSummaries(db),
db.execute(
sql`SELECT \`rank\`, COUNT(*) AS total FROM users GROUP BY \`rank\``,
),
]);
const countRows = countResult[0] as unknown as {
rank: number | bigint;
total: number | bigint;
}[];
const counts = new Map(
countRows.map((row) => [Number(row.rank), Number(row.total)]),
);
return ranks.map((rank) => ({
id: rank.id,
name: rank.rank_name,
level: rank.level,
userCount: counts.get(rank.id) ?? 0,
}));
},
async loadRankDetail(rankId) {
const [{ db }, { sql }, { fetchEmulatorRankForEdit }] = await Promise.all([
import("@/lib/db"),
import("drizzle-orm"),
import("@/lib/services/permission-ranks"),
]);
const rank = await fetchEmulatorRankForEdit(db, rankId);
if (!rank) return null;
const [userResult, roleResult] = await Promise.all([
db.execute(sql`
SELECT id, username
FROM users
WHERE \`rank\` = ${rankId}
ORDER BY username ASC
LIMIT 200
`),
db.execute(sql`
SELECT ar.id, ar.slug, ar.title, ap.slug AS permission_slug
FROM acl_roles ar
LEFT JOIN acl_model_permissions amp
ON amp.model_type = 'Role' AND amp.model_id = ar.id
LEFT JOIN acl_permissions ap ON ap.id = amp.permission_id
WHERE ar.slug = ${`rank_${rankId}`}
ORDER BY ap.slug ASC
`),
]);
const users = userResult[0] as unknown as {
id: number | bigint;
username: string;
}[];
const roles = roleResult[0] as unknown as {
id: number | bigint;
slug: string;
title: string;
permission_slug: string | null;
}[];
const role = roles[0];
return {
id: rank.id,
name: rank.rank_name,
level: rank.level,
userCount: users.length,
badge: rank.badge,
permissions: rank.permissions,
cmsRole: role
? {
id: Number(role.id),
slug: role.slug,
title: role.title,
permissionSlugs: roles.flatMap((row) =>
row.permission_slug ? [row.permission_slug] : [],
),
}
: null,
users: users.map((user) => ({
id: Number(user.id),
username: user.username,
})),
};
},
async loadAclOverview() {
const [{ db }, { sql }] = await Promise.all([
import("@/lib/db"),
import("drizzle-orm"),
]);
const [result] = await db.execute(sql`
SELECT
(SELECT COUNT(*) FROM acl_roles) AS roles,
(SELECT COUNT(*) FROM acl_permissions) AS permissions
`);
const row = (
result as unknown as {
roles: number | bigint;
permissions: number | bigint;
}[]
)[0];
return {
roles: Number(row?.roles ?? 0),
permissions: Number(row?.permissions ?? 0),
};
},
};
export const systemAccessQuery = createSystemAccessQuery(systemAccessAdapters);
@@ -0,0 +1,172 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../../foundation/authorization";
import {
anyCapability,
fail,
type HousekeepingQuery,
ok,
} from "../../../foundation/contracts";
export interface SystemSettingRow {
readonly key: string;
readonly value: string;
readonly comment?: string | null;
}
export interface SystemEmulatorTextResult {
readonly rows: readonly SystemSettingRow[];
readonly total: number;
}
export interface SystemConfigurationAdapters {
loadWebsiteSettings(): Promise<readonly SystemSettingRow[]>;
loadEmulatorSettings(): Promise<readonly SystemSettingRow[]>;
loadEmulatorTexts(): Promise<SystemEmulatorTextResult>;
}
export type SystemConfigurationQueryInput =
| { readonly routeId: "system.configuration.settings" }
| { readonly routeId: "system.configuration.emulator" };
export type SystemConfigurationQueryData =
| {
readonly kind: "settings";
readonly settings: readonly SystemSettingRow[];
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "emulator";
readonly settings: readonly SystemSettingRow[];
readonly texts: readonly SystemSettingRow[];
readonly textsTotal: number;
readonly partialDependencies: readonly string[];
};
const configurationCapability = anyCapability(PERMS.SETTINGS_VIEW);
function unavailable(correlationId: string) {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
correlationId,
);
}
export function createSystemConfigurationQuery(
adapters: SystemConfigurationAdapters,
): HousekeepingQuery<
SystemConfigurationQueryInput,
SystemConfigurationQueryData
> {
return {
id: "system.configuration.query",
owner: "system",
capability: configurationCapability,
async run(context, input) {
const authorization = authorizeHousekeeping(
context,
configurationCapability,
);
if (!authorization.ok) return authorization;
const correlationId = authorization.correlationId;
if (input.routeId === "system.configuration.settings") {
try {
return ok(
{
kind: "settings" as const,
settings: await adapters.loadWebsiteSettings(),
partialDependencies: [],
},
correlationId,
);
} catch {
return unavailable(correlationId);
}
}
const [settingsResult, textsResult] = await Promise.allSettled([
adapters.loadEmulatorSettings(),
adapters.loadEmulatorTexts(),
]);
if (
settingsResult.status === "rejected" &&
textsResult.status === "rejected"
) {
return unavailable(correlationId);
}
const partialDependencies: string[] = [];
if (settingsResult.status === "rejected") {
partialDependencies.push("emulator-settings");
}
if (textsResult.status === "rejected") {
partialDependencies.push("emulator-texts");
}
const textData =
textsResult.status === "fulfilled"
? textsResult.value
: { rows: [], total: 0 };
return ok(
{
kind: "emulator",
settings:
settingsResult.status === "fulfilled" ? settingsResult.value : [],
texts: textData.rows,
textsTotal: textData.total,
partialDependencies,
},
correlationId,
);
},
};
}
export const systemConfigurationAdapters: SystemConfigurationAdapters = {
async loadWebsiteSettings() {
const [{ asc }, { db, WebsiteSetting }] = await Promise.all([
import("drizzle-orm"),
import("@/lib/db"),
]);
return db
.select({
key: WebsiteSetting.key,
value: WebsiteSetting.value,
comment: WebsiteSetting.comment,
})
.from(WebsiteSetting)
.orderBy(asc(WebsiteSetting.key));
},
async loadEmulatorSettings() {
const [{ asc }, { db, EmulatorSettings }] = await Promise.all([
import("drizzle-orm"),
import("@/lib/db"),
]);
return db
.select({ key: EmulatorSettings.key, value: EmulatorSettings.value })
.from(EmulatorSettings)
.orderBy(asc(EmulatorSettings.key));
},
async loadEmulatorTexts() {
const [{ asc, count }, { db, EmulatorTexts }] = await Promise.all([
import("drizzle-orm"),
import("@/lib/db"),
]);
const [rows, totals] = await Promise.all([
db
.select({ key: EmulatorTexts.key, value: EmulatorTexts.value })
.from(EmulatorTexts)
.orderBy(asc(EmulatorTexts.key))
.limit(300),
db.select({ total: count() }).from(EmulatorTexts),
]);
return { rows, total: Number(totals[0]?.total ?? 0) };
},
};
export const systemConfigurationQuery = createSystemConfigurationQuery(
systemConfigurationAdapters,
);
@@ -0,0 +1,427 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../../foundation/authorization";
import {
anyCapability,
fail,
type HousekeepingQuery,
ok,
} from "../../../foundation/contracts";
export interface SystemMetric {
readonly label: string;
readonly value: number | string;
}
export interface SystemObservationRow {
readonly id: string;
readonly primary: string;
readonly secondary?: string;
readonly timestamp?: number | string;
}
export interface SystemObservationList {
readonly metrics: readonly SystemMetric[];
readonly rows: readonly SystemObservationRow[];
}
export interface SystemHealthSnapshot {
readonly dbOk: boolean;
readonly dbLatencyMs: number;
readonly redisOk: boolean | null;
readonly emulatorOk: boolean;
readonly onlineUsers: number;
}
export interface SystemObservabilityAdapters {
loadAnalytics(
view: "overview" | "activity" | "economy",
): Promise<SystemObservationList>;
loadLogs(
view: "staff" | "audit" | "chat" | "commands" | "trades",
): Promise<SystemObservationList>;
loadHealth(): Promise<SystemHealthSnapshot>;
loadErrors(): Promise<readonly SystemObservationRow[]>;
}
export type SystemObservabilityQueryInput = {
readonly routeId:
| "system.observability.analytics"
| "system.observability.analytics-activity"
| "system.observability.analytics-economy"
| "system.observability.devops"
| "system.observability.devops-errors"
| "system.observability.logs-staff"
| "system.observability.logs-audit"
| "system.observability.logs-chat"
| "system.observability.logs-commands"
| "system.observability.logs-trades";
};
export type SystemObservabilityQueryData =
| {
readonly kind: "analytics" | "logs";
readonly metrics: readonly SystemMetric[];
readonly rows: readonly SystemObservationRow[];
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "devops";
readonly health: SystemHealthSnapshot | null;
readonly errors: readonly SystemObservationRow[];
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "devops-errors";
readonly errors: readonly SystemObservationRow[];
readonly partialDependencies: readonly string[];
};
const broadCapability = anyCapability(
PERMS.ANALYTICS_VIEW,
PERMS.DEVOPS_VIEW,
PERMS.LOGS_VIEW,
);
function capabilityForRoute(routeId: SystemObservabilityQueryInput["routeId"]) {
if (routeId.startsWith("system.observability.analytics")) {
return anyCapability(PERMS.ANALYTICS_VIEW);
}
if (routeId.startsWith("system.observability.devops")) {
return anyCapability(PERMS.DEVOPS_VIEW);
}
return anyCapability(PERMS.LOGS_VIEW);
}
function unavailable(correlationId: string) {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
correlationId,
);
}
export function createSystemObservabilityQuery(
adapters: SystemObservabilityAdapters,
): HousekeepingQuery<
SystemObservabilityQueryInput,
SystemObservabilityQueryData
> {
return {
id: "system.observability.query",
owner: "system",
capability: broadCapability,
async run(context, input) {
const authorization = authorizeHousekeeping(
context,
capabilityForRoute(input.routeId),
);
if (!authorization.ok) return authorization;
const correlationId = authorization.correlationId;
if (input.routeId === "system.observability.devops") {
const [healthResult, errorsResult] = await Promise.allSettled([
adapters.loadHealth(),
adapters.loadErrors(),
]);
if (
healthResult.status === "rejected" &&
errorsResult.status === "rejected"
) {
return unavailable(correlationId);
}
const partialDependencies: string[] = [];
if (healthResult.status === "rejected") {
partialDependencies.push("health");
}
if (errorsResult.status === "rejected") {
partialDependencies.push("emulator-errors");
}
return ok(
{
kind: "devops",
health:
healthResult.status === "fulfilled" ? healthResult.value : null,
errors:
errorsResult.status === "fulfilled" ? errorsResult.value : [],
partialDependencies,
},
correlationId,
);
}
if (input.routeId === "system.observability.devops-errors") {
try {
return ok(
{
kind: "devops-errors" as const,
errors: await adapters.loadErrors(),
partialDependencies: [],
},
correlationId,
);
} catch {
return unavailable(correlationId);
}
}
const analyticsView = {
"system.observability.analytics": "overview",
"system.observability.analytics-activity": "activity",
"system.observability.analytics-economy": "economy",
} as const;
const analytics =
analyticsView[input.routeId as keyof typeof analyticsView];
try {
if (analytics) {
const data = await adapters.loadAnalytics(analytics);
return ok(
{
kind: "analytics" as const,
...data,
partialDependencies: [],
},
correlationId,
);
}
const logViews = {
"system.observability.logs-staff": "staff",
"system.observability.logs-audit": "audit",
"system.observability.logs-chat": "chat",
"system.observability.logs-commands": "commands",
"system.observability.logs-trades": "trades",
} as const;
const data = await adapters.loadLogs(
logViews[input.routeId as keyof typeof logViews],
);
return ok(
{
kind: "logs" as const,
...data,
partialDependencies: [],
},
correlationId,
);
} catch {
return unavailable(correlationId);
}
},
};
}
async function rawRows<T>(query: unknown): Promise<T[]> {
const { db } = await import("@/lib/db");
const [rows] = await db.execute(query as never);
return (rows ?? []) as unknown as T[];
}
export const systemObservabilityAdapters: SystemObservabilityAdapters = {
async loadAnalytics(view) {
const { sql } = await import("drizzle-orm");
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86_400;
if (view === "overview") {
const rows = await rawRows<Record<string, number | bigint>>(sql`
SELECT
(SELECT COUNT(*) FROM users) AS totalUsers,
(SELECT COUNT(*) FROM users WHERE online = '1') AS onlineUsers,
(SELECT COUNT(*) FROM rooms) AS totalRooms,
(SELECT COUNT(*) FROM chatlogs_room WHERE timestamp >= ${weekAgo}) AS weekChats,
(SELECT COUNT(*) FROM room_trade_log WHERE timestamp >= ${weekAgo}) AS weekTrades
`);
const row = rows[0] ?? {};
return {
metrics: [
{ label: "Total users", value: Number(row.totalUsers ?? 0) },
{ label: "Online users", value: Number(row.onlineUsers ?? 0) },
{ label: "Total rooms", value: Number(row.totalRooms ?? 0) },
{ label: "Weekly chats", value: Number(row.weekChats ?? 0) },
{ label: "Weekly trades", value: Number(row.weekTrades ?? 0) },
],
rows: [],
};
}
if (view === "activity") {
const rows = await rawRows<Record<string, number | bigint>>(sql`
SELECT
(SELECT COUNT(*) FROM chatlogs_room WHERE timestamp >= ${weekAgo}) AS chats,
(SELECT COUNT(*) FROM commandlogs WHERE timestamp >= ${weekAgo}) AS commands,
(SELECT COUNT(*) FROM bans WHERE timestamp >= ${weekAgo}) AS bans,
(SELECT COUNT(*) FROM users WHERE account_created >= ${weekAgo}) AS registrations
`);
const row = rows[0] ?? {};
return {
metrics: [
{ label: "Chats", value: Number(row.chats ?? 0) },
{ label: "Commands", value: Number(row.commands ?? 0) },
{ label: "Bans", value: Number(row.bans ?? 0) },
{ label: "Registrations", value: Number(row.registrations ?? 0) },
],
rows: [],
};
}
const rows = await rawRows<Record<string, number | bigint>>(sql`
SELECT
COALESCE(SUM(credits), 0) AS credits,
COALESCE(SUM(pixels), 0) AS pixels,
COALESCE(SUM(points), 0) AS points,
(SELECT COUNT(*) FROM logs_shop_purchases WHERE timestamp >= ${weekAgo}) AS purchases,
(SELECT COUNT(*) FROM room_trade_log WHERE timestamp >= ${weekAgo}) AS trades
FROM users
`);
const row = rows[0] ?? {};
return {
metrics: [
{ label: "Credits", value: Number(row.credits ?? 0) },
{ label: "Pixels", value: Number(row.pixels ?? 0) },
{ label: "Points", value: Number(row.points ?? 0) },
{ label: "Purchases", value: Number(row.purchases ?? 0) },
{ label: "Trades", value: Number(row.trades ?? 0) },
],
rows: [],
};
},
async loadLogs(view) {
if (view === "audit") {
const { getAuditLogs } = await import("@/lib/services/audit");
const result = await getAuditLogs({ page: 1, perPage: 50 });
return {
metrics: [{ label: "Audit entries", value: result.total }],
rows: result.rows.map((row) => ({
id: String(row.id),
primary: `${row.username}: ${row.action}`,
secondary: row.target,
timestamp: row.createdAt,
})),
};
}
const { sql } = await import("drizzle-orm");
if (view === "staff") {
const rows = await rawRows<{
id: number;
action: string;
description: string;
username: string | null;
createdAt: string;
}>(sql`
SELECT sa.id, sa.action, sa.description, u.username,
sa.created_at AS createdAt
FROM staff_activities sa
LEFT JOIN users u ON u.id = sa.user_id
ORDER BY sa.id DESC LIMIT 50
`);
return {
metrics: [{ label: "Staff activities", value: rows.length }],
rows: rows.map((row) => ({
id: String(row.id),
primary: `${row.username ?? "Unknown"}: ${row.action}`,
secondary: row.description,
timestamp: row.createdAt,
})),
};
}
if (view === "chat") {
const rows = await rawRows<{
id: number;
username: string | null;
message: string;
timestamp: number;
}>(sql`
SELECT c.id, u.username, c.message, c.timestamp
FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id
ORDER BY c.timestamp DESC LIMIT 50
`);
return {
metrics: [{ label: "Chat entries", value: rows.length }],
rows: rows.map((row) => ({
id: String(row.id),
primary: row.username ?? "Unknown",
secondary: row.message,
timestamp: row.timestamp,
})),
};
}
if (view === "commands") {
const rows = await rawRows<{
id: number;
username: string | null;
command: string;
params: string;
timestamp: number;
}>(sql`
SELECT c.id, u.username, c.command, c.params, c.timestamp
FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id
ORDER BY c.timestamp DESC LIMIT 50
`);
return {
metrics: [{ label: "Command entries", value: rows.length }],
rows: rows.map((row) => ({
id: String(row.id),
primary: `${row.username ?? "Unknown"}: ${row.command}`,
secondary: row.params,
timestamp: row.timestamp,
})),
};
}
const rows = await rawRows<{
id: number;
userOne: string | null;
userTwo: string | null;
timestamp: number;
}>(sql`
SELECT t.id, u1.username AS userOne, u2.username AS userTwo, t.timestamp
FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id
ORDER BY t.timestamp DESC LIMIT 50
`);
return {
metrics: [{ label: "Trade entries", value: rows.length }],
rows: rows.map((row) => ({
id: String(row.id),
primary: `${row.userOne ?? "Unknown"} ↔ ${row.userTwo ?? "Unknown"}`,
secondary: "Completed",
timestamp: row.timestamp,
})),
};
},
async loadHealth() {
const { fetchOpsHealth } = await import("@/lib/admin/ops-health");
return fetchOpsHealth();
},
async loadErrors() {
const { sql } = await import("drizzle-orm");
const rows = await rawRows<{
id: number;
type: string;
version: string;
stacktrace: Uint8Array | string;
timestamp: number;
}>(sql`
SELECT id, type, version, stacktrace, timestamp
FROM emulator_errors
ORDER BY id DESC LIMIT 50
`);
return rows.map((row) => ({
id: String(row.id),
primary: `${row.type} (${row.version})`,
secondary:
typeof row.stacktrace === "string"
? row.stacktrace
: Buffer.from(row.stacktrace).toString("utf8"),
timestamp: row.timestamp,
}));
},
};
export const systemObservabilityQuery = createSystemObservabilityQuery(
systemObservabilityAdapters,
);
@@ -0,0 +1,246 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../../foundation/authorization";
import {
anyCapability,
fail,
type HousekeepingQuery,
ok,
} from "../../../foundation/contracts";
import type { SystemHealthSnapshot } from "./observability";
export interface SystemAlertRow {
readonly id: number;
readonly severity: string;
readonly type: string;
readonly message: string;
readonly isRead: boolean;
readonly createdAt?: string | Date | null;
}
export interface SystemOnlineRoster {
readonly count: number;
readonly users: readonly {
readonly id: number;
readonly username: string;
readonly roomId?: number | null;
readonly roomName?: string | null;
}[];
}
export interface SystemMaintenanceSnapshot {
readonly enabled: boolean;
readonly message: string;
readonly minimumLoginRank: number;
}
export interface SystemOperationsAdapters {
loadAlerts(): Promise<readonly SystemAlertRow[]>;
loadHealth(): Promise<SystemHealthSnapshot>;
loadOnlineUsers(): Promise<SystemOnlineRoster>;
loadMaintenance(): Promise<SystemMaintenanceSnapshot>;
}
export type SystemOperationsQueryInput = {
readonly routeId:
| "system.operations.alerts"
| "system.operations.command-center"
| "system.operations.maintenance";
};
export type SystemOperationsQueryData =
| {
readonly kind: "alerts";
readonly alerts: readonly SystemAlertRow[];
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "command-center";
readonly health: SystemHealthSnapshot | null;
readonly onlineUsers: SystemOnlineRoster;
readonly maintenance: SystemMaintenanceSnapshot | null;
readonly partialDependencies: readonly string[];
}
| {
readonly kind: "maintenance";
readonly maintenance: SystemMaintenanceSnapshot;
readonly partialDependencies: readonly string[];
};
const broadCapability = anyCapability(
PERMS.NOTIFICATIONS_VIEW,
PERMS.RCON_EXECUTE,
PERMS.SETTINGS_VIEW,
);
function capabilityForRoute(routeId: SystemOperationsQueryInput["routeId"]) {
if (routeId === "system.operations.alerts") {
return anyCapability(PERMS.NOTIFICATIONS_VIEW);
}
if (routeId === "system.operations.command-center") {
return anyCapability(PERMS.RCON_EXECUTE);
}
return anyCapability(PERMS.SETTINGS_VIEW);
}
function unavailable(correlationId: string) {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
correlationId,
);
}
export function createSystemOperationsQuery(
adapters: SystemOperationsAdapters,
): HousekeepingQuery<SystemOperationsQueryInput, SystemOperationsQueryData> {
return {
id: "system.operations.query",
owner: "system",
capability: broadCapability,
async run(context, input) {
const authorization = authorizeHousekeeping(
context,
capabilityForRoute(input.routeId),
);
if (!authorization.ok) return authorization;
const correlationId = authorization.correlationId;
if (input.routeId === "system.operations.alerts") {
try {
return ok(
{
kind: "alerts" as const,
alerts: await adapters.loadAlerts(),
partialDependencies: [],
},
correlationId,
);
} catch {
return unavailable(correlationId);
}
}
if (input.routeId === "system.operations.maintenance") {
try {
return ok(
{
kind: "maintenance" as const,
maintenance: await adapters.loadMaintenance(),
partialDependencies: [],
},
correlationId,
);
} catch {
return unavailable(correlationId);
}
}
const [healthResult, onlineResult, maintenanceResult] =
await Promise.allSettled([
adapters.loadHealth(),
adapters.loadOnlineUsers(),
adapters.loadMaintenance(),
]);
if (
healthResult.status === "rejected" &&
onlineResult.status === "rejected" &&
maintenanceResult.status === "rejected"
) {
return unavailable(correlationId);
}
const partialDependencies: string[] = [];
if (healthResult.status === "rejected")
partialDependencies.push("health");
if (onlineResult.status === "rejected") {
partialDependencies.push("online-users");
}
if (maintenanceResult.status === "rejected") {
partialDependencies.push("maintenance");
}
return ok(
{
kind: "command-center",
health:
healthResult.status === "fulfilled" ? healthResult.value : null,
onlineUsers:
onlineResult.status === "fulfilled"
? onlineResult.value
: { count: 0, users: [] },
maintenance:
maintenanceResult.status === "fulfilled"
? maintenanceResult.value
: null,
partialDependencies,
},
correlationId,
);
},
};
}
export const systemOperationsAdapters: SystemOperationsAdapters = {
async loadAlerts() {
const [{ desc }, { AlertLogs, db }] = await Promise.all([
import("drizzle-orm"),
import("@/lib/db"),
]);
const rows = await db
.select({
id: AlertLogs.id,
severity: AlertLogs.severity,
type: AlertLogs.type,
message: AlertLogs.message,
isRead: AlertLogs.isRead,
createdAt: AlertLogs.createdAt,
})
.from(AlertLogs)
.orderBy(desc(AlertLogs.id))
.limit(100);
return rows.map((row) => ({ ...row, id: Number(row.id) }));
},
async loadHealth() {
const { fetchOpsHealth } = await import("@/lib/admin/ops-health");
return fetchOpsHealth();
},
async loadOnlineUsers() {
const { fetchOpsOnlineUsers } = await import(
"@/lib/admin/ops-online-users"
);
return fetchOpsOnlineUsers(40);
},
async loadMaintenance() {
const [{ inArray }, { db, WebsiteSetting }] = await Promise.all([
import("drizzle-orm"),
import("@/lib/db"),
]);
const rows = await db
.select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
.from(WebsiteSetting)
.where(
inArray(WebsiteSetting.key, [
"maintenance_enabled",
"maintenance_message",
"min_maintenance_login_rank",
]),
);
const values = new Map(rows.map((row) => [row.key, row.value]));
const enabled = values.get("maintenance_enabled") ?? "0";
const parsedRank = Number.parseInt(
values.get("min_maintenance_login_rank") ?? "5",
10,
);
return {
enabled: enabled === "1" || enabled.toLowerCase() === "true",
message: values.get("maintenance_message") ?? "",
minimumLoginRank: Number.isFinite(parsedRank) ? parsedRank : 5,
};
},
};
export const systemOperationsQuery = createSystemOperationsQuery(
systemOperationsAdapters,
);
@@ -0,0 +1,207 @@
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
import { createSystemAccessQuery } from "./access";
import { createSystemConfigurationQuery } from "./configuration";
import { createSystemObservabilityQuery } from "./observability";
import { createSystemOperationsQuery } from "./operations";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 99 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
describe("System access query", () => {
it("combines live rank and ACL data through injected adapters", async () => {
const query = createSystemAccessQuery({
loadRanks: async () => [
{ id: 7, name: "Administrator", level: 7, userCount: 2 },
],
loadRankDetail: async () => null,
loadAclOverview: async () => ({ roles: 3, permissions: 18 }),
});
const result = await query.run(context([PERMS.PERMISSIONS_MANAGE]), {
routeId: "system.access.permissions",
});
expect(result).toMatchObject({
ok: true,
data: {
kind: "permissions",
ranks: [{ id: 7, name: "Administrator", level: 7, userCount: 2 }],
acl: { roles: 3, permissions: 18 },
partialDependencies: [],
},
});
});
it("fails closed before calling adapters and maps total dependency failure", async () => {
const loadRanks = vi.fn(async () => {
throw new Error("database offline");
});
const loadAclOverview = vi.fn(async () => {
throw new Error("acl offline");
});
const query = createSystemAccessQuery({
loadRanks,
loadRankDetail: async () => null,
loadAclOverview,
});
const forbidden = await query.run(context([]), {
routeId: "system.access.permissions",
});
expect(forbidden).toMatchObject({
ok: false,
error: { code: "FORBIDDEN" },
});
expect(loadRanks).not.toHaveBeenCalled();
const unavailable = await query.run(context([PERMS.PERMISSIONS_MANAGE]), {
routeId: "system.access.permissions",
});
expect(unavailable).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
});
});
});
describe("System configuration query", () => {
it("returns usable emulator settings as partial when texts are unavailable", async () => {
const query = createSystemConfigurationQuery({
loadWebsiteSettings: async () => [],
loadEmulatorSettings: async () => [{ key: "version", value: "1.0" }],
loadEmulatorTexts: async () => {
throw new Error("emulator_texts unavailable");
},
});
const result = await query.run(context([PERMS.SETTINGS_VIEW]), {
routeId: "system.configuration.emulator",
});
expect(result).toMatchObject({
ok: true,
data: {
kind: "emulator",
settings: [{ key: "version", value: "1.0" }],
texts: [],
textsTotal: 0,
partialDependencies: ["emulator-texts"],
},
});
});
});
describe("System observability query", () => {
it("keeps health evidence when the error feed is unavailable", async () => {
const query = createSystemObservabilityQuery({
loadAnalytics: async () => ({ metrics: [], rows: [] }),
loadLogs: async () => ({ metrics: [], rows: [] }),
loadHealth: async () => ({
dbOk: true,
dbLatencyMs: 4,
redisOk: null,
emulatorOk: true,
onlineUsers: 12,
}),
loadErrors: async () => {
throw new Error("errors table unavailable");
},
});
const result = await query.run(context([PERMS.DEVOPS_VIEW]), {
routeId: "system.observability.devops",
});
expect(result).toMatchObject({
ok: true,
data: {
kind: "devops",
health: { dbOk: true, onlineUsers: 12 },
errors: [],
partialDependencies: ["emulator-errors"],
},
});
});
});
describe("System operations query", () => {
it("maps an unavailable command-center dependency set to a typed failure", async () => {
const down = async () => {
throw new Error("dependency unavailable");
};
const query = createSystemOperationsQuery({
loadAlerts: down,
loadHealth: down,
loadOnlineUsers: down,
loadMaintenance: down,
});
const result = await query.run(context([PERMS.RCON_EXECUTE]), {
routeId: "system.operations.command-center",
});
expect(result).toMatchObject({
ok: false,
error: {
code: "DEPENDENCY_UNAVAILABLE",
messageKey: "errors.housekeeping.dependencyUnavailable",
},
});
});
it("loads alert and maintenance workflows from their dedicated adapters", async () => {
const query = createSystemOperationsQuery({
loadAlerts: async () => [
{
id: 1,
severity: "warning",
type: "runtime",
message: "High load",
isRead: false,
},
],
loadHealth: async () => ({
dbOk: true,
dbLatencyMs: 1,
redisOk: true,
emulatorOk: true,
onlineUsers: 1,
}),
loadOnlineUsers: async () => ({ count: 1, users: [] }),
loadMaintenance: async () => ({
enabled: false,
message: "",
minimumLoginRank: 5,
}),
});
const alerts = await query.run(context([PERMS.NOTIFICATIONS_VIEW]), {
routeId: "system.operations.alerts",
});
const maintenance = await query.run(context([PERMS.SETTINGS_VIEW]), {
routeId: "system.operations.maintenance",
});
expect(alerts).toMatchObject({
ok: true,
data: { kind: "alerts", alerts: [{ message: "High load" }] },
});
expect(maintenance).toMatchObject({
ok: true,
data: {
kind: "maintenance",
maintenance: { enabled: false, minimumLoginRank: 5 },
},
});
});
});
@@ -0,0 +1,26 @@
import type { HousekeepingRouteHandler } from "../../route-handlers";
import { renderSystemAccessPage } from "./pages/access";
import { renderSystemConfigurationPage } from "./pages/configuration";
import { renderSystemObservabilityPage } from "./pages/observability";
import { renderSystemOperationsPage } from "./pages/operations";
import { SYSTEM_ROUTE_IDS, type SystemRouteId } from "./routes";
function rendererFor(
routeId: SystemRouteId,
): HousekeepingRouteHandler["render"] {
if (routeId.startsWith("system.access.")) return renderSystemAccessPage;
if (routeId.startsWith("system.configuration.")) {
return renderSystemConfigurationPage;
}
if (routeId.startsWith("system.observability.")) {
return renderSystemObservabilityPage;
}
return renderSystemOperationsPage;
}
export const SYSTEM_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
Object.freeze(
SYSTEM_ROUTE_IDS.map((routeId) =>
Object.freeze({ routeId, render: rendererFor(routeId) }),
),
);
@@ -0,0 +1,147 @@
import { describe, expect, it } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import { systemMigrationEntries } from "../../migration/system";
import { SYSTEM_ROUTE_IDS, SYSTEM_ROUTES } from "./routes";
const expectedRoutes = [
[
"system.access.permissions",
"/ase/system/access/permissions",
"pages.admin.hubs.tabs.permissions",
PERMS.PERMISSIONS_MANAGE,
],
[
"system.access.permission-detail",
"/ase/system/access/permissions/:id",
"pages.admin.hubs.tabs.permissions",
PERMS.PERMISSIONS_MANAGE,
],
[
"system.configuration.settings",
"/ase/system/configuration/settings",
"pages.admin.hubs.tabs.cms",
PERMS.SETTINGS_VIEW,
],
[
"system.configuration.emulator",
"/ase/system/configuration/emulator",
"pages.admin.hubs.tabs.emulator",
PERMS.SETTINGS_VIEW,
],
[
"system.observability.analytics",
"/ase/system/observability/analytics",
"pages.admin.hubs.tabs.analytics",
PERMS.ANALYTICS_VIEW,
],
[
"system.observability.analytics-activity",
"/ase/system/observability/analytics/activity",
"pages.admin.hubs.tabs.activity",
PERMS.ANALYTICS_VIEW,
],
[
"system.observability.analytics-economy",
"/ase/system/observability/analytics/economy",
"pages.admin.hubs.tabs.economy",
PERMS.ANALYTICS_VIEW,
],
[
"system.observability.devops",
"/ase/system/observability/devops",
"pages.admin.hubs.tabs.devops",
PERMS.DEVOPS_VIEW,
],
[
"system.observability.devops-errors",
"/ase/system/observability/devops/errors",
"pages.admin.hubs.tabs.errors",
PERMS.DEVOPS_VIEW,
],
[
"system.observability.logs-staff",
"/ase/system/observability/logs/staff",
"pages.admin.hubs.tabs.logs",
PERMS.LOGS_VIEW,
],
[
"system.observability.logs-audit",
"/ase/system/observability/logs/audit",
"pages.admin.hubs.tabs.audit",
PERMS.LOGS_VIEW,
],
[
"system.observability.logs-chat",
"/ase/system/observability/logs/chat",
"pages.admin.hubs.tabs.chat",
PERMS.LOGS_VIEW,
],
[
"system.observability.logs-commands",
"/ase/system/observability/logs/commands",
"pages.admin.hubs.tabs.commands",
PERMS.LOGS_VIEW,
],
[
"system.observability.logs-trades",
"/ase/system/observability/logs/trades",
"pages.admin.hubs.tabs.trades",
PERMS.LOGS_VIEW,
],
[
"system.operations.alerts",
"/ase/system/operations/alerts",
"pages.admin.hubs.tabs.alerts",
PERMS.NOTIFICATIONS_VIEW,
],
[
"system.operations.command-center",
"/ase/system/operations/command-center",
"pages.admin.hubs.tabs.commando",
PERMS.RCON_EXECUTE,
],
[
"system.operations.maintenance",
"/ase/system/operations/maintenance",
"pages.admin.hubs.tabs.maintenance",
PERMS.SETTINGS_VIEW,
],
] as const;
describe("SYSTEM_ROUTES", () => {
it("declares the exact ordered System route IDs", () => {
expect(SYSTEM_ROUTE_IDS).toEqual(expectedRoutes.map(([id]) => id));
expect(SYSTEM_ROUTES.map((route) => route.id)).toEqual(SYSTEM_ROUTE_IDS);
});
it("uses the canonical destinations, stable labels, and read capabilities", () => {
expect(
SYSTEM_ROUTES.map((route) => [
route.id,
route.href,
route.labelKey,
route.capability.mode,
route.capability.slugs,
]),
).toEqual(
expectedRoutes.map(([id, href, labelKey, capability]) => [
id,
href,
labelKey,
"any",
[capability],
]),
);
});
it("covers every non-removed System migration destination exactly once", () => {
const plannedTargets = systemMigrationEntries
.filter((entry) => entry.targetPath !== null)
.map((entry) => entry.targetPath)
.sort();
const routeTargets = SYSTEM_ROUTES.map((route) => route.href).sort();
expect(routeTargets).toEqual(plannedTargets);
expect(new Set(routeTargets).size).toBe(routeTargets.length);
});
});
@@ -0,0 +1,132 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingRouteDefinition,
} from "../../foundation/contracts";
export const SYSTEM_ROUTE_IDS = [
"system.access.permissions",
"system.access.permission-detail",
"system.configuration.settings",
"system.configuration.emulator",
"system.observability.analytics",
"system.observability.analytics-activity",
"system.observability.analytics-economy",
"system.observability.devops",
"system.observability.devops-errors",
"system.observability.logs-staff",
"system.observability.logs-audit",
"system.observability.logs-chat",
"system.observability.logs-commands",
"system.observability.logs-trades",
"system.operations.alerts",
"system.operations.command-center",
"system.operations.maintenance",
] as const;
export type SystemRouteId = (typeof SYSTEM_ROUTE_IDS)[number];
export const SYSTEM_ROUTES = [
{
id: "system.access.permissions",
labelKey: "pages.admin.hubs.tabs.permissions",
href: "/ase/system/access/permissions",
capability: anyCapability(PERMS.PERMISSIONS_MANAGE),
},
{
id: "system.access.permission-detail",
labelKey: "pages.admin.hubs.tabs.permissions",
href: "/ase/system/access/permissions/:id",
capability: anyCapability(PERMS.PERMISSIONS_MANAGE),
},
{
id: "system.configuration.settings",
labelKey: "pages.admin.hubs.tabs.cms",
href: "/ase/system/configuration/settings",
capability: anyCapability(PERMS.SETTINGS_VIEW),
},
{
id: "system.configuration.emulator",
labelKey: "pages.admin.hubs.tabs.emulator",
href: "/ase/system/configuration/emulator",
capability: anyCapability(PERMS.SETTINGS_VIEW),
},
{
id: "system.observability.analytics",
labelKey: "pages.admin.hubs.tabs.analytics",
href: "/ase/system/observability/analytics",
capability: anyCapability(PERMS.ANALYTICS_VIEW),
},
{
id: "system.observability.analytics-activity",
labelKey: "pages.admin.hubs.tabs.activity",
href: "/ase/system/observability/analytics/activity",
capability: anyCapability(PERMS.ANALYTICS_VIEW),
},
{
id: "system.observability.analytics-economy",
labelKey: "pages.admin.hubs.tabs.economy",
href: "/ase/system/observability/analytics/economy",
capability: anyCapability(PERMS.ANALYTICS_VIEW),
},
{
id: "system.observability.devops",
labelKey: "pages.admin.hubs.tabs.devops",
href: "/ase/system/observability/devops",
capability: anyCapability(PERMS.DEVOPS_VIEW),
},
{
id: "system.observability.devops-errors",
labelKey: "pages.admin.hubs.tabs.errors",
href: "/ase/system/observability/devops/errors",
capability: anyCapability(PERMS.DEVOPS_VIEW),
},
{
id: "system.observability.logs-staff",
labelKey: "pages.admin.hubs.tabs.logs",
href: "/ase/system/observability/logs/staff",
capability: anyCapability(PERMS.LOGS_VIEW),
},
{
id: "system.observability.logs-audit",
labelKey: "pages.admin.hubs.tabs.audit",
href: "/ase/system/observability/logs/audit",
capability: anyCapability(PERMS.LOGS_VIEW),
},
{
id: "system.observability.logs-chat",
labelKey: "pages.admin.hubs.tabs.chat",
href: "/ase/system/observability/logs/chat",
capability: anyCapability(PERMS.LOGS_VIEW),
},
{
id: "system.observability.logs-commands",
labelKey: "pages.admin.hubs.tabs.commands",
href: "/ase/system/observability/logs/commands",
capability: anyCapability(PERMS.LOGS_VIEW),
},
{
id: "system.observability.logs-trades",
labelKey: "pages.admin.hubs.tabs.trades",
href: "/ase/system/observability/logs/trades",
capability: anyCapability(PERMS.LOGS_VIEW),
},
{
id: "system.operations.alerts",
labelKey: "pages.admin.hubs.tabs.alerts",
href: "/ase/system/operations/alerts",
capability: anyCapability(PERMS.NOTIFICATIONS_VIEW),
},
{
id: "system.operations.command-center",
labelKey: "pages.admin.hubs.tabs.commando",
href: "/ase/system/operations/command-center",
capability: anyCapability(PERMS.RCON_EXECUTE),
},
{
id: "system.operations.maintenance",
labelKey: "pages.admin.hubs.tabs.maintenance",
href: "/ase/system/operations/maintenance",
capability: anyCapability(PERMS.SETTINGS_VIEW),
},
] as const satisfies readonly HousekeepingRouteDefinition[];
@@ -0,0 +1,108 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
const { markReadWhere, rconSend } = vi.hoisted(() => ({
markReadWhere: vi.fn(),
rconSend: vi.fn(),
}));
vi.mock("@/lib/db", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db")>();
return {
...actual,
db: {
...actual.db,
update: vi.fn(() => ({
set: vi.fn(() => ({ where: markReadWhere })),
})),
},
};
});
vi.mock("@/lib/services/rcon", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/services/rcon")>();
return {
...actual,
rcon: { send: rconSend },
};
});
import { systemMutationService } from "./mutations";
function capabilityContext(
granted: readonly string[],
): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 500 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
describe("System production mutation failures", () => {
beforeEach(() => {
markReadWhere.mockReset();
rconSend.mockReset();
});
it("maps a failed alert broadcast to dependency unavailable", async () => {
rconSend.mockResolvedValue(false);
const result = await systemMutationService.execute(
{
capability: capabilityContext([PERMS.NOTIFICATIONS_EDIT]),
correlationId: "broadcast-failure",
},
"operations.alert.broadcast",
{ message: "Hotel notice" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
correlationId: "broadcast-failure",
});
});
it("maps a thrown alert broadcast to dependency unavailable", async () => {
rconSend.mockRejectedValue(new Error("RCON unavailable"));
const result = await systemMutationService.execute(
{
capability: capabilityContext([PERMS.NOTIFICATIONS_EDIT]),
correlationId: "broadcast-error",
},
"operations.alert.broadcast",
{ message: "Hotel notice" },
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
correlationId: "broadcast-error",
});
});
it("maps mark-read persistence failure to dependency unavailable", async () => {
markReadWhere.mockRejectedValue(new Error("database unavailable"));
const result = await systemMutationService.execute(
{
capability: capabilityContext([PERMS.NOTIFICATIONS_VIEW]),
correlationId: "mark-read-error",
},
"operations.alerts.mark-read",
{},
);
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
correlationId: "mark-read-error",
});
});
});
@@ -0,0 +1,763 @@
import "server-only";
import { and, count, eq, inArray, sql } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config";
import {
AclModelPermission,
AclModelRole,
AclPermission,
AclRole,
AlertLogs,
db,
EmulatorSettings,
EmulatorTexts,
User,
WebsiteSetting,
} from "@/lib/db";
import {
HABBO_GAMEDATA_HOTEL_SETTING_KEY,
normalizeHabboGamedataHotel,
} from "@/lib/habbo-gamedata-hotel";
import { PERMS } from "@/lib/permission-slugs";
import { clearOfficialHabboFurnidataCache } from "@/lib/services/habbo-furnidata-cache";
import { clearBadgeCache } from "@/lib/services/habboassets";
import {
createEmulatorRank,
deleteEmulatorRank,
updateEmulatorRank,
} from "@/lib/services/permission-ranks";
import { rcon } from "@/lib/services/rcon";
import { siteSettings } from "@/lib/services/site-settings";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { satisfiesCapability } from "../../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingErrorCode,
type HousekeepingResult,
ok,
} from "../../../foundation/contracts";
export type SystemMutationOperation =
| "access.rank.create"
| "access.rank.delete"
| "access.rank.update"
| "access.permissions.update"
| "access.permissions.repair"
| "configuration.settings.save"
| "configuration.setting.create"
| "configuration.setting.update"
| "configuration.setting.delete"
| "configuration.emulator-setting.update"
| "configuration.emulator-text.update"
| "operations.alerts.mark-read"
| "operations.alert.broadcast"
| "rcon.update-catalog"
| "rcon.update-word-filter"
| "rcon.update-navigator"
| "rcon.hotel-alert"
| "rcon.disconnect-user"
| "rcon.alert-user"
| "rcon.forward-user"
| "rcon.give-credits"
| "rcon.give-duckets"
| "rcon.give-diamonds"
| "rcon.give-badge"
| "rcon.set-motto"
| "rcon.set-rank"
| "rcon.execute-command"
| "rcon.send-gift"
| "operations.maintenance.update";
export interface SystemMutationContext {
readonly capability: HousekeepingCapabilityContext;
readonly correlationId: string;
}
export interface SystemMutationAdapter {
execute(
operation: SystemMutationOperation,
input: unknown,
context: SystemMutationContext,
): Promise<unknown>;
}
export interface SystemMutationService {
execute(
context: SystemMutationContext,
operation: SystemMutationOperation,
input: unknown,
): Promise<HousekeepingResult<unknown>>;
}
class SystemMutationFailure extends Error {
constructor(
readonly code: HousekeepingErrorCode,
readonly messageKey: string,
readonly fieldErrors?: Readonly<Record<string, readonly string[]>>,
) {
super(messageKey);
this.name = "SystemMutationFailure";
}
}
function operationCapability(operation: SystemMutationOperation) {
if (operation.startsWith("access.")) {
return anyCapability(PERMS.PERMISSIONS_MANAGE);
}
if (operation.startsWith("configuration.")) {
return anyCapability(PERMS.SETTINGS_EDIT);
}
if (operation === "operations.alerts.mark-read") {
return anyCapability(PERMS.NOTIFICATIONS_VIEW);
}
if (operation === "operations.alert.broadcast") {
return anyCapability(PERMS.NOTIFICATIONS_EDIT);
}
if (operation.startsWith("rcon.")) {
return anyCapability(PERMS.RCON_EXECUTE);
}
return anyCapability(PERMS.SETTINGS_EDIT);
}
export function createSystemMutationService(
adapter: SystemMutationAdapter,
): SystemMutationService {
return {
async execute(context, operation, input) {
if (
!satisfiesCapability(context.capability, operationCapability(operation))
) {
return fail(
"FORBIDDEN",
"errors.housekeeping.forbidden",
context.correlationId,
);
}
try {
return ok(
await adapter.execute(operation, input, context),
context.correlationId,
);
} catch (error) {
if (error instanceof SystemMutationFailure) {
return fail(
error.code,
error.messageKey,
context.correlationId,
error.fieldErrors,
);
}
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
context.correlationId,
);
}
},
};
}
function record(input: unknown): Record<string, unknown> {
if (typeof input !== "object" || input === null || Array.isArray(input)) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return input as Record<string, unknown>;
}
function text(value: unknown, maxLength: number, trim = true): string {
const normalized = String(value ?? "").normalize("NFC");
return (trim ? normalized.trim() : normalized).slice(0, maxLength);
}
function positiveInteger(value: unknown): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return parsed;
}
function normalizeSettingValue(key: string, value: string): string {
return key === HABBO_GAMEDATA_HOTEL_SETTING_KEY
? normalizeHabboGamedataHotel(value)
: value;
}
function bustGamedataCachesIfNeeded(key: string): void {
if (key !== HABBO_GAMEDATA_HOTEL_SETTING_KEY) return;
clearOfficialHabboFurnidataCache();
clearBadgeCache();
}
async function requireRcon(result: boolean): Promise<void> {
if (!result) {
throw new SystemMutationFailure(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
);
}
}
async function upsertWebsiteSetting(
key: string,
value: string,
comment?: string | null,
): Promise<void> {
await db
.insert(WebsiteSetting)
.values({ key, value, ...(comment === undefined ? {} : { comment }) })
.onDuplicateKeyUpdate({ set: { value } });
}
async function executeAccessMutation(
operation: Extract<SystemMutationOperation, `access.${string}`>,
input: unknown,
context: SystemMutationContext,
): Promise<unknown> {
const data = record(input);
if (operation === "access.rank.create") {
const name = text(data.name, 25);
const level = positiveInteger(data.level);
const id = await createEmulatorRank(db, { rank_name: name, level });
await db
.insert(AclRole)
.values({
slug: `rank_${id}`,
title: name,
description: "CMS role synchronized from permission_ranks",
})
.onDuplicateKeyUpdate({ set: { title: name } });
await logStaffActivity({
staffId: context.capability.actor.id,
action: "rank_create",
description: `Created rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
return { id };
}
const id = positiveInteger(data.id);
if (operation === "access.rank.delete") {
const [userCount] = await db
.select({ total: count() })
.from(User)
.where(eq(User.rank, id));
const users = Number(userCount?.total ?? 0);
if (users > 0) {
throw new SystemMutationFailure(
"CONFLICT",
"errors.housekeeping.system.rankInUse",
{ rank: [String(users)] },
);
}
const [role] = await db
.select({ id: AclRole.id })
.from(AclRole)
.where(eq(AclRole.slug, `rank_${id}`))
.limit(1);
await deleteEmulatorRank(db, id);
if (role) {
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
await tx.delete(AclModelRole).where(eq(AclModelRole.roleId, role.id));
await tx.delete(AclRole).where(eq(AclRole.id, role.id));
});
}
await logStaffActivity({
staffId: context.capability.actor.id,
action: "rank_delete",
description: `Deleted rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
return null;
}
if (operation === "access.rank.update") {
const fields = record(data.fields);
const normalizedFields = Object.fromEntries(
Object.entries(fields).flatMap(([key, value]) =>
typeof value === "string" || typeof value === "number"
? [[key, value] as const]
: [],
),
);
await updateEmulatorRank(db, id, normalizedFields);
if (typeof normalizedFields.rank_name === "string") {
await db
.update(AclRole)
.set({ title: normalizedFields.rank_name })
.where(eq(AclRole.slug, `rank_${id}`));
}
await logStaffActivity({
staffId: context.capability.actor.id,
action: "rank_update",
description: `Updated rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
return null;
}
if (operation === "access.permissions.update") {
const roleId = positiveInteger(data.roleId);
const slugs = Array.isArray(data.permissionSlugs)
? data.permissionSlugs.map((slug) => text(slug, 160)).filter(Boolean)
: [];
const [role] = await db
.select({ id: AclRole.id, slug: AclRole.slug })
.from(AclRole)
.where(eq(AclRole.id, roleId))
.limit(1);
if (!role) {
throw new SystemMutationFailure(
"NOT_FOUND",
"errors.housekeeping.system.roleNotFound",
);
}
const permissions = slugs.length
? await db
.select({ id: AclPermission.id })
.from(AclPermission)
.where(inArray(AclPermission.slug, slugs))
: [];
await db.transaction(async (tx) => {
await tx
.delete(AclModelPermission)
.where(
and(
eq(AclModelPermission.modelId, role.id),
eq(AclModelPermission.modelType, "Role"),
),
);
if (permissions.length) {
await tx.insert(AclModelPermission).values(
permissions.map((permission) => ({
modelId: role.id,
modelType: "Role",
permissionId: permission.id,
})),
);
}
});
await logStaffActivity({
staffId: context.capability.actor.id,
action: "acl_role_permissions_update",
description: `Updated ${permissions.length} permissions for ${role.slug}`,
targetType: "acl_role",
targetId: role.id,
});
return { updated: permissions.length };
}
const [dashboardFillResult] = await db.execute(sql`
INSERT INTO acl_model_permissions (model_type, model_id, permission_id)
SELECT 'Role', ar.id, ap.id
FROM acl_roles ar
JOIN acl_permissions ap ON ap.slug LIKE 'admin.%'
WHERE EXISTS (
SELECT 1 FROM acl_model_permissions amp
JOIN acl_permissions apdash ON apdash.id = amp.permission_id
WHERE amp.model_type = 'Role' AND amp.model_id = ar.id
AND apdash.slug = 'admin.dashboard'
)
AND NOT EXISTS (
SELECT 1 FROM acl_model_permissions amp2
WHERE amp2.model_type = 'Role' AND amp2.model_id = ar.id
AND amp2.permission_id = ap.id
)
`);
const [midRankViewsResult] = await db.execute(sql`
INSERT INTO acl_model_permissions (model_type, model_id, permission_id)
SELECT 'Role', ar.id, ap.id
FROM permission_ranks pr
JOIN acl_roles ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN acl_permissions ap ON (
ap.slug = 'admin.dashboard'
OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view')
)
WHERE pr.id >= 6
AND NOT EXISTS (
SELECT 1
FROM acl_model_permissions amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const [highRankToolsResult] = await db.execute(sql`
INSERT INTO acl_model_permissions (model_type, model_id, permission_id)
SELECT 'Role', ar.id, ap.id
FROM permission_ranks pr
JOIN acl_roles ar ON ar.slug = CONCAT('rank_', pr.id)
JOIN acl_permissions ap ON (
(ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit')
OR ap.slug IN (
'admin.permissions.manage',
'admin.rcon.execute',
'admin.assets.import',
'admin.export',
'admin.analytics.export',
'admin.users.ban',
'admin.users.reset_password',
'admin.room.delete'
)
)
WHERE pr.id >= 7
AND NOT EXISTS (
SELECT 1
FROM acl_model_permissions amp
WHERE amp.model_type = 'Role'
AND amp.model_id = ar.id
AND amp.permission_id = ap.id
)
`);
const inserted =
Number((dashboardFillResult as ResultSetHeader).affectedRows) +
Number((midRankViewsResult as ResultSetHeader).affectedRows) +
Number((highRankToolsResult as ResultSetHeader).affectedRows);
await logStaffActivity({
staffId: context.capability.actor.id,
action: "acl_nav_grants_repair",
description: `Repaired admin nav ACL grants (${inserted} rows inserted)`,
targetType: "acl",
targetId: 0,
});
return { inserted };
}
async function executeConfigurationMutation(
operation: Extract<SystemMutationOperation, `configuration.${string}`>,
input: unknown,
): Promise<unknown> {
const data = record(input);
if (operation === "configuration.settings.save") {
const settings = record(data.settings);
const allowedKeys = new Set<string>(MANAGED_SETTING_KEYS);
const entries = Object.entries(settings).flatMap(([key, value]) =>
allowedKeys.has(key) && typeof value === "string"
? [[key, normalizeSettingValue(key, value)] as const]
: [],
);
await Promise.all(
entries.map(([key, value]) => upsertWebsiteSetting(key, value)),
);
await siteSettings.reload();
if (entries.some(([key]) => key === HABBO_GAMEDATA_HOTEL_SETTING_KEY)) {
clearOfficialHabboFurnidataCache();
clearBadgeCache();
}
return { saved: entries.length };
}
if (operation === "configuration.emulator-setting.update") {
const key = text(data.key, 100);
if (!key) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
const value = text(data.value, 512, false);
await db
.insert(EmulatorSettings)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } });
return null;
}
if (operation === "configuration.emulator-text.update") {
const key = text(data.key, 100);
if (!key) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
const value = text(data.value, 4096, false);
await db
.insert(EmulatorTexts)
.values({ key, value })
.onDuplicateKeyUpdate({ set: { value } });
return null;
}
const key = text(data.key, 255);
if (!key) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
if (operation === "configuration.setting.delete") {
await db.delete(WebsiteSetting).where(eq(WebsiteSetting.key, key));
await siteSettings.reload();
bustGamedataCachesIfNeeded(key);
return null;
}
const value = normalizeSettingValue(key, text(data.value, 65_535, false));
const comment =
operation === "configuration.setting.create"
? text(data.comment, 255) || null
: undefined;
await upsertWebsiteSetting(key, value, comment);
await siteSettings.reload();
bustGamedataCachesIfNeeded(key);
return null;
}
async function executeRconMutation(
operation: Extract<SystemMutationOperation, `rcon.${string}`>,
input: unknown,
context: SystemMutationContext,
): Promise<unknown> {
const data = record(input);
switch (operation) {
case "rcon.update-catalog":
await requireRcon(await rcon.updateCatalog());
break;
case "rcon.update-word-filter":
await requireRcon(await rcon.updateWordFilter());
break;
case "rcon.update-navigator":
await requireRcon(await rcon.send("updatenavigator", null));
break;
case "rcon.hotel-alert":
await requireRcon(
await rcon.send("hotelalert", { message: text(data.message, 512) }),
);
break;
case "rcon.disconnect-user":
await requireRcon(
await rcon.disconnectUser(
positiveInteger(data.userId),
text(data.username, 255),
),
);
break;
case "rcon.alert-user":
await requireRcon(
await rcon.alertUser(
positiveInteger(data.userId),
text(data.message, 512),
),
);
break;
case "rcon.forward-user":
await requireRcon(
await rcon.forwardUser(
positiveInteger(data.userId),
positiveInteger(data.roomId),
),
);
break;
case "rcon.give-credits":
await requireRcon(
await rcon.giveCredits(
positiveInteger(data.userId),
positiveInteger(data.amount),
),
);
break;
case "rcon.give-duckets":
await requireRcon(
await rcon.giveDuckets(
positiveInteger(data.userId),
positiveInteger(data.amount),
),
);
break;
case "rcon.give-diamonds":
await requireRcon(
await rcon.giveDiamonds(
positiveInteger(data.userId),
positiveInteger(data.amount),
),
);
break;
case "rcon.give-badge":
await requireRcon(
await rcon.giveBadge(
positiveInteger(data.userId),
text(data.badge, 32),
),
);
break;
case "rcon.set-motto":
await requireRcon(
await rcon.setMotto(
positiveInteger(data.userId),
text(data.motto, 127),
),
);
break;
case "rcon.set-rank": {
const userId = positiveInteger(data.userId);
const rank = positiveInteger(data.rank);
const [target] = await db
.select({ rank: User.rank })
.from(User)
.where(eq(User.id, userId))
.limit(1);
if (!target) {
throw new SystemMutationFailure(
"NOT_FOUND",
"errors.housekeeping.system.userNotFound",
);
}
let rankRows: { id: number }[] = [];
try {
const [rows] = await db.execute(
sql`SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1`,
);
rankRows = rows as unknown as { id: number }[];
} catch {
rankRows = [];
}
if (rankRows.length === 0) {
throw new SystemMutationFailure(
"NOT_FOUND",
"errors.housekeeping.system.rankNotFound",
);
}
if (!context.capability.isSuperAdmin) {
const actorRank = context.capability.actor.rank;
if (target.rank >= actorRank) {
throw new SystemMutationFailure(
"FORBIDDEN",
"errors.housekeeping.system.cannotChangePeerRank",
);
}
if (rank >= actorRank) {
throw new SystemMutationFailure(
"FORBIDDEN",
"errors.housekeeping.system.cannotAssignPeerRank",
);
}
}
await requireRcon(await rcon.setRank(userId, rank));
await db.update(User).set({ rank }).where(eq(User.id, userId));
break;
}
case "rcon.execute-command":
await requireRcon(
await rcon.executeCommand(
positiveInteger(data.userId),
text(data.command, 100),
),
);
break;
case "rcon.send-gift":
await requireRcon(
await rcon.sendGift(
positiveInteger(data.userId),
positiveInteger(data.itemId),
text(data.message || "Here is a gift.", 255) || "Here is a gift.",
),
);
break;
}
return null;
}
const systemProductionMutationAdapter: SystemMutationAdapter = {
async execute(operation, input, context) {
if (operation.startsWith("access.")) {
return executeAccessMutation(
operation as Extract<SystemMutationOperation, `access.${string}`>,
input,
context,
);
}
if (operation.startsWith("configuration.")) {
return executeConfigurationMutation(
operation as Extract<
SystemMutationOperation,
`configuration.${string}`
>,
input,
);
}
if (operation.startsWith("rcon.")) {
return executeRconMutation(
operation as Extract<SystemMutationOperation, `rcon.${string}`>,
input,
context,
);
}
if (operation === "operations.alerts.mark-read") {
await db
.update(AlertLogs)
.set({ isRead: true, updatedAt: new Date() })
.where(eq(AlertLogs.isRead, false));
return null;
}
if (operation === "operations.alert.broadcast") {
const message = text(record(input).message, 1000);
if (!message) {
throw new SystemMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
await requireRcon(await rcon.send("hotelalert", { message }));
return { delivered: true };
}
const data = record(input);
const enabled = Boolean(data.enabled);
const message = text(data.message, 65_535, false);
const parsedRank = Number(data.minimumLoginRank);
const minimumLoginRank =
Number.isInteger(parsedRank) && parsedRank >= 0 ? parsedRank : 5;
const rows = [
[
"maintenance_enabled",
enabled ? "1" : "0",
"Determines whether maintenance is enabled or not",
],
[
"maintenance_message",
message,
"The maintenance message displayed to users while maintenance is activated",
],
[
"min_maintenance_login_rank",
String(minimumLoginRank),
"The minimum rank required to login to the hotel during maintenance",
],
] as const;
for (const [key, value, comment] of rows) {
await upsertWebsiteSetting(key, value, comment);
}
await siteSettings.reload();
return null;
},
};
export const systemMutationService = createSystemMutationService(
systemProductionMutationAdapter,
);
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { SYSTEM_COMMAND_IDS } from "../../domains/system/commands/system-commands";
import { anyCapability, ok } from "../contracts";
import {
defineHousekeepingCommands,
@@ -7,6 +8,7 @@ import {
registerHousekeepingCommands,
} from "./bootstrap";
import {
getHousekeepingCommand,
type HousekeepingCommand,
registerHousekeepingCommand,
} from "./registry";
@@ -55,6 +57,9 @@ describe("housekeeping command bootstrap", () => {
});
it("registers the complete current list and seals during module initialization", () => {
expect(housekeepingCommandRegistryReady).toBe(true);
expect(
SYSTEM_COMMAND_IDS.map((id) => getHousekeepingCommand(id)?.id),
).toEqual(SYSTEM_COMMAND_IDS);
expect(() =>
registerHousekeepingCommand({
id: "system.bootstrap.too-late",
@@ -1,5 +1,6 @@
import "server-only";
import { SYSTEM_COMMANDS } from "../../domains/system/commands/system-commands";
import type { HousekeepingCommand } from "./registry";
import {
registerHousekeepingCommand,
@@ -34,7 +35,9 @@ export function registerHousekeepingCommands<
}
}
const currentHousekeepingCommands = defineHousekeepingCommands();
const currentHousekeepingCommands = defineHousekeepingCommands(
...SYSTEM_COMMANDS,
);
registerHousekeepingCommands(currentHousekeepingCommands);
sealHousekeepingCommandRegistry();
@@ -4,6 +4,7 @@ import { join, posix } from "node:path";
import { createElement, type ReactElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { SYSTEM_ROUTE_IDS } from "../domains/system/routes";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
import { discoverLegacyPages } from "../migration/discover-legacy-pages";
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../migration/matrix";
@@ -17,6 +18,81 @@ const SERVER_CAPABILITY_CONTEXT =
"src/features/housekeeping/foundation/server-capability-context.ts";
const PERMISSIONS_ADAPTER = "src/lib/permissions";
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
const approvedSystemRuntimeImports = new Map<string, ReadonlySet<string>>([
[
"src/features/housekeeping/domains/system/commands/system-commands.ts",
new Set(["src/features/housekeeping/domains/system/services/mutations"]),
],
[
"src/features/housekeeping/domains/system/pages/access.tsx",
new Set(["src/features/housekeeping/domains/system/queries/access"]),
],
[
"src/features/housekeeping/domains/system/pages/configuration.tsx",
new Set(["src/features/housekeeping/domains/system/queries/configuration"]),
],
[
"src/features/housekeeping/domains/system/pages/observability.tsx",
new Set(["src/features/housekeeping/domains/system/queries/observability"]),
],
[
"src/features/housekeeping/domains/system/pages/operations.tsx",
new Set(["src/features/housekeeping/domains/system/queries/operations"]),
],
[
"src/features/housekeeping/domains/system/queries/access.ts",
new Set(["src/lib/db", "drizzle-orm"]),
],
[
"src/features/housekeeping/domains/system/queries/configuration.ts",
new Set(["src/lib/db", "drizzle-orm"]),
],
[
"src/features/housekeeping/domains/system/queries/observability.ts",
new Set(["src/lib/db", "drizzle-orm"]),
],
[
"src/features/housekeeping/domains/system/queries/operations.ts",
new Set([
"src/features/housekeeping/domains/system/queries/observability",
"src/lib/db",
"drizzle-orm",
]),
],
[
"src/features/housekeeping/domains/system/services/mutations.ts",
new Set([
"src/app/admin/settings/cms-settings-config",
"src/lib/db",
"drizzle-orm",
"mysql2",
]),
],
[
"src/features/housekeeping/domains/system/manifest.ts",
new Set(["src/features/housekeeping/domains/system/routes"]),
],
[
"src/features/housekeeping/domains/system/route-handlers.ts",
new Set([
"src/features/housekeeping/domains/system/pages/access",
"src/features/housekeeping/domains/system/pages/configuration",
"src/features/housekeeping/domains/system/pages/observability",
"src/features/housekeeping/domains/system/pages/operations",
"src/features/housekeeping/domains/system/routes",
]),
],
[
"src/features/housekeeping/foundation/commands/bootstrap.ts",
new Set([
"src/features/housekeeping/domains/system/commands/system-commands",
]),
],
[
"src/features/housekeeping/route-handlers.ts",
new Set(["src/features/housekeeping/domains/system/route-handlers"]),
],
]);
const forbiddenModuleRoots = [
"src/lib/db",
"src/lib/db-pool",
@@ -358,6 +434,17 @@ function isAllowedPermissionSetTypeImport(
);
}
function isApprovedSystemRuntimeImport(
canonical: CanonicalModuleSpecifier,
sourceFile: string,
): boolean {
const allowed = approvedSystemRuntimeImports.get(sourceFile);
return (
allowed !== undefined &&
canonical.candidates.some((candidate) => allowed.has(candidate))
);
}
function findHousekeepingImportBoundaryViolations(
source: string,
sourceFile: string,
@@ -370,6 +457,7 @@ function findHousekeepingImportBoundaryViolations(
if (canonical.violation) violations.push(canonical.violation);
if (isAllowedPermissionSetTypeImport(access, canonical, sourceFile))
continue;
if (isApprovedSystemRuntimeImport(canonical, sourceFile)) continue;
const forbiddenPath = canonical.candidates.find((candidate) =>
isForbiddenModulePath(candidate, sourceFile),
);
@@ -398,6 +486,33 @@ describe("housekeeping runtime import boundary", () => {
}
});
it("allows only the approved System vertical runtime edges", () => {
expect(
findHousekeepingImportBoundaryViolations(
'import { systemMutationService } from "../services/mutations";',
"src/features/housekeeping/domains/system/commands/system-commands.ts",
),
).toEqual([]);
expect(
findHousekeepingImportBoundaryViolations(
'import { db } from "@/lib/db";',
"src/features/housekeeping/domains/system/queries/access.ts",
),
).toEqual([]);
expect(
findHousekeepingImportBoundaryViolations(
'import { db } from "@/lib/db";',
"src/features/housekeeping/domains/system/commands/system-commands.ts",
),
).toEqual(["src/lib/db"]);
expect(
findHousekeepingImportBoundaryViolations(
'import service from "../system/services/mutations";',
"src/features/housekeeping/domains/people/manifest.ts",
),
).toEqual(["src/features/housekeeping/domains/system/services/mutations"]);
});
it.each([
[
"aliased database import",
@@ -561,7 +676,7 @@ describe("housekeeping foundation completion contracts", () => {
}
});
it("creates the real six-domain registry in locked order without workflows", () => {
it("creates the real six-domain registry with only the System routes enabled", () => {
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
expect(registry.domains.map((domain) => domain.id)).toEqual([
@@ -572,9 +687,16 @@ describe("housekeeping foundation completion contracts", () => {
"hotel",
"system",
]);
expect(registry.domains.every((domain) => domain.routes.length === 0)).toBe(
true,
);
expect(
registry.domains
.filter((domain) => domain.id !== "system")
.every((domain) => domain.routes.length === 0),
).toBe(true);
expect(
registry.domains
.find((domain) => domain.id === "system")
?.routes.map((route) => route.id),
).toEqual(SYSTEM_ROUTE_IDS);
});
it("keeps production preview disabled even when the flag is true", () => {
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import { SYSTEM_ROUTES } from "../domains/system/routes";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../migration/matrix";
import {
@@ -353,7 +354,9 @@ describe("housekeeping registry", () => {
labelKey: expected.labelKey,
descriptionKey: expected.descriptionKey,
});
expect(actual.routes).toEqual([]);
expect(actual.routes).toEqual(
expected.id === "system" ? SYSTEM_ROUTES : [],
);
expect(actual.searchProviders).toEqual([]);
expect(actual.inboxSources).toEqual([]);
expect(actual.widgets).toEqual([]);
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { SYSTEM_ROUTE_IDS } from "./domains/system/routes";
import { createHousekeepingRegistry } from "./foundation/registry";
import { HOUSEKEEPING_MANIFESTS } from "./manifests";
import { HOUSEKEEPING_ROUTE_HANDLERS } from "./route-handlers";
@@ -15,6 +16,6 @@ describe("housekeeping route handlers", () => {
expect(new Set(handlerIds).size).toBe(handlerIds.length);
expect([...handlerIds].sort()).toEqual([...routeIds].sort());
expect(handlerIds).toEqual([]);
expect(handlerIds).toEqual(SYSTEM_ROUTE_IDS);
});
});
+2 -1
View File
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import { SYSTEM_ROUTE_HANDLERS } from "./domains/system/route-handlers";
import type { HousekeepingCapabilityContext } from "./foundation/contracts";
import type { HousekeepingRouteMatch } from "./foundation/routing/match-route";
@@ -13,4 +14,4 @@ export interface HousekeepingRouteHandler {
}
export const HOUSEKEEPING_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
Object.freeze([]);
Object.freeze([...SYSTEM_ROUTE_HANDLERS]);
+17 -6
View File
@@ -3,12 +3,23 @@ import { describe, expect, it } from "vitest";
describe("ACL management contract", () => {
it("uses normalized ACL persistence and the permissions.manage guard", () => {
const source = readFileSync("src/actions/permissions.ts", "utf8");
expect(source).toContain("PERMS.PERMISSIONS_MANAGE");
expect(source).toContain("adminAction");
expect(source).toContain("AclModelPermission");
expect(source).not.toContain("websiteHousekeepingPermissions");
expect(source).not.toContain("websiteTeams");
const wrapper = readFileSync("src/actions/permissions.ts", "utf8");
const service = readFileSync(
"src/features/housekeeping/domains/system/services/mutations.ts",
"utf8",
);
expect(wrapper).toContain("PERMS.PERMISSIONS_MANAGE");
expect(wrapper).toContain("adminAction");
expect(wrapper).toContain("access.permissions.update");
expect(wrapper).toContain("access.permissions.repair");
expect(service).toContain('import "server-only"');
expect(service).toContain("AclModelPermission");
expect(service).not.toContain(
"export const systemProductionMutationAdapter",
);
expect(service).not.toContain("legacyMessage");
expect(service).not.toContain("websiteHousekeepingPermissions");
expect(service).not.toContain("websiteTeams");
});
it("ships an idempotent ACL completion migration", () => {