feat(audit): локальный журнал и push в auth-portal
Таблица audit_log, recordAudit на CRUD, GET /api/v1/audit и dual-write source_app=cfdm. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -11,6 +11,8 @@ AUTH_JWT_SECRET=dev-secret-change-me
|
||||
# alias: JWT_SECRET=
|
||||
AUTH_ISSUER=https://auth.shnt.top
|
||||
AUTH_PORTAL_URL=http://localhost:5175
|
||||
# Shared with auth-portal AUDIT_INGEST_SECRET — dual-write audit to portal
|
||||
AUTH_AUDIT_INGEST_SECRET=dev-audit-ingest-secret
|
||||
JWT_TTL_HOURS=24
|
||||
|
||||
# Legacy local login (только при AUTH_REQUIRED=false)
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "./routes/domain-monitors.js";
|
||||
import { settingsRoutes } from "./routes/settings.js";
|
||||
import { integrationsVpsTrackerRoutes } from "./routes/integrations-vps-tracker.js";
|
||||
import { auditRoutes } from "./routes/audit.js";
|
||||
import * as certificateService from "./services/certificate-service.js";
|
||||
import * as healthCheckService from "./services/health-check-service.js";
|
||||
import * as serviceConfigService from "./services/service-config-service.js";
|
||||
@@ -83,6 +84,7 @@ export async function buildApp(opts: BuildAppOptions = {}) {
|
||||
await protectedApi.register(domainMonitorRoutes);
|
||||
await protectedApi.register(notificationRoutes);
|
||||
await protectedApi.register(settingsRoutes);
|
||||
await protectedApi.register(auditRoutes);
|
||||
},
|
||||
{ prefix: "/api/v1" },
|
||||
);
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface AppConfig {
|
||||
authRequired: boolean;
|
||||
authIssuer: string;
|
||||
authPortalUrl: string;
|
||||
/** Bearer secret for POST {authPortalUrl}/api/v1/ingest/audit */
|
||||
authAuditIngestSecret: string | null;
|
||||
}
|
||||
|
||||
function boolEnv(v: string | undefined, fallback: boolean): boolean {
|
||||
@@ -65,5 +67,8 @@ export function loadConfig(): AppConfig {
|
||||
process.env.VITE_AUTH_PORTAL_URL ??
|
||||
"http://localhost:5175"
|
||||
).replace(/\/$/, ""),
|
||||
authAuditIngestSecret:
|
||||
process.env.AUTH_AUDIT_INGEST_SECRET?.trim() ||
|
||||
(!isProd ? "dev-audit-ingest-secret" : null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { FastifyInstance, FastifyRequest } from "fastify";
|
||||
import { appendAudit, type AppendAuditInput } from "@cfdm/db";
|
||||
import { pushAuditEvents } from "../services/audit-portal-push.js";
|
||||
|
||||
export function clientIp(request: FastifyRequest): string | null {
|
||||
const forwarded = request.headers["x-forwarded-for"];
|
||||
if (typeof forwarded === "string" && forwarded.trim()) {
|
||||
return forwarded.split(",")[0]?.trim() ?? null;
|
||||
}
|
||||
return request.ip ?? null;
|
||||
}
|
||||
|
||||
export function actorFromRequest(
|
||||
request: FastifyRequest,
|
||||
): Pick<AppendAuditInput, "actorUserId" | "actorEmail" | "actorName"> {
|
||||
const u = request.authUser;
|
||||
if (u) {
|
||||
return {
|
||||
actorUserId: u.id,
|
||||
actorEmail: u.email || null,
|
||||
actorName: u.name || null,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = request.user;
|
||||
if (payload?.sub) {
|
||||
return {
|
||||
actorUserId: String(payload.sub),
|
||||
actorEmail: payload.email ? String(payload.email) : null,
|
||||
actorName: payload.name ? String(payload.name) : null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
actorUserId: null,
|
||||
actorEmail: null,
|
||||
actorName: null,
|
||||
};
|
||||
}
|
||||
|
||||
type RecordAuditInput = Omit<
|
||||
AppendAuditInput,
|
||||
| "sourceApp"
|
||||
| "eventId"
|
||||
| "ip"
|
||||
| "actorUserId"
|
||||
| "actorEmail"
|
||||
| "actorName"
|
||||
>;
|
||||
|
||||
/** Local append + optional async push to auth-portal ingest. */
|
||||
export function recordAudit(
|
||||
app: FastifyInstance,
|
||||
request: FastifyRequest,
|
||||
input: RecordAuditInput,
|
||||
): void {
|
||||
const eventId = randomUUID();
|
||||
const actor = actorFromRequest(request);
|
||||
const ip = clientIp(request);
|
||||
const createdAt = input.createdAt ?? new Date().toISOString();
|
||||
|
||||
const full: AppendAuditInput = {
|
||||
...input,
|
||||
...actor,
|
||||
eventId,
|
||||
sourceApp: "cfdm",
|
||||
ip,
|
||||
createdAt,
|
||||
};
|
||||
|
||||
try {
|
||||
appendAudit(app.db, full);
|
||||
} catch (err) {
|
||||
app.log.warn({ err }, "audit_log append failed");
|
||||
}
|
||||
|
||||
const secret = app.config.authAuditIngestSecret;
|
||||
const portalUrl = app.config.authPortalUrl;
|
||||
if (!secret || !portalUrl) return;
|
||||
|
||||
void pushAuditEvents(portalUrl, secret, [
|
||||
{
|
||||
event_id: eventId,
|
||||
source_app: "cfdm",
|
||||
action: input.action,
|
||||
severity: input.severity,
|
||||
actor_user_id: actor.actorUserId,
|
||||
actor_email: actor.actorEmail,
|
||||
actor_name: actor.actorName,
|
||||
target_type: input.targetType,
|
||||
target_id: input.targetId,
|
||||
summary: input.summary,
|
||||
details: input.details,
|
||||
ip,
|
||||
created_at: createdAt,
|
||||
},
|
||||
]).catch((err) => {
|
||||
app.log.warn({ err, eventId }, "audit portal push failed");
|
||||
});
|
||||
}
|
||||
@@ -118,6 +118,11 @@ const RULES: Rule[] = [
|
||||
p.startsWith("/api/v1/health-checks"),
|
||||
permission: "cfdm:settings:admin",
|
||||
},
|
||||
{
|
||||
methods: ["GET"],
|
||||
match: (p) => p.startsWith("/api/v1/audit"),
|
||||
permission: "cfdm:settings:admin",
|
||||
},
|
||||
];
|
||||
|
||||
/** Resolve required permission for method+path, or null if public / unknown. */
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { listAudit } from "@cfdm/db";
|
||||
import { auditListQuerySchema } from "@cfdm/shared";
|
||||
import { AppError } from "../errors.js";
|
||||
|
||||
export async function auditRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get("/audit", async (request) => {
|
||||
const parsed = auditListQuerySchema.safeParse(request.query);
|
||||
if (!parsed.success) {
|
||||
throw AppError.validation("Некорректные параметры запроса");
|
||||
}
|
||||
|
||||
const q = parsed.data;
|
||||
return listAudit(app.db, {
|
||||
action: q.action,
|
||||
severity: q.severity,
|
||||
userId: q.user_id,
|
||||
sourceApp: q.source_app,
|
||||
limit: q.limit,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import * as dnsService from "../services/dns-service.js";
|
||||
import { recordAudit } from "../lib/audit.js";
|
||||
|
||||
export async function dnsRoutes(app: FastifyInstance) {
|
||||
const createSchema = z.object({
|
||||
@@ -31,12 +32,25 @@ export async function dnsRoutes(app: FastifyInstance) {
|
||||
app.post("/domains/:id/dns", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = createSchema.parse(request.body);
|
||||
return dnsService.create(
|
||||
const record = await dnsService.create(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
Number(id),
|
||||
body,
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "dns.create",
|
||||
targetType: "app_resource",
|
||||
targetId: String(record.id),
|
||||
summary: `Создана DNS-запись ${record.name} (${record.record_type})`,
|
||||
details: {
|
||||
domain_id: Number(id),
|
||||
record_type: record.record_type,
|
||||
name: record.name,
|
||||
content: record.content,
|
||||
},
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
app.post("/domains/:id/dns/bulk", async (request) => {
|
||||
@@ -69,13 +83,21 @@ export async function dnsRoutes(app: FastifyInstance) {
|
||||
id: string;
|
||||
recordId: string;
|
||||
};
|
||||
return dnsService.update(
|
||||
const record = await dnsService.update(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
Number(id),
|
||||
Number(recordId),
|
||||
request.body as dnsService.UpdateDnsRequest,
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "dns.update",
|
||||
targetType: "app_resource",
|
||||
targetId: String(recordId),
|
||||
summary: `Обновлена DNS-запись ${record.name}`,
|
||||
details: { domain_id: Number(id), record_id: Number(recordId) },
|
||||
});
|
||||
return record;
|
||||
});
|
||||
|
||||
app.delete("/domains/:id/dns/:recordId", async (request) => {
|
||||
@@ -83,12 +105,25 @@ export async function dnsRoutes(app: FastifyInstance) {
|
||||
id: string;
|
||||
recordId: string;
|
||||
};
|
||||
const record = dnsService.get(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
Number(recordId),
|
||||
);
|
||||
await dnsService.deleteRecord(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
Number(id),
|
||||
Number(recordId),
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "dns.delete",
|
||||
severity: "warning",
|
||||
targetType: "app_resource",
|
||||
targetId: String(recordId),
|
||||
summary: `Удалена DNS-запись ${record.name}`,
|
||||
details: { domain_id: Number(id), name: record.name },
|
||||
});
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import { bulkUpdateDomainsSchema, updateDomainSchema } from "@cfdm/shared";
|
||||
import { z } from "zod";
|
||||
import * as domainService from "../services/domain-service.js";
|
||||
import { recordAudit } from "../lib/audit.js";
|
||||
|
||||
export async function domainRoutes(app: FastifyInstance) {
|
||||
const createSchema = z.object({
|
||||
@@ -17,12 +18,20 @@ export async function domainRoutes(app: FastifyInstance) {
|
||||
|
||||
app.post("/domains", async (request) => {
|
||||
const body = createSchema.parse(request.body);
|
||||
return domainService.createDomain(
|
||||
const domain = await domainService.createDomain(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
body.group_id ?? null,
|
||||
body.zone_name,
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "domain.create",
|
||||
targetType: "app_resource",
|
||||
targetId: String(domain.id),
|
||||
summary: `Добавлен домен ${domain.zone_name}`,
|
||||
details: { zone_name: domain.zone_name, group_id: domain.group_id },
|
||||
});
|
||||
return domain;
|
||||
});
|
||||
|
||||
app.post("/domains/bulk", async (request) => {
|
||||
@@ -47,12 +56,29 @@ export async function domainRoutes(app: FastifyInstance) {
|
||||
app.patch("/domains/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = updateDomainSchema.parse(request.body);
|
||||
return domainService.updateDomain(request.server.db, Number(id), body);
|
||||
const domain = domainService.updateDomain(request.server.db, Number(id), body);
|
||||
recordAudit(request.server, request, {
|
||||
action: "domain.update",
|
||||
targetType: "app_resource",
|
||||
targetId: String(domain.id),
|
||||
summary: `Обновлён домен ${domain.zone_name}`,
|
||||
details: body,
|
||||
});
|
||||
return domain;
|
||||
});
|
||||
|
||||
app.delete("/domains/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const domain = domainService.getDomain(request.server.db, Number(id));
|
||||
domainService.deleteDomain(request.server.db, Number(id));
|
||||
recordAudit(request.server, request, {
|
||||
action: "domain.delete",
|
||||
severity: "warning",
|
||||
targetType: "app_resource",
|
||||
targetId: String(id),
|
||||
summary: `Удалён домен ${domain.zone_name}`,
|
||||
details: { zone_name: domain.zone_name },
|
||||
});
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { repos } from "@cfdm/db";
|
||||
import * as groupService from "../services/group-service.js";
|
||||
import { recordAudit } from "../lib/audit.js";
|
||||
|
||||
export async function groupRoutes(app: FastifyInstance) {
|
||||
const bodySchema = z.object({
|
||||
@@ -14,7 +16,15 @@ export async function groupRoutes(app: FastifyInstance) {
|
||||
|
||||
app.post("/groups", async (request) => {
|
||||
const body = bodySchema.parse(request.body);
|
||||
return groupService.createGroup(request.server.db, body.name, body.slug);
|
||||
const group = groupService.createGroup(request.server.db, body.name, body.slug);
|
||||
recordAudit(request.server, request, {
|
||||
action: "group.create",
|
||||
targetType: "app_resource",
|
||||
targetId: String(group.id),
|
||||
summary: `Создана группа «${group.name}»`,
|
||||
details: { name: group.name, slug: group.slug },
|
||||
});
|
||||
return group;
|
||||
});
|
||||
|
||||
app.get("/groups/:id", async (request) => {
|
||||
@@ -25,17 +35,34 @@ export async function groupRoutes(app: FastifyInstance) {
|
||||
app.patch("/groups/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = bodySchema.parse(request.body);
|
||||
return groupService.updateGroup(
|
||||
const group = groupService.updateGroup(
|
||||
request.server.db,
|
||||
Number(id),
|
||||
body.name,
|
||||
body.slug,
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "group.update",
|
||||
targetType: "app_resource",
|
||||
targetId: String(group.id),
|
||||
summary: `Обновлена группа «${group.name}»`,
|
||||
details: { name: group.name, slug: group.slug },
|
||||
});
|
||||
return group;
|
||||
});
|
||||
|
||||
app.delete("/groups/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const group = repos.getGroup(request.server.db, Number(id));
|
||||
groupService.deleteGroup(request.server.db, Number(id));
|
||||
recordAudit(request.server, request, {
|
||||
action: "group.delete",
|
||||
severity: "warning",
|
||||
targetType: "app_resource",
|
||||
targetId: String(id),
|
||||
summary: `Удалена группа «${group.name}»`,
|
||||
details: { name: group.name, slug: group.slug },
|
||||
});
|
||||
return { deleted: true };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { reorderServicesSchema, updateServiceConfigSchema } from "@cfdm/shared";
|
||||
import { repos } from "@cfdm/db";
|
||||
import * as serviceConfig from "../services/service-config-service.js";
|
||||
import { recordAudit } from "../lib/audit.js";
|
||||
|
||||
export async function serviceRoutes(app: FastifyInstance) {
|
||||
const createSchema = z.object({
|
||||
@@ -39,7 +40,15 @@ export async function serviceRoutes(app: FastifyInstance) {
|
||||
body.service_group_id,
|
||||
);
|
||||
}
|
||||
return serviceConfig.getView(request.server.db, service.id);
|
||||
const view = serviceConfig.getView(request.server.db, service.id);
|
||||
recordAudit(request.server, request, {
|
||||
action: "service.create",
|
||||
targetType: "app_resource",
|
||||
targetId: String(service.id),
|
||||
summary: `Создан сервис «${service.name}»`,
|
||||
details: { name: service.name, slug: service.slug },
|
||||
});
|
||||
return view;
|
||||
});
|
||||
|
||||
app.get("/services/:id", async (request) => {
|
||||
@@ -50,17 +59,34 @@ export async function serviceRoutes(app: FastifyInstance) {
|
||||
app.patch("/services/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const body = updateServiceConfigSchema.parse(request.body);
|
||||
return serviceConfig.updateConfig(
|
||||
const view = await serviceConfig.updateConfig(
|
||||
request.server.db,
|
||||
request.server.cf,
|
||||
Number(id),
|
||||
body,
|
||||
);
|
||||
recordAudit(request.server, request, {
|
||||
action: "service.update",
|
||||
targetType: "app_resource",
|
||||
targetId: String(id),
|
||||
summary: `Обновлён сервис «${view.name}»`,
|
||||
details: body,
|
||||
});
|
||||
return view;
|
||||
});
|
||||
|
||||
app.delete("/services/:id", async (request) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const view = serviceConfig.getView(request.server.db, Number(id));
|
||||
repos.deleteService(request.server.db, Number(id));
|
||||
recordAudit(request.server, request, {
|
||||
action: "service.delete",
|
||||
severity: "warning",
|
||||
targetType: "app_resource",
|
||||
targetId: String(id),
|
||||
summary: `Удалён сервис «${view.name}»`,
|
||||
details: { name: view.name, slug: view.slug },
|
||||
});
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { request } from "undici";
|
||||
import type { IngestAuditEvent } from "@cfdm/shared";
|
||||
|
||||
export async function pushAuditEvents(
|
||||
portalUrl: string,
|
||||
secret: string,
|
||||
events: IngestAuditEvent[],
|
||||
): Promise<void> {
|
||||
const base = portalUrl.replace(/\/$/, "");
|
||||
const res = await request(`${base}/api/v1/ingest/audit`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Bearer ${secret}`,
|
||||
},
|
||||
body: JSON.stringify({ events }),
|
||||
});
|
||||
|
||||
if (res.statusCode >= 400) {
|
||||
const body = await res.body.text();
|
||||
throw new Error(`portal audit ingest ${res.statusCode}: ${body}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import * as auditPortalPush from "../src/services/audit-portal-push.js";
|
||||
|
||||
describe("audit log", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("GET /api/v1/audit returns local entries", async () => {
|
||||
const pushSpy = vi
|
||||
.spyOn(auditPortalPush, "pushAuditEvents")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const app = await buildApp({
|
||||
config: {
|
||||
...loadConfig(),
|
||||
staticDir: null,
|
||||
authAuditIngestSecret: null,
|
||||
},
|
||||
memory: true,
|
||||
});
|
||||
|
||||
const login = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/auth/login",
|
||||
payload: { username: "admin", password: "admin" },
|
||||
});
|
||||
expect(login.statusCode).toBe(200);
|
||||
const token = (login.json() as { token: string }).token;
|
||||
|
||||
const create = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/groups",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: "Audit Test", slug: "audit-test" },
|
||||
});
|
||||
expect(create.statusCode).toBe(200);
|
||||
|
||||
const list = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/audit?action=group.create",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(list.statusCode).toBe(200);
|
||||
const entries = list.json() as { action: string; source_app: string }[];
|
||||
expect(entries.length).toBeGreaterThanOrEqual(1);
|
||||
expect(entries[0]?.action).toBe("group.create");
|
||||
expect(entries[0]?.source_app).toBe("cfdm");
|
||||
|
||||
expect(pushSpy).not.toHaveBeenCalled();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("recordAudit pushes to portal when secret configured", async () => {
|
||||
const pushSpy = vi
|
||||
.spyOn(auditPortalPush, "pushAuditEvents")
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const app = await buildApp({
|
||||
config: {
|
||||
...loadConfig(),
|
||||
staticDir: null,
|
||||
authPortalUrl: "http://portal.test",
|
||||
authAuditIngestSecret: "test-ingest-secret",
|
||||
},
|
||||
memory: true,
|
||||
});
|
||||
|
||||
const login = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/auth/login",
|
||||
payload: { username: "admin", password: "admin" },
|
||||
});
|
||||
const token = (login.json() as { token: string }).token;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/groups",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: "Portal Push", slug: "portal-push" },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(pushSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
const [portalUrl, secret, events] = pushSpy.mock.calls[0]!;
|
||||
expect(portalUrl).toBe("http://portal.test");
|
||||
expect(secret).toBe("test-ingest-secret");
|
||||
expect(events[0]?.source_app).toBe("cfdm");
|
||||
expect(events[0]?.action).toBe("group.create");
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Vendored
+573
-2
File diff suppressed because one or more lines are too long
Vendored
+187
-95
@@ -245,6 +245,22 @@ var notificationLog = sqliteTable("notification_log", {
|
||||
message: text("message").notNull(),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var auditLog = sqliteTable("audit_log", {
|
||||
id: text("id").primaryKey(),
|
||||
event_id: text("event_id"),
|
||||
source_app: text("source_app").notNull().default("cfdm"),
|
||||
action: text("action").notNull(),
|
||||
severity: text("severity").notNull().default("info"),
|
||||
actor_user_id: text("actor_user_id"),
|
||||
actor_email: text("actor_email"),
|
||||
actor_name: text("actor_name"),
|
||||
target_type: text("target_type"),
|
||||
target_id: text("target_id"),
|
||||
summary: text("summary").notNull(),
|
||||
details_json: text("details_json"),
|
||||
ip: text("ip"),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var schema = {
|
||||
groups,
|
||||
services,
|
||||
@@ -264,7 +280,8 @@ var schema = {
|
||||
domainTags,
|
||||
domainMonitors,
|
||||
domainMonitorResults,
|
||||
notificationLog
|
||||
notificationLog,
|
||||
auditLog
|
||||
};
|
||||
|
||||
// src/client.ts
|
||||
@@ -329,8 +346,80 @@ var ConflictError = class extends Error {
|
||||
}
|
||||
};
|
||||
|
||||
// src/audit-log.ts
|
||||
import { and, desc, eq, or } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
function mapRow(row) {
|
||||
let details = null;
|
||||
if (row.details_json) {
|
||||
try {
|
||||
details = JSON.parse(row.details_json);
|
||||
} catch {
|
||||
details = { raw: row.details_json };
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
event_id: row.event_id,
|
||||
source_app: row.source_app || "cfdm",
|
||||
action: row.action,
|
||||
severity: row.severity,
|
||||
actor_user_id: row.actor_user_id,
|
||||
actor_email: row.actor_email,
|
||||
actor_name: row.actor_name,
|
||||
target_type: row.target_type ?? null,
|
||||
target_id: row.target_id,
|
||||
summary: row.summary,
|
||||
details,
|
||||
ip: row.ip,
|
||||
created_at: row.created_at
|
||||
};
|
||||
}
|
||||
function appendAudit(db, input) {
|
||||
const now = input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
||||
const eventId = input.eventId ?? null;
|
||||
if (eventId) {
|
||||
const existing = db.select({ id: auditLog.id }).from(auditLog).where(eq(auditLog.event_id, eventId)).get();
|
||||
if (existing) return false;
|
||||
}
|
||||
db.insert(auditLog).values({
|
||||
id: randomUUID(),
|
||||
event_id: eventId,
|
||||
source_app: input.sourceApp ?? "cfdm",
|
||||
action: input.action,
|
||||
severity: input.severity ?? "info",
|
||||
actor_user_id: input.actorUserId ?? null,
|
||||
actor_email: input.actorEmail ?? null,
|
||||
actor_name: input.actorName ?? null,
|
||||
target_type: input.targetType ?? null,
|
||||
target_id: input.targetId ?? null,
|
||||
summary: input.summary,
|
||||
details_json: input.details ? JSON.stringify(input.details) : null,
|
||||
ip: input.ip ?? null,
|
||||
created_at: now
|
||||
}).run();
|
||||
return true;
|
||||
}
|
||||
function listAudit(db, opts = {}) {
|
||||
const limit = opts.limit ?? 200;
|
||||
const conditions = [];
|
||||
if (opts.action) conditions.push(eq(auditLog.action, opts.action));
|
||||
if (opts.severity) conditions.push(eq(auditLog.severity, opts.severity));
|
||||
if (opts.sourceApp) conditions.push(eq(auditLog.source_app, opts.sourceApp));
|
||||
if (opts.userId) {
|
||||
conditions.push(
|
||||
or(
|
||||
eq(auditLog.actor_user_id, opts.userId),
|
||||
eq(auditLog.target_id, opts.userId)
|
||||
)
|
||||
);
|
||||
}
|
||||
const rows = conditions.length > 0 ? db.select().from(auditLog).where(and(...conditions)).orderBy(desc(auditLog.created_at)).limit(limit).all() : db.select().from(auditLog).orderBy(desc(auditLog.created_at)).limit(limit).all();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
// src/settings-repo.ts
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq as eq2 } from "drizzle-orm";
|
||||
var SETTINGS_ID = "settings-main";
|
||||
function toDto(row) {
|
||||
return {
|
||||
@@ -345,17 +434,17 @@ function toDto(row) {
|
||||
};
|
||||
}
|
||||
function getAppSettings(db) {
|
||||
const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
||||
const row = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get();
|
||||
if (!row) {
|
||||
db.insert(appSettings).values({ id: SETTINGS_ID }).run();
|
||||
return toDto(
|
||||
db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get()
|
||||
db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get()
|
||||
);
|
||||
}
|
||||
return toDto(row);
|
||||
}
|
||||
function getAppSettingsSecrets(db) {
|
||||
const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
||||
const row = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get();
|
||||
return {
|
||||
vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "",
|
||||
vpsTrackerIntegrationToken: row?.vps_tracker_integration_token?.trim() ?? "",
|
||||
@@ -363,11 +452,11 @@ function getAppSettingsSecrets(db) {
|
||||
};
|
||||
}
|
||||
function updateAppSettings(db, patch) {
|
||||
const existing = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
||||
const existing = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get();
|
||||
if (!existing) {
|
||||
db.insert(appSettings).values({ id: SETTINGS_ID }).run();
|
||||
}
|
||||
const current = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
||||
const current = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get();
|
||||
db.update(appSettings).set({
|
||||
// app_switcher_json: deprecated — source of truth is auth-portal
|
||||
vps_tracker_url: patch.vpsTrackerUrl !== void 0 ? patch.vpsTrackerUrl : current.vps_tracker_url,
|
||||
@@ -375,14 +464,14 @@ function updateAppSettings(db, patch) {
|
||||
vps_tracker_sync_enabled: patch.vpsTrackerSyncEnabled !== void 0 ? patch.vpsTrackerSyncEnabled : current.vps_tracker_sync_enabled,
|
||||
show_quick_actions: patch.showQuickActions !== void 0 ? patch.showQuickActions : current.show_quick_actions,
|
||||
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
||||
}).where(eq(appSettings.id, SETTINGS_ID)).run();
|
||||
}).where(eq2(appSettings.id, SETTINGS_ID)).run();
|
||||
return getAppSettings(db);
|
||||
}
|
||||
function touchVpsTrackerSync(db) {
|
||||
db.update(appSettings).set({
|
||||
vps_tracker_last_sync_at: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
||||
}).where(eq(appSettings.id, SETTINGS_ID)).run();
|
||||
}).where(eq2(appSettings.id, SETTINGS_ID)).run();
|
||||
}
|
||||
|
||||
// src/repos.ts
|
||||
@@ -496,12 +585,12 @@ __export(repos_exports, {
|
||||
upsertSubdomain: () => upsertSubdomain
|
||||
});
|
||||
import { dnsRecordNamesMatch } from "@cfdm/shared";
|
||||
import { and, asc, count, eq as eq2, isNull, like, notInArray, or, sql as sql2 } from "drizzle-orm";
|
||||
import { and as and2, asc, count, eq as eq3, isNull, like, notInArray, or as or2, sql as sql2 } from "drizzle-orm";
|
||||
function listGroups(db) {
|
||||
return db.select().from(groups).orderBy(asc(groups.name)).all();
|
||||
}
|
||||
function getGroup(db, id) {
|
||||
const row = db.select().from(groups).where(eq2(groups.id, id)).get();
|
||||
const row = db.select().from(groups).where(eq3(groups.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`group ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -519,17 +608,17 @@ function createGroup(db, name, slug) {
|
||||
return getGroup(db, id);
|
||||
}
|
||||
function updateGroup(db, id, name, slug) {
|
||||
const result = db.update(groups).set({ name, slug, updated_at: sql2`datetime('now')` }).where(eq2(groups.id, id)).run();
|
||||
const result = db.update(groups).set({ name, slug, updated_at: sql2`datetime('now')` }).where(eq3(groups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
||||
return getGroup(db, id);
|
||||
}
|
||||
function deleteGroup(db, id) {
|
||||
const result = db.delete(groups).where(eq2(groups.id, id)).run();
|
||||
const result = db.delete(groups).where(eq3(groups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
||||
}
|
||||
function listDomains(db, groupId) {
|
||||
if (groupId != null) {
|
||||
return db.select().from(domains).where(eq2(domains.group_id, groupId)).orderBy(asc(domains.zone_name)).all();
|
||||
return db.select().from(domains).where(eq3(domains.group_id, groupId)).orderBy(asc(domains.zone_name)).all();
|
||||
}
|
||||
return db.select().from(domains).orderBy(asc(domains.zone_name)).all();
|
||||
}
|
||||
@@ -598,7 +687,7 @@ function findDomainByZoneName(db, zoneName) {
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
function getDomain(db, id) {
|
||||
const row = db.select().from(domains).where(eq2(domains.id, id)).get();
|
||||
const row = db.select().from(domains).where(eq3(domains.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`domain ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -623,33 +712,33 @@ function updateDomain(db, id, patch) {
|
||||
if (patch.environment !== void 0) {
|
||||
updates.environment = patch.environment;
|
||||
}
|
||||
const result = db.update(domains).set(updates).where(eq2(domains.id, id)).run();
|
||||
const result = db.update(domains).set(updates).where(eq3(domains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
||||
return getDomain(db, id);
|
||||
}
|
||||
function deleteDomain(db, id) {
|
||||
const result = db.delete(domains).where(eq2(domains.id, id)).run();
|
||||
const result = db.delete(domains).where(eq3(domains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
||||
}
|
||||
function setDomainLastSynced(db, id) {
|
||||
db.update(domains).set({
|
||||
last_synced_at: sql2`datetime('now')`,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(domains.id, id)).run();
|
||||
}).where(eq3(domains.id, id)).run();
|
||||
}
|
||||
function listAllDomains(db) {
|
||||
return listDomains(db);
|
||||
}
|
||||
function listSubdomainsByDomain(db, domainId) {
|
||||
return db.select().from(subdomains).where(eq2(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all();
|
||||
return db.select().from(subdomains).where(eq3(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all();
|
||||
}
|
||||
function getSubdomain(db, id) {
|
||||
const row = db.select().from(subdomains).where(eq2(subdomains.id, id)).get();
|
||||
const row = db.select().from(subdomains).where(eq3(subdomains.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`subdomain ${id}`);
|
||||
return row;
|
||||
}
|
||||
function findSubdomainByDomainAndName(db, domainId, name) {
|
||||
const row = db.select().from(subdomains).where(and(eq2(subdomains.domain_id, domainId), eq2(subdomains.name, name))).get();
|
||||
const row = db.select().from(subdomains).where(and2(eq3(subdomains.domain_id, domainId), eq3(subdomains.name, name))).get();
|
||||
return row ?? null;
|
||||
}
|
||||
function upsertSubdomain(db, domainId, name, fqdn) {
|
||||
@@ -673,12 +762,12 @@ function updateSubdomain(db, id, patch) {
|
||||
if (patch.cert_monitoring !== void 0) {
|
||||
updates.cert_monitoring = patch.cert_monitoring;
|
||||
}
|
||||
const result = db.update(subdomains).set(updates).where(eq2(subdomains.id, id)).run();
|
||||
const result = db.update(subdomains).set(updates).where(eq3(subdomains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
||||
return getSubdomain(db, id);
|
||||
}
|
||||
function deleteSubdomain(db, id) {
|
||||
const result = db.delete(subdomains).where(eq2(subdomains.id, id)).run();
|
||||
const result = db.delete(subdomains).where(eq3(subdomains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
||||
}
|
||||
function listAllSubdomains(db) {
|
||||
@@ -688,9 +777,9 @@ function mapDnsRecord(row) {
|
||||
return row;
|
||||
}
|
||||
function listDnsRecords(db, domainId, filter = {}) {
|
||||
const conditions = [eq2(dnsRecords.domain_id, domainId)];
|
||||
const conditions = [eq3(dnsRecords.domain_id, domainId)];
|
||||
if (filter.record_type) {
|
||||
conditions.push(eq2(dnsRecords.record_type, filter.record_type.toUpperCase()));
|
||||
conditions.push(eq3(dnsRecords.record_type, filter.record_type.toUpperCase()));
|
||||
}
|
||||
if (filter.name) {
|
||||
conditions.push(like(dnsRecords.name, `%${filter.name}%`));
|
||||
@@ -699,15 +788,15 @@ function listDnsRecords(db, domainId, filter = {}) {
|
||||
conditions.push(like(dnsRecords.content, `%${filter.content}%`));
|
||||
}
|
||||
if (filter.proxied != null) {
|
||||
conditions.push(eq2(dnsRecords.proxied, filter.proxied));
|
||||
conditions.push(eq3(dnsRecords.proxied, filter.proxied));
|
||||
}
|
||||
if (filter.sync_status) {
|
||||
conditions.push(eq2(dnsRecords.sync_status, filter.sync_status));
|
||||
conditions.push(eq3(dnsRecords.sync_status, filter.sync_status));
|
||||
}
|
||||
if (filter.q) {
|
||||
const pat = `%${filter.q}%`;
|
||||
conditions.push(
|
||||
or(
|
||||
or2(
|
||||
like(dnsRecords.name, pat),
|
||||
like(dnsRecords.content, pat),
|
||||
like(dnsRecords.record_type, pat)
|
||||
@@ -718,10 +807,10 @@ function listDnsRecords(db, domainId, filter = {}) {
|
||||
const page = Math.max(1, filter.page ?? 1);
|
||||
const limit = Math.min(200, Math.max(1, filter.limit ?? 50));
|
||||
const offset = (page - 1) * limit;
|
||||
return db.select().from(dnsRecords).where(and(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord);
|
||||
return db.select().from(dnsRecords).where(and2(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord);
|
||||
}
|
||||
function getDnsRecord(db, domainId, id) {
|
||||
const row = db.select().from(dnsRecords).where(and(eq2(dnsRecords.id, id), eq2(dnsRecords.domain_id, domainId))).get();
|
||||
const row = db.select().from(dnsRecords).where(and2(eq3(dnsRecords.id, id), eq3(dnsRecords.domain_id, domainId))).get();
|
||||
if (!row) throw new NotFoundError(`dns record ${id}`);
|
||||
return mapDnsRecord(row);
|
||||
}
|
||||
@@ -752,7 +841,7 @@ function updateDnsFields(db, id, recordType, name, content, ttl, proxied, priori
|
||||
sync_status: syncStatus,
|
||||
last_error: lastError,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(dnsRecords.id, id)).run();
|
||||
}).where(eq3(dnsRecords.id, id)).run();
|
||||
}
|
||||
function setDnsSyncStatus(db, id, syncStatus, cfRecordId, lastError) {
|
||||
db.update(dnsRecords).set({
|
||||
@@ -760,19 +849,19 @@ function setDnsSyncStatus(db, id, syncStatus, cfRecordId, lastError) {
|
||||
cf_record_id: cfRecordId,
|
||||
last_error: lastError,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(dnsRecords.id, id)).run();
|
||||
}).where(eq3(dnsRecords.id, id)).run();
|
||||
}
|
||||
function deleteDnsRecord(db, id) {
|
||||
db.delete(dnsRecords).where(eq2(dnsRecords.id, id)).run();
|
||||
db.delete(dnsRecords).where(eq3(dnsRecords.id, id)).run();
|
||||
}
|
||||
function listDnsByDomain(db, domainId) {
|
||||
return db.select().from(dnsRecords).where(eq2(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord);
|
||||
return db.select().from(dnsRecords).where(eq3(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord);
|
||||
}
|
||||
function findDnsByCfId(db, domainId, cfRecordId) {
|
||||
const row = db.select().from(dnsRecords).where(
|
||||
and(
|
||||
eq2(dnsRecords.domain_id, domainId),
|
||||
eq2(dnsRecords.cf_record_id, cfRecordId)
|
||||
and2(
|
||||
eq3(dnsRecords.domain_id, domainId),
|
||||
eq3(dnsRecords.cf_record_id, cfRecordId)
|
||||
)
|
||||
).get();
|
||||
return row ? mapDnsRecord(row) : null;
|
||||
@@ -781,7 +870,7 @@ function markDnsPendingDelete(db, id) {
|
||||
setDnsSyncStatus(db, id, "pending_delete", null, null);
|
||||
}
|
||||
function maxSortOrderInGroup(db, groupId) {
|
||||
const condition = groupId === null ? isNull(services.service_group_id) : eq2(services.service_group_id, groupId);
|
||||
const condition = groupId === null ? isNull(services.service_group_id) : eq3(services.service_group_id, groupId);
|
||||
const row = db.select({ maxOrder: sql2`coalesce(max(${services.sort_order}), -1)` }).from(services).where(condition).get();
|
||||
return row?.maxOrder ?? -1;
|
||||
}
|
||||
@@ -789,13 +878,13 @@ function listServices(db) {
|
||||
return db.select().from(services).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
}
|
||||
function listServicesByGroup(db, groupId) {
|
||||
return db.select().from(services).where(eq2(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
return db.select().from(services).where(eq3(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
}
|
||||
function listUngroupedServices(db) {
|
||||
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
||||
}
|
||||
function getService(db, id) {
|
||||
const row = db.select().from(services).where(eq2(services.id, id)).get();
|
||||
const row = db.select().from(services).where(eq3(services.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -810,11 +899,11 @@ function updateService(db, id, name, slug) {
|
||||
slug,
|
||||
subdomain: slug,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(services.id, id)).run();
|
||||
}).where(eq3(services.id, id)).run();
|
||||
return getService(db, id);
|
||||
}
|
||||
function setServiceEnabled(db, id, enabled) {
|
||||
db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq2(services.id, id)).run();
|
||||
db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq3(services.id, id)).run();
|
||||
return getService(db, id);
|
||||
}
|
||||
function setServiceLb(db, id, weight, priority) {
|
||||
@@ -822,7 +911,7 @@ function setServiceLb(db, id, weight, priority) {
|
||||
lb_weight: weight,
|
||||
lb_priority: priority,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(services.id, id)).run();
|
||||
}).where(eq3(services.id, id)).run();
|
||||
}
|
||||
function setServiceGroup(db, id, groupId) {
|
||||
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
|
||||
@@ -830,14 +919,14 @@ function setServiceGroup(db, id, groupId) {
|
||||
service_group_id: groupId,
|
||||
sort_order: sortOrder,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(services.id, id)).run();
|
||||
}).where(eq3(services.id, id)).run();
|
||||
}
|
||||
function reorderServices(db, groupId, orderedIds) {
|
||||
const uniqueIds = new Set(orderedIds);
|
||||
if (uniqueIds.size !== orderedIds.length) {
|
||||
throw new Error("duplicate service ids in reorder request");
|
||||
}
|
||||
const condition = groupId === null ? isNull(services.service_group_id) : eq2(services.service_group_id, groupId);
|
||||
const condition = groupId === null ? isNull(services.service_group_id) : eq3(services.service_group_id, groupId);
|
||||
const existing = db.select({ id: services.id }).from(services).where(condition).all().map((row) => row.id);
|
||||
const existingSet = new Set(existing);
|
||||
for (const serviceId of orderedIds) {
|
||||
@@ -847,12 +936,12 @@ function reorderServices(db, groupId, orderedIds) {
|
||||
}
|
||||
db.transaction((tx) => {
|
||||
for (let index = 0; index < orderedIds.length; index++) {
|
||||
tx.update(services).set({ sort_order: index, updated_at: sql2`datetime('now')` }).where(eq2(services.id, orderedIds[index])).run();
|
||||
tx.update(services).set({ sort_order: index, updated_at: sql2`datetime('now')` }).where(eq3(services.id, orderedIds[index])).run();
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteService(db, id) {
|
||||
const result = db.delete(services).where(eq2(services.id, id)).run();
|
||||
const result = db.delete(services).where(eq3(services.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
|
||||
}
|
||||
function mapServiceGroup(row) {
|
||||
@@ -880,7 +969,7 @@ function listServiceGroups(db) {
|
||||
return db.select().from(serviceGroups).orderBy(asc(serviceGroups.name)).all().map(mapServiceGroup);
|
||||
}
|
||||
function getServiceGroup(db, id) {
|
||||
const row = db.select().from(serviceGroups).where(eq2(serviceGroups.id, id)).get();
|
||||
const row = db.select().from(serviceGroups).where(eq3(serviceGroups.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service group ${id}`);
|
||||
return mapServiceGroup(row);
|
||||
}
|
||||
@@ -929,37 +1018,37 @@ function updateServiceGroup(db, id, name, groupType, icon, domain, lbPatch) {
|
||||
if (lbPatch.health_check_verify_tls !== void 0)
|
||||
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
|
||||
}
|
||||
const result = db.update(serviceGroups).set(update).where(eq2(serviceGroups.id, id)).run();
|
||||
const result = db.update(serviceGroups).set(update).where(eq3(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function setServiceGroupEnabled(db, id, enabled) {
|
||||
const result = db.update(serviceGroups).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq2(serviceGroups.id, id)).run();
|
||||
const result = db.update(serviceGroups).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq3(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function deleteServiceGroup(db, id) {
|
||||
const result = db.delete(serviceGroups).where(eq2(serviceGroups.id, id)).run();
|
||||
const result = db.delete(serviceGroups).where(eq3(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
}
|
||||
function listServiceIps(db, serviceId) {
|
||||
return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq2(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
|
||||
return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq3(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
|
||||
}
|
||||
function replaceServiceIps(db, serviceId, ips) {
|
||||
db.delete(serviceIps).where(eq2(serviceIps.service_id, serviceId)).run();
|
||||
db.delete(serviceIps).where(eq3(serviceIps.service_id, serviceId)).run();
|
||||
for (const ip of ips) {
|
||||
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
|
||||
}
|
||||
}
|
||||
function listBindingIps(db, bindingId) {
|
||||
return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
|
||||
return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
|
||||
}
|
||||
function listBindingIpsWithMeta(db, bindingId) {
|
||||
return db.select({
|
||||
ip: serviceBindingIps.ip,
|
||||
weight: serviceBindingIps.weight,
|
||||
priority: serviceBindingIps.priority
|
||||
}).from(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).all();
|
||||
}).from(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).all();
|
||||
}
|
||||
function replaceBindingIps(db, bindingId, ips) {
|
||||
replaceBindingIpsWithMeta(
|
||||
@@ -969,7 +1058,7 @@ function replaceBindingIps(db, bindingId, ips) {
|
||||
);
|
||||
}
|
||||
function replaceBindingIpsWithMeta(db, bindingId, entries) {
|
||||
db.delete(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).run();
|
||||
db.delete(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).run();
|
||||
for (const entry of entries) {
|
||||
db.insert(serviceBindingIps).values({
|
||||
binding_id: bindingId,
|
||||
@@ -1000,13 +1089,13 @@ function updateBindingLbConfig(db, bindingId, patch) {
|
||||
update.health_check_timeout_ms = patch.health_check_timeout_ms;
|
||||
if (patch.health_check_verify_tls !== void 0)
|
||||
update.health_check_verify_tls = patch.health_check_verify_tls;
|
||||
db.update(serviceBindings).set(update).where(eq2(serviceBindings.id, bindingId)).run();
|
||||
db.update(serviceBindings).set(update).where(eq3(serviceBindings.id, bindingId)).run();
|
||||
}
|
||||
function setBindingCnameTarget(db, bindingId, target) {
|
||||
db.update(serviceBindings).set({
|
||||
cname_target: target,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(serviceBindings.id, bindingId)).run();
|
||||
}).where(eq3(serviceBindings.id, bindingId)).run();
|
||||
}
|
||||
function listRecordsForBinding(db, bindingId) {
|
||||
return db.all(sql2`
|
||||
@@ -1024,9 +1113,9 @@ function linkBindingRecord(db, bindingId, dnsRecordId) {
|
||||
}
|
||||
function unlinkBindingRecord(db, bindingId, dnsRecordId) {
|
||||
db.delete(serviceBindingRecords).where(
|
||||
and(
|
||||
eq2(serviceBindingRecords.binding_id, bindingId),
|
||||
eq2(serviceBindingRecords.dns_record_id, dnsRecordId)
|
||||
and2(
|
||||
eq3(serviceBindingRecords.binding_id, bindingId),
|
||||
eq3(serviceBindingRecords.dns_record_id, dnsRecordId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
@@ -1046,9 +1135,9 @@ function linkGroupDnsRecord(db, groupId, dnsRecordId) {
|
||||
}
|
||||
function unlinkGroupDnsRecord(db, groupId, dnsRecordId) {
|
||||
db.delete(serviceGroupDnsRecords).where(
|
||||
and(
|
||||
eq2(serviceGroupDnsRecords.group_id, groupId),
|
||||
eq2(serviceGroupDnsRecords.dns_record_id, dnsRecordId)
|
||||
and2(
|
||||
eq3(serviceGroupDnsRecords.group_id, groupId),
|
||||
eq3(serviceGroupDnsRecords.dns_record_id, dnsRecordId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
@@ -1138,7 +1227,7 @@ function listBindingsByService(db, serviceId) {
|
||||
`);
|
||||
}
|
||||
function getBinding(db, id) {
|
||||
const row = db.select().from(serviceBindings).where(eq2(serviceBindings.id, id)).get();
|
||||
const row = db.select().from(serviceBindings).where(eq3(serviceBindings.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service binding ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -1157,10 +1246,10 @@ function getBindingView(db, id) {
|
||||
}
|
||||
function findBinding(db, serviceId, domainId, hostname) {
|
||||
const row = db.select().from(serviceBindings).where(
|
||||
and(
|
||||
eq2(serviceBindings.service_id, serviceId),
|
||||
eq2(serviceBindings.domain_id, domainId),
|
||||
eq2(serviceBindings.hostname, hostname)
|
||||
and2(
|
||||
eq3(serviceBindings.service_id, serviceId),
|
||||
eq3(serviceBindings.domain_id, domainId),
|
||||
eq3(serviceBindings.hostname, hostname)
|
||||
)
|
||||
).get();
|
||||
return row ?? null;
|
||||
@@ -1180,43 +1269,43 @@ function updateBindingFields(db, id, serviceId, hostname, dnsRecordId) {
|
||||
hostname,
|
||||
dns_record_id: dnsRecordId,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(serviceBindings.id, id)).run();
|
||||
}).where(eq3(serviceBindings.id, id)).run();
|
||||
}
|
||||
function setBindingDnsRecordId(db, bindingId, dnsRecordId) {
|
||||
db.update(serviceBindings).set({
|
||||
dns_record_id: dnsRecordId,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(serviceBindings.id, bindingId)).run();
|
||||
}).where(eq3(serviceBindings.id, bindingId)).run();
|
||||
}
|
||||
function bindingsToRemove(db, serviceId, keepIds) {
|
||||
const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all();
|
||||
const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all();
|
||||
return all.filter((b) => !keepIds.includes(b.id));
|
||||
}
|
||||
function deleteBindingsExcept(db, serviceId, keepIds) {
|
||||
const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all();
|
||||
const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all();
|
||||
for (const binding of all) {
|
||||
if (!keepIds.includes(binding.id)) {
|
||||
db.delete(serviceBindings).where(eq2(serviceBindings.id, binding.id)).run();
|
||||
db.delete(serviceBindings).where(eq3(serviceBindings.id, binding.id)).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
function deleteBinding(db, id) {
|
||||
const result = db.delete(serviceBindings).where(eq2(serviceBindings.id, id)).run();
|
||||
const result = db.delete(serviceBindings).where(eq3(serviceBindings.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service binding ${id}`);
|
||||
}
|
||||
function listCertificates(db, status) {
|
||||
if (status) {
|
||||
return db.select().from(certificates).where(eq2(certificates.status, status)).orderBy(asc(certificates.expires_at)).all();
|
||||
return db.select().from(certificates).where(eq3(certificates.status, status)).orderBy(asc(certificates.expires_at)).all();
|
||||
}
|
||||
return db.select().from(certificates).orderBy(asc(certificates.expires_at)).all();
|
||||
}
|
||||
function getCertificate(db, id) {
|
||||
const row = db.select().from(certificates).where(eq2(certificates.id, id)).get();
|
||||
const row = db.select().from(certificates).where(eq3(certificates.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`certificate ${id}`);
|
||||
return row;
|
||||
}
|
||||
function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError) {
|
||||
const existing = db.select().from(certificates).where(eq2(certificates.hostname, hostname)).get();
|
||||
const existing = db.select().from(certificates).where(eq3(certificates.hostname, hostname)).get();
|
||||
if (existing) {
|
||||
db.update(certificates).set({
|
||||
domain_id: domainId,
|
||||
@@ -1226,7 +1315,7 @@ function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt,
|
||||
last_error: lastError,
|
||||
status,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(certificates.id, existing.id)).run();
|
||||
}).where(eq3(certificates.id, existing.id)).run();
|
||||
return getCertificate(db, existing.id);
|
||||
}
|
||||
const id = db.insert(certificates).values({
|
||||
@@ -1259,7 +1348,7 @@ function createSyncJob(db, id, domainId) {
|
||||
db.insert(syncJobs).values({ id, domain_id: domainId, status: "pending" }).run();
|
||||
}
|
||||
function getSyncJob(db, id) {
|
||||
const row = db.select().from(syncJobs).where(eq2(syncJobs.id, id)).get();
|
||||
const row = db.select().from(syncJobs).where(eq3(syncJobs.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`sync job ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -1268,7 +1357,7 @@ function finishSyncJob(db, id, status, message) {
|
||||
status,
|
||||
message,
|
||||
finished_at: sql2`datetime('now')`
|
||||
}).where(eq2(syncJobs.id, id)).run();
|
||||
}).where(eq3(syncJobs.id, id)).run();
|
||||
}
|
||||
function listIpHealthStatus(db, scope, refId) {
|
||||
return db.all(sql2`
|
||||
@@ -1409,18 +1498,18 @@ function upsertIpHealthStatus(db, scope, refId, ip, status, latencyMs, consecuti
|
||||
}
|
||||
function deleteIpHealthStatusForRef(db, scope, refId) {
|
||||
db.delete(ipHealthStatus).where(
|
||||
and(
|
||||
eq2(ipHealthStatus.scope, scope),
|
||||
eq2(ipHealthStatus.ref_id, refId)
|
||||
and2(
|
||||
eq3(ipHealthStatus.scope, scope),
|
||||
eq3(ipHealthStatus.ref_id, refId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
function deleteIpHealthStatusForIp(db, scope, refId, ip) {
|
||||
db.delete(ipHealthStatus).where(
|
||||
and(
|
||||
eq2(ipHealthStatus.scope, scope),
|
||||
eq2(ipHealthStatus.ref_id, refId),
|
||||
eq2(ipHealthStatus.ip, ip)
|
||||
and2(
|
||||
eq3(ipHealthStatus.scope, scope),
|
||||
eq3(ipHealthStatus.ref_id, refId),
|
||||
eq3(ipHealthStatus.ip, ip)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
@@ -1553,10 +1642,10 @@ function listHealthCheckTargets(db) {
|
||||
}));
|
||||
}
|
||||
function listDomainTags(db, domainId) {
|
||||
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq2(domainTags.domain_id, domainId)).all().map((r) => r.tag);
|
||||
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq3(domainTags.domain_id, domainId)).all().map((r) => r.tag);
|
||||
}
|
||||
function setDomainTags(db, domainId, tags) {
|
||||
db.delete(domainTags).where(eq2(domainTags.domain_id, domainId)).run();
|
||||
db.delete(domainTags).where(eq3(domainTags.domain_id, domainId)).run();
|
||||
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
||||
for (const tag of unique) {
|
||||
db.insert(domainTags).values({ domain_id: domainId, tag }).run();
|
||||
@@ -1573,13 +1662,13 @@ function addDomainTags(db, domainId, tags) {
|
||||
}
|
||||
}
|
||||
function listDomainMonitors(db, domainId) {
|
||||
return db.select().from(domainMonitors).where(eq2(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all();
|
||||
return db.select().from(domainMonitors).where(eq3(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all();
|
||||
}
|
||||
function listEnabledDomainMonitors(db) {
|
||||
return db.select().from(domainMonitors).where(eq2(domainMonitors.enabled, true)).all();
|
||||
return db.select().from(domainMonitors).where(eq3(domainMonitors.enabled, true)).all();
|
||||
}
|
||||
function getDomainMonitor(db, id) {
|
||||
const row = db.select().from(domainMonitors).where(eq2(domainMonitors.id, id)).get();
|
||||
const row = db.select().from(domainMonitors).where(eq3(domainMonitors.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`domain_monitor ${id}`);
|
||||
return row;
|
||||
}
|
||||
@@ -1597,7 +1686,7 @@ function createDomainMonitor(db, domainId, input) {
|
||||
return getDomainMonitor(db, id);
|
||||
}
|
||||
function deleteDomainMonitor(db, id) {
|
||||
const result = db.delete(domainMonitors).where(eq2(domainMonitors.id, id)).run();
|
||||
const result = db.delete(domainMonitors).where(eq3(domainMonitors.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`domain_monitor ${id}`);
|
||||
}
|
||||
function updateDomainMonitorResult(db, monitorId, status, latencyMs, error) {
|
||||
@@ -1607,7 +1696,7 @@ function updateDomainMonitorResult(db, monitorId, status, latencyMs, error) {
|
||||
last_checked_at: sql2`datetime('now')`,
|
||||
last_error: error,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq2(domainMonitors.id, monitorId)).run();
|
||||
}).where(eq3(domainMonitors.id, monitorId)).run();
|
||||
db.insert(domainMonitorResults).values({
|
||||
monitor_id: monitorId,
|
||||
status,
|
||||
@@ -1665,6 +1754,8 @@ export {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
appSettings,
|
||||
appendAudit,
|
||||
auditLog,
|
||||
certificates,
|
||||
createDb,
|
||||
createMemoryDb,
|
||||
@@ -1678,6 +1769,7 @@ export {
|
||||
groups,
|
||||
healthCheck,
|
||||
ipHealthStatus,
|
||||
listAudit,
|
||||
notificationLog,
|
||||
repos_exports as repos,
|
||||
resolveDatabasePath,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
event_id TEXT,
|
||||
source_app TEXT NOT NULL DEFAULT 'cfdm',
|
||||
action TEXT NOT NULL,
|
||||
severity TEXT NOT NULL DEFAULT 'info',
|
||||
actor_user_id TEXT,
|
||||
actor_email TEXT,
|
||||
actor_name TEXT,
|
||||
target_type TEXT,
|
||||
target_id TEXT,
|
||||
summary TEXT NOT NULL,
|
||||
details_json TEXT,
|
||||
ip TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_target ON audit_log(target_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_source ON audit_log(source_app, created_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_log_event_id ON audit_log(event_id) WHERE event_id IS NOT NULL;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { and, desc, eq, or } from "drizzle-orm";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type {
|
||||
AuditLogEntry,
|
||||
AuditSeverity,
|
||||
AuditSourceApp,
|
||||
AuditTargetType,
|
||||
} from "@cfdm/shared";
|
||||
import type { Db } from "./client.js";
|
||||
import { auditLog } from "./schema.js";
|
||||
|
||||
export type AppendAuditInput = {
|
||||
eventId?: string | null;
|
||||
sourceApp?: AuditSourceApp;
|
||||
action: string;
|
||||
severity?: AuditSeverity;
|
||||
actorUserId?: string | null;
|
||||
actorEmail?: string | null;
|
||||
actorName?: string | null;
|
||||
targetType?: AuditTargetType | null;
|
||||
targetId?: string | null;
|
||||
summary: string;
|
||||
details?: Record<string, unknown> | null;
|
||||
ip?: string | null;
|
||||
createdAt?: string | null;
|
||||
};
|
||||
|
||||
function mapRow(row: typeof auditLog.$inferSelect): AuditLogEntry {
|
||||
let details: Record<string, unknown> | null = null;
|
||||
if (row.details_json) {
|
||||
try {
|
||||
details = JSON.parse(row.details_json) as Record<string, unknown>;
|
||||
} catch {
|
||||
details = { raw: row.details_json };
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
event_id: row.event_id,
|
||||
source_app: (row.source_app as AuditSourceApp) || "cfdm",
|
||||
action: row.action,
|
||||
severity: row.severity as AuditSeverity,
|
||||
actor_user_id: row.actor_user_id,
|
||||
actor_email: row.actor_email,
|
||||
actor_name: row.actor_name,
|
||||
target_type: (row.target_type as AuditTargetType | null) ?? null,
|
||||
target_id: row.target_id,
|
||||
summary: row.summary,
|
||||
details,
|
||||
ip: row.ip,
|
||||
created_at: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
/** @returns true if inserted, false if duplicate event_id */
|
||||
export function appendAudit(db: Db, input: AppendAuditInput): boolean {
|
||||
const now = input.createdAt ?? new Date().toISOString();
|
||||
const eventId = input.eventId ?? null;
|
||||
|
||||
if (eventId) {
|
||||
const existing = db
|
||||
.select({ id: auditLog.id })
|
||||
.from(auditLog)
|
||||
.where(eq(auditLog.event_id, eventId))
|
||||
.get();
|
||||
if (existing) return false;
|
||||
}
|
||||
|
||||
db.insert(auditLog)
|
||||
.values({
|
||||
id: randomUUID(),
|
||||
event_id: eventId,
|
||||
source_app: input.sourceApp ?? "cfdm",
|
||||
action: input.action,
|
||||
severity: input.severity ?? "info",
|
||||
actor_user_id: input.actorUserId ?? null,
|
||||
actor_email: input.actorEmail ?? null,
|
||||
actor_name: input.actorName ?? null,
|
||||
target_type: input.targetType ?? null,
|
||||
target_id: input.targetId ?? null,
|
||||
summary: input.summary,
|
||||
details_json: input.details ? JSON.stringify(input.details) : null,
|
||||
ip: input.ip ?? null,
|
||||
created_at: now,
|
||||
})
|
||||
.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function listAudit(
|
||||
db: Db,
|
||||
opts: {
|
||||
action?: string;
|
||||
severity?: AuditSeverity;
|
||||
userId?: string;
|
||||
sourceApp?: AuditSourceApp;
|
||||
limit?: number;
|
||||
} = {},
|
||||
): AuditLogEntry[] {
|
||||
const limit = opts.limit ?? 200;
|
||||
const conditions = [];
|
||||
if (opts.action) conditions.push(eq(auditLog.action, opts.action));
|
||||
if (opts.severity) conditions.push(eq(auditLog.severity, opts.severity));
|
||||
if (opts.sourceApp) conditions.push(eq(auditLog.source_app, opts.sourceApp));
|
||||
if (opts.userId) {
|
||||
conditions.push(
|
||||
or(
|
||||
eq(auditLog.actor_user_id, opts.userId),
|
||||
eq(auditLog.target_id, opts.userId),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
const rows =
|
||||
conditions.length > 0
|
||||
? db
|
||||
.select()
|
||||
.from(auditLog)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(auditLog.created_at))
|
||||
.limit(limit)
|
||||
.all()
|
||||
: db
|
||||
.select()
|
||||
.from(auditLog)
|
||||
.orderBy(desc(auditLog.created_at))
|
||||
.limit(limit)
|
||||
.all();
|
||||
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./schema.js";
|
||||
export * from "./client.js";
|
||||
export * from "./errors.js";
|
||||
export * from "./audit-log.js";
|
||||
export * from "./settings-repo.js";
|
||||
export * as repos from "./repos.js";
|
||||
export type { DnsListFilter, UpdateSubdomainPatch } from "./repos.js";
|
||||
|
||||
@@ -359,6 +359,25 @@ export const notificationLog = sqliteTable("notification_log", {
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const auditLog = sqliteTable("audit_log", {
|
||||
id: text("id").primaryKey(),
|
||||
event_id: text("event_id"),
|
||||
source_app: text("source_app").notNull().default("cfdm"),
|
||||
action: text("action").notNull(),
|
||||
severity: text("severity").notNull().default("info"),
|
||||
actor_user_id: text("actor_user_id"),
|
||||
actor_email: text("actor_email"),
|
||||
actor_name: text("actor_name"),
|
||||
target_type: text("target_type"),
|
||||
target_id: text("target_id"),
|
||||
summary: text("summary").notNull(),
|
||||
details_json: text("details_json"),
|
||||
ip: text("ip"),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const schema = {
|
||||
groups,
|
||||
services,
|
||||
@@ -379,4 +398,5 @@ export const schema = {
|
||||
domainMonitors,
|
||||
domainMonitorResults,
|
||||
notificationLog,
|
||||
auditLog,
|
||||
};
|
||||
|
||||
Vendored
+109
-1
@@ -1503,4 +1503,112 @@ declare const vpsTrackerEventSchema: z.ZodObject<{
|
||||
}, z.core.$strip>;
|
||||
type VpsTrackerEvent = z.infer<typeof vpsTrackerEventSchema>;
|
||||
|
||||
export { type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfZone, type CfdmBindingSyncItem, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NotificationLog, type ParsedFqdn, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ipHealthStateSchema, ipHealthStatusSchema, isValidIpv4, lbModeSchema, loginSchema, normalizeDnsRecordName, notificationLogSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema };
|
||||
declare const AUDIT_SEVERITIES: readonly ["info", "warning", "critical"];
|
||||
type AuditSeverity = (typeof AUDIT_SEVERITIES)[number];
|
||||
declare const auditSeveritySchema: z.ZodEnum<{
|
||||
warning: "warning";
|
||||
info: "info";
|
||||
critical: "critical";
|
||||
}>;
|
||||
declare const AUDIT_SOURCE_APPS: readonly ["portal", "vps", "cfdm", "bgp", "fw"];
|
||||
type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number];
|
||||
declare const auditSourceAppSchema: z.ZodEnum<{
|
||||
bgp: "bgp";
|
||||
vps: "vps";
|
||||
portal: "portal";
|
||||
cfdm: "cfdm";
|
||||
fw: "fw";
|
||||
}>;
|
||||
declare const AUDIT_TARGET_TYPES: readonly ["user", "settings", "session", "system", "app_resource"];
|
||||
type AuditTargetType = (typeof AUDIT_TARGET_TYPES)[number];
|
||||
declare const auditTargetTypeSchema: z.ZodEnum<{
|
||||
user: "user";
|
||||
settings: "settings";
|
||||
session: "session";
|
||||
system: "system";
|
||||
app_resource: "app_resource";
|
||||
}>;
|
||||
declare const auditLogEntrySchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
event_id: z.ZodNullable<z.ZodString>;
|
||||
source_app: z.ZodEnum<{
|
||||
bgp: "bgp";
|
||||
vps: "vps";
|
||||
portal: "portal";
|
||||
cfdm: "cfdm";
|
||||
fw: "fw";
|
||||
}>;
|
||||
action: z.ZodString;
|
||||
severity: z.ZodEnum<{
|
||||
warning: "warning";
|
||||
info: "info";
|
||||
critical: "critical";
|
||||
}>;
|
||||
actor_user_id: z.ZodNullable<z.ZodString>;
|
||||
actor_email: z.ZodNullable<z.ZodString>;
|
||||
actor_name: z.ZodNullable<z.ZodString>;
|
||||
target_type: z.ZodNullable<z.ZodEnum<{
|
||||
user: "user";
|
||||
settings: "settings";
|
||||
session: "session";
|
||||
system: "system";
|
||||
app_resource: "app_resource";
|
||||
}>>;
|
||||
target_id: z.ZodNullable<z.ZodString>;
|
||||
summary: z.ZodString;
|
||||
details: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
ip: z.ZodNullable<z.ZodString>;
|
||||
created_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type AuditLogEntry = z.infer<typeof auditLogEntrySchema>;
|
||||
declare const auditListQuerySchema: z.ZodObject<{
|
||||
action: z.ZodOptional<z.ZodString>;
|
||||
severity: z.ZodOptional<z.ZodEnum<{
|
||||
warning: "warning";
|
||||
info: "info";
|
||||
critical: "critical";
|
||||
}>>;
|
||||
user_id: z.ZodOptional<z.ZodString>;
|
||||
source_app: z.ZodOptional<z.ZodEnum<{
|
||||
bgp: "bgp";
|
||||
vps: "vps";
|
||||
portal: "portal";
|
||||
cfdm: "cfdm";
|
||||
fw: "fw";
|
||||
}>>;
|
||||
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
||||
}, z.core.$strip>;
|
||||
type AuditListQuery = z.infer<typeof auditListQuerySchema>;
|
||||
declare const ingestAuditEventSchema: z.ZodObject<{
|
||||
event_id: z.ZodString;
|
||||
source_app: z.ZodEnum<{
|
||||
bgp: "bgp";
|
||||
vps: "vps";
|
||||
cfdm: "cfdm";
|
||||
fw: "fw";
|
||||
}>;
|
||||
action: z.ZodString;
|
||||
severity: z.ZodOptional<z.ZodEnum<{
|
||||
warning: "warning";
|
||||
info: "info";
|
||||
critical: "critical";
|
||||
}>>;
|
||||
actor_user_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
actor_email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
actor_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
target_type: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
||||
user: "user";
|
||||
settings: "settings";
|
||||
session: "session";
|
||||
system: "system";
|
||||
app_resource: "app_resource";
|
||||
}>>>;
|
||||
target_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
summary: z.ZodString;
|
||||
details: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
||||
ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
type IngestAuditEvent = z.infer<typeof ingestAuditEventSchema>;
|
||||
|
||||
export { AUDIT_SEVERITIES, AUDIT_SOURCE_APPS, AUDIT_TARGET_TYPES, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type AuditListQuery, type AuditLogEntry, type AuditSeverity, type AuditSourceApp, type AuditTargetType, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfZone, type CfdmBindingSyncItem, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IngestAuditEvent, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NotificationLog, type ParsedFqdn, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isValidIpv4, lbModeSchema, loginSchema, normalizeDnsRecordName, notificationLogSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema };
|
||||
|
||||
Vendored
+68
@@ -601,7 +601,69 @@ var vpsTrackerEventSchema = z3.object({
|
||||
),
|
||||
timestamp: z3.string().datetime().optional()
|
||||
});
|
||||
|
||||
// src/audit.ts
|
||||
import { z as z4 } from "zod";
|
||||
var AUDIT_SEVERITIES = ["info", "warning", "critical"];
|
||||
var auditSeveritySchema = z4.enum(AUDIT_SEVERITIES);
|
||||
var AUDIT_SOURCE_APPS = [
|
||||
"portal",
|
||||
"vps",
|
||||
"cfdm",
|
||||
"bgp",
|
||||
"fw"
|
||||
];
|
||||
var auditSourceAppSchema = z4.enum(AUDIT_SOURCE_APPS);
|
||||
var AUDIT_TARGET_TYPES = [
|
||||
"user",
|
||||
"settings",
|
||||
"session",
|
||||
"system",
|
||||
"app_resource"
|
||||
];
|
||||
var auditTargetTypeSchema = z4.enum(AUDIT_TARGET_TYPES);
|
||||
var auditLogEntrySchema = z4.object({
|
||||
id: z4.string(),
|
||||
event_id: z4.string().nullable(),
|
||||
source_app: auditSourceAppSchema,
|
||||
action: z4.string(),
|
||||
severity: auditSeveritySchema,
|
||||
actor_user_id: z4.string().nullable(),
|
||||
actor_email: z4.string().nullable(),
|
||||
actor_name: z4.string().nullable(),
|
||||
target_type: auditTargetTypeSchema.nullable(),
|
||||
target_id: z4.string().nullable(),
|
||||
summary: z4.string(),
|
||||
details: z4.record(z4.string(), z4.unknown()).nullable(),
|
||||
ip: z4.string().nullable(),
|
||||
created_at: z4.string()
|
||||
});
|
||||
var auditListQuerySchema = z4.object({
|
||||
action: z4.string().optional(),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
user_id: z4.string().optional(),
|
||||
source_app: auditSourceAppSchema.optional(),
|
||||
limit: z4.coerce.number().int().min(1).max(500).default(200)
|
||||
});
|
||||
var ingestAuditEventSchema = z4.object({
|
||||
event_id: z4.string().min(1).max(128),
|
||||
source_app: z4.enum(["vps", "cfdm", "bgp", "fw"]),
|
||||
action: z4.string().min(1).max(200),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
actor_user_id: z4.string().nullable().optional(),
|
||||
actor_email: z4.string().email().nullable().optional(),
|
||||
actor_name: z4.string().nullable().optional(),
|
||||
target_type: auditTargetTypeSchema.nullable().optional(),
|
||||
target_id: z4.string().nullable().optional(),
|
||||
summary: z4.string().min(1).max(500),
|
||||
details: z4.record(z4.string(), z4.unknown()).nullable().optional(),
|
||||
ip: z4.string().nullable().optional(),
|
||||
created_at: z4.string().optional()
|
||||
});
|
||||
export {
|
||||
AUDIT_SEVERITIES,
|
||||
AUDIT_SOURCE_APPS,
|
||||
AUDIT_TARGET_TYPES,
|
||||
CERT_ERROR,
|
||||
CERT_EXPIRED,
|
||||
CERT_MONITORING_VALUES,
|
||||
@@ -621,6 +683,11 @@ export {
|
||||
appSwitcherConfigSchema,
|
||||
appSwitcherEntrySchema,
|
||||
appSwitcherIconSchema,
|
||||
auditListQuerySchema,
|
||||
auditLogEntrySchema,
|
||||
auditSeveritySchema,
|
||||
auditSourceAppSchema,
|
||||
auditTargetTypeSchema,
|
||||
bindingToFqdn,
|
||||
bulkUpdateDomainsSchema,
|
||||
certMonitoringSchema,
|
||||
@@ -653,6 +720,7 @@ export {
|
||||
healthCheckScopeSchema,
|
||||
healthCheckTypeSchema,
|
||||
healthStatusQuerySchema,
|
||||
ingestAuditEventSchema,
|
||||
ipHealthStateSchema,
|
||||
ipHealthStatusSchema,
|
||||
isValidIpv4,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AUDIT_SEVERITIES = ["info", "warning", "critical"] as const;
|
||||
export type AuditSeverity = (typeof AUDIT_SEVERITIES)[number];
|
||||
export const auditSeveritySchema = z.enum(AUDIT_SEVERITIES);
|
||||
|
||||
export const AUDIT_SOURCE_APPS = [
|
||||
"portal",
|
||||
"vps",
|
||||
"cfdm",
|
||||
"bgp",
|
||||
"fw",
|
||||
] as const;
|
||||
export type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number];
|
||||
export const auditSourceAppSchema = z.enum(AUDIT_SOURCE_APPS);
|
||||
|
||||
export const AUDIT_TARGET_TYPES = [
|
||||
"user",
|
||||
"settings",
|
||||
"session",
|
||||
"system",
|
||||
"app_resource",
|
||||
] as const;
|
||||
export type AuditTargetType = (typeof AUDIT_TARGET_TYPES)[number];
|
||||
export const auditTargetTypeSchema = z.enum(AUDIT_TARGET_TYPES);
|
||||
|
||||
export const auditLogEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
event_id: z.string().nullable(),
|
||||
source_app: auditSourceAppSchema,
|
||||
action: z.string(),
|
||||
severity: auditSeveritySchema,
|
||||
actor_user_id: z.string().nullable(),
|
||||
actor_email: z.string().nullable(),
|
||||
actor_name: z.string().nullable(),
|
||||
target_type: auditTargetTypeSchema.nullable(),
|
||||
target_id: z.string().nullable(),
|
||||
summary: z.string(),
|
||||
details: z.record(z.string(), z.unknown()).nullable(),
|
||||
ip: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
export type AuditLogEntry = z.infer<typeof auditLogEntrySchema>;
|
||||
|
||||
export const auditListQuerySchema = z.object({
|
||||
action: z.string().optional(),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
user_id: z.string().optional(),
|
||||
source_app: auditSourceAppSchema.optional(),
|
||||
limit: z.coerce.number().int().min(1).max(500).default(200),
|
||||
});
|
||||
export type AuditListQuery = z.infer<typeof auditListQuerySchema>;
|
||||
|
||||
export const ingestAuditEventSchema = z.object({
|
||||
event_id: z.string().min(1).max(128),
|
||||
source_app: z.enum(["vps", "cfdm", "bgp", "fw"]),
|
||||
action: z.string().min(1).max(200),
|
||||
severity: auditSeveritySchema.optional(),
|
||||
actor_user_id: z.string().nullable().optional(),
|
||||
actor_email: z.string().email().nullable().optional(),
|
||||
actor_name: z.string().nullable().optional(),
|
||||
target_type: auditTargetTypeSchema.nullable().optional(),
|
||||
target_id: z.string().nullable().optional(),
|
||||
summary: z.string().min(1).max(500),
|
||||
details: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
ip: z.string().nullable().optional(),
|
||||
created_at: z.string().optional(),
|
||||
});
|
||||
export type IngestAuditEvent = z.infer<typeof ingestAuditEventSchema>;
|
||||
@@ -5,6 +5,7 @@ export * from "./parse-fqdn.js";
|
||||
export * from "./schemas.js";
|
||||
export * from "./app-switcher.js";
|
||||
export * from "./integration-vps-tracker.js";
|
||||
export * from "./audit.js";
|
||||
export type {
|
||||
CfZone,
|
||||
CfDnsRecord,
|
||||
|
||||
Reference in New Issue
Block a user