fix: test server registration gaps, Redis subscriber cleanup, atomic settings upsert

This commit is contained in:
SnapOtter
2026-06-14 14:30:35 +08:00
parent 29dd675f42
commit 954cfb01a6
6 changed files with 66 additions and 42 deletions
+2 -14
View File
@@ -27,6 +27,7 @@ import { eq, lt } from "drizzle-orm";
import type { FastifyBaseLogger } from "fastify";
import { env } from "../config.js";
import { db, schema } from "../db/index.js";
import { upsertSetting } from "../lib/settings-helpers.js";
type ArchivalState = "PENDING" | "EXPORTING" | "EXPORTED" | "PURGING" | "COMPLETE";
@@ -40,7 +41,7 @@ interface ArchivalRun {
checksum: string;
}
// -- Settings helpers (same pattern as siem-forward.ts) ------------------------
// -- Settings helpers ----------------------------------------------------------
async function readSettingValue(key: string): Promise<string | null> {
const [row] = await db
@@ -50,19 +51,6 @@ async function readSettingValue(key: string): Promise<string | null> {
return row?.value ?? null;
}
async function upsertSetting(key: string, value: string): Promise<void> {
const [existing] = await db.select().from(schema.settings).where(eq(schema.settings.key, key));
if (existing) {
await db
.update(schema.settings)
.set({ value, updatedAt: new Date() })
.where(eq(schema.settings.key, key));
} else {
await db.insert(schema.settings).values({ key, value });
}
}
async function deleteSetting(key: string): Promise<void> {
await db.delete(schema.settings).where(eq(schema.settings.key, key));
}
+1 -13
View File
@@ -18,6 +18,7 @@ import { asc, eq, gte } from "drizzle-orm";
import { env } from "../config.js";
import { db, schema } from "../db/index.js";
import { decrypt, isEncrypted } from "../lib/encryption.js";
import { upsertSetting } from "../lib/settings-helpers.js";
import { deliverWebhook } from "../lib/webhook-delivery.js";
import { readSiemConfig } from "../routes/enterprise/siem.js";
@@ -34,19 +35,6 @@ async function readSettingValue(key: string): Promise<string | null> {
return row?.value ?? null;
}
async function upsertSetting(key: string, value: string): Promise<void> {
const [existing] = await db.select().from(schema.settings).where(eq(schema.settings.key, key));
if (existing) {
await db
.update(schema.settings)
.set({ value, updatedAt: new Date() })
.where(eq(schema.settings.key, key));
} else {
await db.insert(schema.settings).values({ key, value });
}
}
export async function runSiemForward(): Promise<{ forwarded: number } | void> {
// 1. Read SIEM config
const config = await readSiemConfig();
+14
View File
@@ -1,6 +1,20 @@
import { eq } from "drizzle-orm";
import { db, schema } from "../db/index.js";
/**
* Atomically insert or update a setting using Postgres ON CONFLICT DO UPDATE.
* Eliminates the TOCTOU race in the old SELECT-then-INSERT/UPDATE pattern.
*/
export async function upsertSetting(key: string, value: string): Promise<void> {
await db
.insert(schema.settings)
.values({ key, value })
.onConflictDoUpdate({
target: schema.settings.key,
set: { value, updatedAt: new Date() },
});
}
/**
* Read a numeric setting from the DB `settings` table.
* Returns `defaultValue` when the key is missing, non-numeric, or on DB error.
+11 -1
View File
@@ -140,7 +140,17 @@ export async function registerIpAllowlist(app: FastifyInstance): Promise<void> {
const sub = redis.duplicate();
await sub.subscribe(ALLOWLIST_CHANNEL);
sub.on("message", async () => {
await refreshAllowlist();
refreshAllowlist().catch(() => {});
});
// Clean up subscriber on shutdown
app.addHook("onClose", async () => {
try {
await sub.unsubscribe();
await sub.quit();
} catch {
// Best-effort cleanup
}
});
// Hook -- runs before auth, before routes
+1 -13
View File
@@ -4,7 +4,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { db, schema } from "../../db/index.js";
import { sharedRedis } from "../../jobs/connection.js";
import { auditLog } from "../../lib/audit.js";
import { getSettingString } from "../../lib/settings-helpers.js";
import { getSettingString, upsertSetting } from "../../lib/settings-helpers.js";
import { requirePermission } from "../../permissions.js";
import { hashPassword, verifyPassword } from "../../plugins/auth.js";
@@ -179,18 +179,6 @@ function scimListResponse(
// ── Route Registration ───────────────────────────────────────────
async function upsertSetting(key: string, value: string): Promise<void> {
const [existing] = await db.select().from(schema.settings).where(eq(schema.settings.key, key));
if (existing) {
await db
.update(schema.settings)
.set({ value, updatedAt: new Date() })
.where(eq(schema.settings.key, key));
} else {
await db.insert(schema.settings).values({ key, value });
}
}
export async function registerScimRoutes(app: FastifyInstance): Promise<void> {
// ── Token Management Endpoints ────────────────────────────────