From ca0dd09964fad0884aa6ceb24dac43fcac9b2400 Mon Sep 17 00:00:00 2001
From: Owen
Date: Mon, 30 Mar 2026 20:35:00 -0700
Subject: [PATCH 01/16] Add crud
---
server/auth/actions.ts | 6 +-
server/db/pg/schema/privateSchema.ts | 31 ++++
server/db/sqlite/schema/privateSchema.ts | 40 ++++-
.../createEventStreamingDestination.ts | 124 +++++++++++++++
.../deleteEventStreamingDestination.ts | 103 +++++++++++++
.../eventStreamingDestination/index.ts | 17 +++
.../listEventStreamingDestinations.ts | 144 ++++++++++++++++++
.../updateEventStreamingDestination.ts | 141 +++++++++++++++++
server/private/routers/external.ts | 38 +++++
9 files changed, 642 insertions(+), 2 deletions(-)
create mode 100644 server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts
create mode 100644 server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts
create mode 100644 server/private/routers/eventStreamingDestination/index.ts
create mode 100644 server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts
create mode 100644 server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
diff --git a/server/auth/actions.ts b/server/auth/actions.ts
index fc5daa4f8..213dab9d3 100644
--- a/server/auth/actions.ts
+++ b/server/auth/actions.ts
@@ -140,7 +140,11 @@ export enum ActionsEnum {
exportLogs = "exportLogs",
listApprovals = "listApprovals",
updateApprovals = "updateApprovals",
- signSshKey = "signSshKey"
+ signSshKey = "signSshKey",
+ createEventStreamingDestination = "createEventStreamingDestination",
+ updateEventStreamingDestination = "updateEventStreamingDestination",
+ deleteEventStreamingDestination = "deleteEventStreamingDestination",
+ listEventStreamingDestinations = "listEventStreamingDestinations"
}
export async function checkUserActionPermission(
diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts
index 9d5955d51..1b031636f 100644
--- a/server/db/pg/schema/privateSchema.ts
+++ b/server/db/pg/schema/privateSchema.ts
@@ -417,6 +417,25 @@ export const siteProvisioningKeyOrg = pgTable(
]
);
+export const eventStreamingDestinations = pgTable(
+ "eventStreamingDestinations",
+ {
+ destinationId: serial("destinationId").primaryKey(),
+ orgId: varchar("orgId", { length: 255 })
+ .notNull()
+ .references(() => orgs.orgId, { onDelete: "cascade" }),
+ sendConnectionLogs: boolean("sendConnectionLogs").notNull().default(false),
+ sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
+ sendActionLogs: boolean("sendActionLogs").notNull().default(false),
+ sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
+ type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
+ config: text("config").notNull(), // JSON string with the configuration for the destination
+ enabled: boolean("enabled").notNull().default(true),
+ createdAt: bigint("createdAt", { mode: "number" }).notNull(),
+ updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
+ }
+);
+
export type Approval = InferSelectModel;
export type Limit = InferSelectModel;
export type Account = InferSelectModel;
@@ -439,3 +458,15 @@ export type LoginPageBranding = InferSelectModel;
export type ActionAuditLog = InferSelectModel;
export type AccessAuditLog = InferSelectModel;
export type ConnectionAuditLog = InferSelectModel;
+export type SessionTransferToken = InferSelectModel<
+ typeof sessionTransferToken
+>;
+export type BannedEmail = InferSelectModel;
+export type BannedIp = InferSelectModel;
+export type SiteProvisioningKey = InferSelectModel;
+export type SiteProvisioningKeyOrg = InferSelectModel<
+ typeof siteProvisioningKeyOrg
+>;
+export type EventStreamingDestination = InferSelectModel<
+ typeof eventStreamingDestinations
+>;
diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts
index 809c0c45d..9bb994266 100644
--- a/server/db/sqlite/schema/privateSchema.ts
+++ b/server/db/sqlite/schema/privateSchema.ts
@@ -7,7 +7,16 @@ import {
sqliteTable,
text
} from "drizzle-orm/sqlite-core";
-import { clients, domains, exitNodes, orgs, sessions, siteResources, sites, users } from "./schema";
+import {
+ clients,
+ domains,
+ exitNodes,
+ orgs,
+ sessions,
+ siteResources,
+ sites,
+ users
+} from "./schema";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
@@ -401,6 +410,29 @@ export const siteProvisioningKeyOrg = sqliteTable(
]
);
+export const eventStreamingDestinations = sqliteTable(
+ "eventStreamingDestinations",
+ {
+ destinationId: integer("destinationId").primaryKey({
+ autoIncrement: true
+ }),
+ orgId: text("orgId")
+ .notNull()
+ .references(() => orgs.orgId, { onDelete: "cascade" }),
+ sendConnectionLogs: integer("sendConnectionLogs", { mode: "boolean" }).notNull().default(false),
+ sendRequestLogs: integer("sendRequestLogs", { mode: "boolean" }).notNull().default(false),
+ sendActionLogs: integer("sendActionLogs", { mode: "boolean" }).notNull().default(false),
+ sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" }).notNull().default(false),
+ type: text("type").notNull(), // e.g. "http", "kafka", etc.
+ config: text("config").notNull(), // JSON string with the configuration for the destination
+ enabled: integer("enabled", { mode: "boolean" })
+ .notNull()
+ .default(true),
+ createdAt: integer("createdAt").notNull(),
+ updatedAt: integer("updatedAt").notNull()
+ }
+);
+
export type Approval = InferSelectModel;
export type Limit = InferSelectModel;
export type Account = InferSelectModel;
@@ -423,3 +455,9 @@ export type LoginPageBranding = InferSelectModel;
export type ActionAuditLog = InferSelectModel;
export type AccessAuditLog = InferSelectModel;
export type ConnectionAuditLog = InferSelectModel;
+export type BannedEmail = InferSelectModel;
+export type BannedIp = InferSelectModel;
+export type SiteProvisioningKey = InferSelectModel;
+export type EventStreamingDestination = InferSelectModel<
+ typeof eventStreamingDestinations
+>;
diff --git a/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts
new file mode 100644
index 000000000..1c9de788a
--- /dev/null
+++ b/server/private/routers/eventStreamingDestination/createEventStreamingDestination.ts
@@ -0,0 +1,124 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { eventStreamingDestinations } from "@server/db";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { OpenAPITags, registry } from "@server/openApi";
+
+const paramsSchema = z.strictObject({
+ orgId: z.string().nonempty()
+});
+
+const bodySchema = z.strictObject({
+ type: z.string().nonempty(),
+ config: z.string().nonempty(),
+ enabled: z.boolean().optional().default(true),
+ sendConnectionLogs: z.boolean().optional().default(false),
+ sendRequestLogs: z.boolean().optional().default(false),
+ sendActionLogs: z.boolean().optional().default(false),
+ sendAccessLogs: z.boolean().optional().default(false)
+});
+
+export type CreateEventStreamingDestinationResponse = {
+ destinationId: number;
+};
+
+registry.registerPath({
+ method: "put",
+ path: "/org/{orgId}/event-streaming-destination",
+ description: "Create an event streaming destination for a specific organization.",
+ tags: [OpenAPITags.Org],
+ request: {
+ params: paramsSchema,
+ body: {
+ content: {
+ "application/json": {
+ schema: bodySchema
+ }
+ }
+ }
+ },
+ responses: {}
+});
+
+export async function createEventStreamingDestination(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const parsedParams = paramsSchema.safeParse(req.params);
+ if (!parsedParams.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedParams.error).toString()
+ )
+ );
+ }
+
+ const { orgId } = parsedParams.data;
+
+ const parsedBody = bodySchema.safeParse(req.body);
+ if (!parsedBody.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedBody.error).toString()
+ )
+ );
+ }
+
+ const { type, config, enabled } = parsedBody.data;
+
+ const now = Date.now();
+
+ const [destination] = await db
+ .insert(eventStreamingDestinations)
+ .values({
+ orgId,
+ type,
+ config,
+ enabled,
+ createdAt: now,
+ updatedAt: now,
+ sendAccessLogs: parsedBody.data.sendAccessLogs,
+ sendActionLogs: parsedBody.data.sendActionLogs,
+ sendConnectionLogs: parsedBody.data.sendConnectionLogs,
+ sendRequestLogs: parsedBody.data.sendRequestLogs
+ })
+ .returning();
+
+ return response(res, {
+ data: {
+ destinationId: destination.destinationId
+ },
+ success: true,
+ error: false,
+ message: "Event streaming destination created successfully",
+ status: HttpCode.CREATED
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+ );
+ }
+}
diff --git a/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts
new file mode 100644
index 000000000..d93bc4405
--- /dev/null
+++ b/server/private/routers/eventStreamingDestination/deleteEventStreamingDestination.ts
@@ -0,0 +1,103 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { eventStreamingDestinations } from "@server/db";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { OpenAPITags, registry } from "@server/openApi";
+import { and, eq } from "drizzle-orm";
+
+const paramsSchema = z
+ .object({
+ orgId: z.string().nonempty(),
+ destinationId: z.coerce.number()
+ })
+ .strict();
+
+registry.registerPath({
+ method: "delete",
+ path: "/org/{orgId}/event-streaming-destination/{destinationId}",
+ description: "Delete an event streaming destination for a specific organization.",
+ tags: [OpenAPITags.Org],
+ request: {
+ params: paramsSchema
+ },
+ responses: {}
+});
+
+export async function deleteEventStreamingDestination(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const parsedParams = paramsSchema.safeParse(req.params);
+ if (!parsedParams.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedParams.error).toString()
+ )
+ );
+ }
+
+ const { orgId, destinationId } = parsedParams.data;
+
+ const [existing] = await db
+ .select()
+ .from(eventStreamingDestinations)
+ .where(
+ and(
+ eq(eventStreamingDestinations.destinationId, destinationId),
+ eq(eventStreamingDestinations.orgId, orgId)
+ )
+ );
+
+ if (!existing) {
+ return next(
+ createHttpError(
+ HttpCode.NOT_FOUND,
+ "Event streaming destination not found"
+ )
+ );
+ }
+
+ await db
+ .delete(eventStreamingDestinations)
+ .where(
+ and(
+ eq(eventStreamingDestinations.destinationId, destinationId),
+ eq(eventStreamingDestinations.orgId, orgId)
+ )
+ );
+
+ return response(res, {
+ data: null,
+ success: true,
+ error: false,
+ message: "Event streaming destination deleted successfully",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+ );
+ }
+}
\ No newline at end of file
diff --git a/server/private/routers/eventStreamingDestination/index.ts b/server/private/routers/eventStreamingDestination/index.ts
new file mode 100644
index 000000000..595e9595b
--- /dev/null
+++ b/server/private/routers/eventStreamingDestination/index.ts
@@ -0,0 +1,17 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+
+export * from "./createEventStreamingDestination";
+export * from "./updateEventStreamingDestination";
+export * from "./deleteEventStreamingDestination";
+export * from "./listEventStreamingDestinations";
\ No newline at end of file
diff --git a/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts
new file mode 100644
index 000000000..b3f5ff149
--- /dev/null
+++ b/server/private/routers/eventStreamingDestination/listEventStreamingDestinations.ts
@@ -0,0 +1,144 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { eventStreamingDestinations } from "@server/db";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { OpenAPITags, registry } from "@server/openApi";
+import { eq, sql } from "drizzle-orm";
+
+const paramsSchema = z.strictObject({
+ orgId: z.string().nonempty()
+});
+
+const querySchema = z.strictObject({
+ limit: z
+ .string()
+ .optional()
+ .default("1000")
+ .transform(Number)
+ .pipe(z.int().nonnegative()),
+ offset: z
+ .string()
+ .optional()
+ .default("0")
+ .transform(Number)
+ .pipe(z.int().nonnegative())
+});
+
+export type ListEventStreamingDestinationsResponse = {
+ destinations: {
+ destinationId: number;
+ orgId: string;
+ type: string;
+ config: string;
+ enabled: boolean;
+ createdAt: number;
+ updatedAt: number;
+ sendConnectionLogs: boolean;
+ sendRequestLogs: boolean;
+ sendActionLogs: boolean;
+ sendAccessLogs: boolean;
+ }[];
+ pagination: {
+ total: number;
+ limit: number;
+ offset: number;
+ };
+};
+
+async function query(orgId: string, limit: number, offset: number) {
+ const res = await db
+ .select()
+ .from(eventStreamingDestinations)
+ .where(eq(eventStreamingDestinations.orgId, orgId))
+ .orderBy(sql`${eventStreamingDestinations.createdAt} DESC`)
+ .limit(limit)
+ .offset(offset);
+ return res;
+}
+
+registry.registerPath({
+ method: "get",
+ path: "/org/{orgId}/event-streaming-destination",
+ description: "List all event streaming destinations for a specific organization.",
+ tags: [OpenAPITags.Org],
+ request: {
+ query: querySchema,
+ params: paramsSchema
+ },
+ responses: {}
+});
+
+export async function listEventStreamingDestinations(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const parsedParams = paramsSchema.safeParse(req.params);
+ if (!parsedParams.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedParams.error).toString()
+ )
+ );
+ }
+ const { orgId } = parsedParams.data;
+
+ const parsedQuery = querySchema.safeParse(req.query);
+ if (!parsedQuery.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedQuery.error).toString()
+ )
+ );
+ }
+ const { limit, offset } = parsedQuery.data;
+
+ const list = await query(orgId, limit, offset);
+
+ const [{ count }] = await db
+ .select({ count: sql`count(*)` })
+ .from(eventStreamingDestinations)
+ .where(eq(eventStreamingDestinations.orgId, orgId));
+
+ return response(res, {
+ data: {
+ destinations: list,
+ pagination: {
+ total: count,
+ limit,
+ offset
+ }
+ },
+ success: true,
+ error: false,
+ message: "Event streaming destinations retrieved successfully",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+ );
+ }
+}
diff --git a/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
new file mode 100644
index 000000000..1ad8f0081
--- /dev/null
+++ b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
@@ -0,0 +1,141 @@
+/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+
+import { Request, Response, NextFunction } from "express";
+import { z } from "zod";
+import { db } from "@server/db";
+import { eventStreamingDestinations } from "@server/db";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { OpenAPITags, registry } from "@server/openApi";
+import { and, eq } from "drizzle-orm";
+import { parse } from "zod/v4/core";
+
+const paramsSchema = z
+ .object({
+ orgId: z.string().nonempty(),
+ destinationId: z.coerce.number()
+ })
+ .strict();
+
+const bodySchema = z.strictObject({
+ type: z.string().optional(),
+ config: z.string().optional(),
+ enabled: z.boolean().optional(),
+ sendConnectionLogs: z.boolean().optional().default(false),
+ sendRequestLogs: z.boolean().optional().default(false),
+ sendActionLogs: z.boolean().optional().default(false),
+ sendAccessLogs: z.boolean().optional().default(false)
+});
+
+export type UpdateEventStreamingDestinationResponse = {
+ destinationId: number;
+};
+
+registry.registerPath({
+ method: "post",
+ path: "/org/{orgId}/event-streaming-destination/{destinationId}",
+ description: "Update an event streaming destination for a specific organization.",
+ tags: [OpenAPITags.Org],
+ request: {
+ params: paramsSchema,
+ body: {
+ content: {
+ "application/json": {
+ schema: bodySchema
+ }
+ }
+ }
+ },
+ responses: {}
+});
+
+export async function updateEventStreamingDestination(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const parsedParams = paramsSchema.safeParse(req.params);
+ if (!parsedParams.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedParams.error).toString()
+ )
+ );
+ }
+
+ const { orgId, destinationId } = parsedParams.data;
+
+ const parsedBody = bodySchema.safeParse(req.body);
+ if (!parsedBody.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedBody.error).toString()
+ )
+ );
+ }
+
+ const [existing] = await db
+ .select()
+ .from(eventStreamingDestinations)
+ .where(
+ and(
+ eq(eventStreamingDestinations.destinationId, destinationId),
+ eq(eventStreamingDestinations.orgId, orgId)
+ )
+ );
+
+ if (!existing) {
+ return next(
+ createHttpError(
+ HttpCode.NOT_FOUND,
+ "Event streaming destination not found"
+ )
+ );
+ }
+
+ const updateData = parsedBody.data;
+
+ await db
+ .update(eventStreamingDestinations)
+ .set(updateData)
+ .where(
+ and(
+ eq(eventStreamingDestinations.destinationId, destinationId),
+ eq(eventStreamingDestinations.orgId, orgId)
+ )
+ );
+
+
+ return response(res, {
+ data: {
+ destinationId
+ },
+ success: true,
+ error: false,
+ message: "Event streaming destination updated successfully",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+ );
+ }
+}
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index 412895a41..41a4919a0 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -28,6 +28,7 @@ import * as approval from "#private/routers/approvals";
import * as ssh from "#private/routers/ssh";
import * as user from "#private/routers/user";
import * as siteProvisioning from "#private/routers/siteProvisioning";
+import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import {
verifyOrgAccess,
@@ -615,3 +616,40 @@ authenticated.patch(
logActionAudit(ActionsEnum.updateSiteProvisioningKey),
siteProvisioning.updateSiteProvisioningKey
);
+
+authenticated.put(
+ "/org/:orgId/event-streaming-destination",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyLimits,
+ verifyUserHasAction(ActionsEnum.createEventStreamingDestination),
+ logActionAudit(ActionsEnum.createEventStreamingDestination),
+ eventStreamingDestination.createEventStreamingDestination
+);
+
+authenticated.post(
+ "/org/:orgId/event-streaming-destination/:destinationId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyLimits,
+ verifyUserHasAction(ActionsEnum.updateEventStreamingDestination),
+ logActionAudit(ActionsEnum.updateEventStreamingDestination),
+ eventStreamingDestination.updateEventStreamingDestination
+);
+
+authenticated.delete(
+ "/org/:orgId/event-streaming-destination/:destinationId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.deleteEventStreamingDestination),
+ logActionAudit(ActionsEnum.deleteEventStreamingDestination),
+ eventStreamingDestination.deleteEventStreamingDestination
+);
+
+authenticated.get(
+ "/org/:orgId/event-streaming-destinations",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.listEventStreamingDestinations),
+ eventStreamingDestination.listEventStreamingDestinations
+);
From 5150a2c3862a32bac29d229cff33c869495f165e Mon Sep 17 00:00:00 2001
From: Owen
Date: Mon, 30 Mar 2026 21:00:05 -0700
Subject: [PATCH 02/16] Basic ui done
---
messages/en-US.json | 1 +
server/lib/billing/tierMatrix.ts | 6 +-
server/private/routers/external.ts | 1 -
.../[orgId]/settings/logs/streaming/page.tsx | 861 ++++++++++++++++++
src/app/navigation.tsx | 6 +
5 files changed, 872 insertions(+), 3 deletions(-)
create mode 100644 src/app/[orgId]/settings/logs/streaming/page.tsx
diff --git a/messages/en-US.json b/messages/en-US.json
index 6a137e2ba..e8c7cb47d 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -2460,6 +2460,7 @@
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
+ "sidebarLogsStreaming": "Streaming",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts
index 2aa38e1ef..c76dcd95b 100644
--- a/server/lib/billing/tierMatrix.ts
+++ b/server/lib/billing/tierMatrix.ts
@@ -18,7 +18,8 @@ export enum TierFeature {
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
SshPam = "sshPam",
FullRbac = "fullRbac",
- SiteProvisioningKeys = "siteProvisioningKeys" // handle downgrade by revoking keys if needed
+ SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
+ SIEM = "siem" // handle downgrade by disabling SIEM integrations
}
export const tierMatrix: Record = {
@@ -54,5 +55,6 @@ export const tierMatrix: Record = {
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
- [TierFeature.SiteProvisioningKeys]: ["enterprise"]
+ [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
+ [TierFeature.SIEM]: ["enterprise"]
};
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index 41a4919a0..4410a44c8 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -648,7 +648,6 @@ authenticated.delete(
authenticated.get(
"/org/:orgId/event-streaming-destinations",
- verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listEventStreamingDestinations),
eventStreamingDestination.listEventStreamingDestinations
diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx
new file mode 100644
index 000000000..265001e8e
--- /dev/null
+++ b/src/app/[orgId]/settings/logs/streaming/page.tsx
@@ -0,0 +1,861 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useParams } from "next/navigation";
+import { createApiClient, formatAxiosError } from "@app/lib/api";
+import { useEnvContext } from "@app/hooks/useEnvContext";
+import { toast } from "@app/hooks/useToast";
+import { usePaidStatus } from "@app/hooks/usePaidStatus";
+import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
+import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
+import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import {
+ Credenza,
+ CredenzaBody,
+ CredenzaClose,
+ CredenzaContent,
+ CredenzaDescription,
+ CredenzaFooter,
+ CredenzaHeader,
+ CredenzaTitle
+} from "@app/components/Credenza";
+import { Button } from "@app/components/ui/button";
+import { Input } from "@app/components/ui/input";
+import { Label } from "@app/components/ui/label";
+import { Switch } from "@app/components/ui/switch";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger
+} from "@app/components/ui/tabs";
+import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
+import { Textarea } from "@app/components/ui/textarea";
+import { Globe, Plus, Pencil, Trash2, X } from "lucide-react";
+import { AxiosResponse } from "axios";
+import { build } from "@server/build";
+
+// ── Types ──────────────────────────────────────────────────────────────────────
+
+type AuthType = "none" | "bearer" | "basic" | "custom";
+
+interface HttpConfig {
+ name: string;
+ url: string;
+ authType: AuthType;
+ bearerToken?: string;
+ basicCredentials?: string;
+ customHeaderName?: string;
+ customHeaderValue?: string;
+ headers: Array<{ key: string; value: string }>;
+ useBodyTemplate: boolean;
+ bodyTemplate?: string;
+}
+
+interface Destination {
+ destinationId: number;
+ orgId: string;
+ type: string;
+ config: string;
+ enabled: boolean;
+ createdAt: number;
+ updatedAt: number;
+}
+
+interface ListDestinationsResponse {
+ destinations: Destination[];
+ pagination: {
+ total: number;
+ limit: number;
+ offset: number;
+ };
+}
+
+// ── Helpers ────────────────────────────────────────────────────────────────────
+
+const defaultConfig = (): HttpConfig => ({
+ name: "",
+ url: "",
+ authType: "none",
+ bearerToken: "",
+ basicCredentials: "",
+ customHeaderName: "",
+ customHeaderValue: "",
+ headers: [],
+ useBodyTemplate: false,
+ bodyTemplate: ""
+});
+
+function parseConfig(raw: string): HttpConfig {
+ try {
+ return { ...defaultConfig(), ...JSON.parse(raw) };
+ } catch {
+ return defaultConfig();
+ }
+}
+
+// ── Headers editor ─────────────────────────────────────────────────────────────
+
+interface HeadersEditorProps {
+ headers: Array<{ key: string; value: string }>;
+ onChange: (headers: Array<{ key: string; value: string }>) => void;
+}
+
+function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
+ const addRow = () => onChange([...headers, { key: "", value: "" }]);
+
+ const removeRow = (i: number) =>
+ onChange(headers.filter((_, idx) => idx !== i));
+
+ const updateRow = (
+ i: number,
+ field: "key" | "value",
+ val: string
+ ) => {
+ const next = [...headers];
+ next[i] = { ...next[i], [field]: val };
+ onChange(next);
+ };
+
+ return (
+
+ {headers.length === 0 && (
+
+ No custom headers configured. Click "Add Header" to add one.
+
+ )}
+ {headers.map((h, i) => (
+
+ updateRow(i, "key", e.target.value)}
+ placeholder="Header name"
+ className="flex-1"
+ />
+ updateRow(i, "value", e.target.value)}
+ placeholder="Value"
+ className="flex-1"
+ />
+
+
+ ))}
+
+
+ );
+}
+
+// ── Destination card ───────────────────────────────────────────────────────────
+
+interface DestinationCardProps {
+ destination: Destination;
+ onToggle: (id: number, enabled: boolean) => void;
+ onEdit: (destination: Destination) => void;
+ isToggling: boolean;
+ disabled?: boolean;
+}
+
+function DestinationCard({
+ destination,
+ onToggle,
+ onEdit,
+ isToggling,
+ disabled = false
+}: DestinationCardProps) {
+ const cfg = parseConfig(destination.config);
+
+ return (
+
+ {/* Top row: icon + name/type + toggle */}
+
+
+
+
+
+
+
+ {cfg.name || "Unnamed destination"}
+
+
+ HTTP
+
+
+
+
+ onToggle(destination.destinationId, v)
+ }
+ disabled={isToggling || disabled}
+ className="shrink-0 mt-0.5"
+ />
+
+
+ {/* URL preview */}
+
+ {cfg.url || (
+ No URL configured
+ )}
+
+
+ {/* Footer: edit button */}
+
+
+
+
+ );
+}
+
+// ── Add destination card ───────────────────────────────────────────────────────
+
+function AddDestinationCard({
+ onClick,
+ disabled = false
+}: {
+ onClick: () => void;
+ disabled?: boolean;
+}) {
+ return (
+
+ );
+}
+
+// ── Destination modal ──────────────────────────────────────────────────────────
+
+interface DestinationModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ editing: Destination | null;
+ orgId: string;
+ onSaved: () => void;
+ onDeleted: () => void;
+}
+
+function DestinationModal({
+ open,
+ onOpenChange,
+ editing,
+ orgId,
+ onSaved,
+ onDeleted
+}: DestinationModalProps) {
+ const api = createApiClient(useEnvContext());
+
+ const [saving, setSaving] = useState(false);
+ const [deleting, setDeleting] = useState(false);
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ const [cfg, setCfg] = useState(defaultConfig());
+
+ useEffect(() => {
+ if (open) {
+ setCfg(editing ? parseConfig(editing.config) : defaultConfig());
+ setConfirmDelete(false);
+ }
+ }, [open, editing]);
+
+ const update = (patch: Partial) =>
+ setCfg((prev) => ({ ...prev, ...patch }));
+
+ const isValid =
+ cfg.name.trim() !== "" && cfg.url.trim() !== "";
+
+ async function handleSave() {
+ if (!isValid) return;
+ setSaving(true);
+ try {
+ const payload = {
+ type: "http",
+ config: JSON.stringify(cfg)
+ };
+ if (editing) {
+ await api.post(
+ `/org/${orgId}/event-streaming-destination/${editing.destinationId}`,
+ payload
+ );
+ toast({ title: "Destination updated successfully" });
+ } else {
+ await api.put(
+ `/org/${orgId}/event-streaming-destination`,
+ payload
+ );
+ toast({ title: "Destination created successfully" });
+ }
+ onSaved();
+ onOpenChange(false);
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: editing
+ ? "Failed to update destination"
+ : "Failed to create destination",
+ description: formatAxiosError(
+ e,
+ "An unexpected error occurred."
+ )
+ });
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handleDelete() {
+ if (!editing) return;
+ if (!confirmDelete) {
+ setConfirmDelete(true);
+ return;
+ }
+ setDeleting(true);
+ try {
+ await api.delete(
+ `/org/${orgId}/event-streaming-destination/${editing.destinationId}`
+ );
+ toast({ title: "Destination deleted successfully" });
+ onDeleted();
+ onOpenChange(false);
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: "Failed to delete destination",
+ description: formatAxiosError(
+ e,
+ "An unexpected error occurred."
+ )
+ });
+ } finally {
+ setDeleting(false);
+ }
+ }
+
+ return (
+
+
+
+
+ {editing ? "Edit Destination" : "Add Destination"}
+
+
+ {editing
+ ? "Update the configuration for this HTTP event streaming destination."
+ : "Configure a new HTTP endpoint to receive your organization's events."}
+
+
+
+
+
+
+ Settings
+ Headers
+ Body Template
+
+
+ {/* ── Settings ─────────────────────────────────── */}
+
+
+
+
+ update({ name: e.target.value })
+ }
+ />
+
+
+
+
+
+ update({ url: e.target.value })
+ }
+ />
+
+
+
+
+
+ update({ authType: v as AuthType })
+ }
+ className="gap-2"
+ >
+ {/* None */}
+
+
+
+
+
+ Sends requests without an{" "}
+
+ Authorization
+ {" "}
+ header.
+
+
+
+
+ {/* Bearer */}
+
+
+
+
+
+
+ Adds an{" "}
+
+ Authorization: Bearer <token>
+ {" "}
+ header to each request.
+
+
+ {cfg.authType === "bearer" && (
+
+ update({
+ bearerToken:
+ e.target.value
+ })
+ }
+ />
+ )}
+
+
+
+ {/* Basic */}
+
+
+
+
+
+
+ Adds an{" "}
+
+ Authorization: Basic <credentials>
+ {" "}
+ header. Provide credentials as{" "}
+
+ username:password
+
+ .
+
+
+ {cfg.authType === "basic" && (
+
+ update({
+ basicCredentials:
+ e.target.value
+ })
+ }
+ />
+ )}
+
+
+
+ {/* Custom */}
+
+
+
+
+
+
+ Specify a custom HTTP header name and value for
+ authentication (e.g.{" "}
+
+ X-API-Key
+
+ ).
+
+
+ {cfg.authType === "custom" && (
+
+
+ update({
+ customHeaderName:
+ e.target.value
+ })
+ }
+ className="flex-1"
+ />
+
+ update({
+ customHeaderValue:
+ e.target.value
+ })
+ }
+ className="flex-1"
+ />
+
+ )}
+
+
+
+
+
+
+ {/* ── Headers ───────────────────────────────────── */}
+
+
+
+ Custom HTTP Headers
+
+
+ Add custom HTTP headers to every outgoing request.
+ Useful for passing static tokens, setting a custom{" "}
+
+ Content-Type
+
+ , or other API requirements. By default, the{" "}
+
+ Content-Type
+ {" "}
+ is{" "}
+
+ application/json
+
+ .
+
+
update({ headers })}
+ />
+
+
+
+ {/* ── Body Template ─────────────────────────────── */}
+
+
+
+ Custom Body Template
+
+
+ Control the structure of the JSON payload sent to your
+ endpoint. If disabled, a default JSON object is sent for
+ each event.
+
+
+
+
+
+ update({ useBodyTemplate: v })
+ }
+ />
+
+
+
+ {cfg.useBodyTemplate && (
+
+
+
+ )}
+
+
+
+
+
+ {editing && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+// ── Main page ──────────────────────────────────────────────────────────────────
+
+export default function StreamingDestinationsPage() {
+ const { orgId } = useParams() as { orgId: string };
+ const api = createApiClient(useEnvContext());
+ const { isPaidUser } = usePaidStatus();
+ const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]);
+
+ const [destinations, setDestinations] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [editingDestination, setEditingDestination] =
+ useState(null);
+ const [togglingIds, setTogglingIds] = useState>(new Set());
+
+ const loadDestinations = useCallback(async () => {
+ if (build == "oss") {
+ setDestinations([]);
+ setLoading(false);
+ return;
+ }
+ try {
+ const res = await api.get>(
+ `/org/${orgId}/event-streaming-destinations`
+ );
+ setDestinations(res.data.data.destinations ?? []);
+ } catch (e) {
+ toast({
+ variant: "destructive",
+ title: "Failed to load destinations",
+ description: formatAxiosError(
+ e,
+ "An unexpected error occurred."
+ )
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, [orgId]);
+
+ useEffect(() => {
+ loadDestinations();
+ }, [loadDestinations]);
+
+ const handleToggle = async (destinationId: number, enabled: boolean) => {
+ // Optimistic update
+ setDestinations((prev) =>
+ prev.map((d) =>
+ d.destinationId === destinationId ? { ...d, enabled } : d
+ )
+ );
+ setTogglingIds((prev) => new Set(prev).add(destinationId));
+
+ try {
+ await api.post(
+ `/org/${orgId}/event-streaming-destination/${destinationId}`,
+ { enabled }
+ );
+ } catch (e) {
+ // Revert on failure
+ setDestinations((prev) =>
+ prev.map((d) =>
+ d.destinationId === destinationId
+ ? { ...d, enabled: !enabled }
+ : d
+ )
+ );
+ toast({
+ variant: "destructive",
+ title: "Failed to update destination",
+ description: formatAxiosError(
+ e,
+ "An unexpected error occurred."
+ )
+ });
+ } finally {
+ setTogglingIds((prev) => {
+ const next = new Set(prev);
+ next.delete(destinationId);
+ return next;
+ });
+ }
+ };
+
+ const openCreate = () => {
+ setEditingDestination(null);
+ setModalOpen(true);
+ };
+
+ const openEdit = (destination: Destination) => {
+ setEditingDestination(destination);
+ setModalOpen(true);
+ };
+
+ return (
+ <>
+
+
+
+
+ {loading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {destinations.map((dest) => (
+
+ ))}
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx
index 66e6cdad0..ac7a4a10f 100644
--- a/src/app/navigation.tsx
+++ b/src/app/navigation.tsx
@@ -23,6 +23,7 @@ import {
Settings,
SquareMousePointer,
TicketCheck,
+ Unplug,
User,
UserCog,
Users,
@@ -196,6 +197,11 @@ export const orgNavSections = (
title: "sidebarLogsConnection",
href: "/{orgId}/settings/logs/connection",
icon:
+ },
+ {
+ title: "sidebarLogsStreaming",
+ href: "/{orgId}/settings/logs/streaming",
+ icon:
}
]
: [])
From 45c613dec46108192b3ea2320e985d4497e02ac9 Mon Sep 17 00:00:00 2001
From: Owen
Date: Mon, 30 Mar 2026 21:09:14 -0700
Subject: [PATCH 03/16] Tweaking the ui
---
.../[orgId]/settings/logs/streaming/page.tsx | 85 +++++++++++--------
1 file changed, 50 insertions(+), 35 deletions(-)
diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx
index 265001e8e..c732274b4 100644
--- a/src/app/[orgId]/settings/logs/streaming/page.tsx
+++ b/src/app/[orgId]/settings/logs/streaming/page.tsx
@@ -7,6 +7,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
+import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
@@ -23,15 +24,10 @@ import { Button } from "@app/components/ui/button";
import { Input } from "@app/components/ui/input";
import { Label } from "@app/components/ui/label";
import { Switch } from "@app/components/ui/switch";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger
-} from "@app/components/ui/tabs";
+import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Textarea } from "@app/components/ui/textarea";
-import { Globe, Plus, Pencil, Trash2, X } from "lucide-react";
+import { Globe, Plus, Trash2, X } from "lucide-react";
import { AxiosResponse } from "axios";
import { build } from "@server/build";
@@ -217,15 +213,14 @@ function DestinationCard({
{/* Footer: edit button */}
-
+
@@ -286,13 +281,15 @@ function DestinationModal({
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
- const [confirmDelete, setConfirmDelete] = useState(false);
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [cfg, setCfg] = useState
(defaultConfig());
useEffect(() => {
if (open) {
setCfg(editing ? parseConfig(editing.config) : defaultConfig());
- setConfirmDelete(false);
+ }
+ if (!open) {
+ setDeleteDialogOpen(false);
}
}, [open, editing]);
@@ -343,10 +340,6 @@ function DestinationModal({
async function handleDelete() {
if (!editing) return;
- if (!confirmDelete) {
- setConfirmDelete(true);
- return;
- }
setDeleting(true);
try {
await api.delete(
@@ -370,6 +363,7 @@ function DestinationModal({
}
return (
+ <>
@@ -384,15 +378,16 @@ function DestinationModal({
-
-
- Settings
- Headers
- Body Template
-
-
+
{/* ── Settings ─────────────────────────────────── */}
-
+
{/* ── Headers ───────────────────────────────────── */}
-
+
Custom HTTP Headers
@@ -621,10 +616,10 @@ function DestinationModal({
onChange={(headers) => update({ headers })}
/>
-
+
{/* ── Body Template ─────────────────────────────── */}
-
+
Custom Body Template
@@ -676,8 +671,8 @@ function DestinationModal({
)}
-
-
+
+
@@ -685,13 +680,12 @@ function DestinationModal({
)}
@@ -714,6 +708,27 @@ function DestinationModal({
+
+ {editing && (
+
+ Are you sure you want to delete the destination{" "}
+
+ {parseConfig(editing.config).name || "this destination"}
+
+ ? All configuration will be permanently removed.
+
+ }
+ buttonText="Delete Destination"
+ onConfirm={handleDelete}
+ />
+ )}
+ >
);
}
From a73879ec7a55c6f164f661e361255e37227e005f Mon Sep 17 00:00:00 2001
From: Owen
Date: Mon, 30 Mar 2026 21:35:37 -0700
Subject: [PATCH 04/16] Fix formatting
---
.../updateEventStreamingDestination.ts | 24 +-
.../[orgId]/settings/logs/streaming/page.tsx | 233 +++++++++++++++---
2 files changed, 212 insertions(+), 45 deletions(-)
diff --git a/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
index 1ad8f0081..3d3321824 100644
--- a/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
+++ b/server/private/routers/eventStreamingDestination/updateEventStreamingDestination.ts
@@ -22,7 +22,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { and, eq } from "drizzle-orm";
-import { parse } from "zod/v4/core";
+
const paramsSchema = z
.object({
@@ -35,10 +35,10 @@ const bodySchema = z.strictObject({
type: z.string().optional(),
config: z.string().optional(),
enabled: z.boolean().optional(),
- sendConnectionLogs: z.boolean().optional().default(false),
- sendRequestLogs: z.boolean().optional().default(false),
- sendActionLogs: z.boolean().optional().default(false),
- sendAccessLogs: z.boolean().optional().default(false)
+ sendConnectionLogs: z.boolean().optional(),
+ sendRequestLogs: z.boolean().optional(),
+ sendActionLogs: z.boolean().optional(),
+ sendAccessLogs: z.boolean().optional()
});
export type UpdateEventStreamingDestinationResponse = {
@@ -110,7 +110,19 @@ export async function updateEventStreamingDestination(
);
}
- const updateData = parsedBody.data;
+ const { type, config, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
+
+ const updateData: Record = {
+ updatedAt: Date.now()
+ };
+
+ if (type !== undefined) updateData.type = type;
+ if (config !== undefined) updateData.config = config;
+ if (enabled !== undefined) updateData.enabled = enabled;
+ if (sendAccessLogs !== undefined) updateData.sendAccessLogs = sendAccessLogs;
+ if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;
+ if (sendConnectionLogs !== undefined) updateData.sendConnectionLogs = sendConnectionLogs;
+ if (sendRequestLogs !== undefined) updateData.sendRequestLogs = sendRequestLogs;
await db
.update(eventStreamingDestinations)
diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx
index c732274b4..a89c961b4 100644
--- a/src/app/[orgId]/settings/logs/streaming/page.tsx
+++ b/src/app/[orgId]/settings/logs/streaming/page.tsx
@@ -27,7 +27,8 @@ import { Switch } from "@app/components/ui/switch";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Textarea } from "@app/components/ui/textarea";
-import { Globe, Plus, Trash2, X } from "lucide-react";
+import { Checkbox } from "@app/components/ui/checkbox";
+import { Globe, Plus, X } from "lucide-react";
import { AxiosResponse } from "axios";
import { build } from "@server/build";
@@ -54,6 +55,10 @@ interface Destination {
type: string;
config: string;
enabled: boolean;
+ sendAccessLogs: boolean;
+ sendActionLogs: boolean;
+ sendConnectionLogs: boolean;
+ sendRequestLogs: boolean;
createdAt: number;
updatedAt: number;
}
@@ -215,7 +220,7 @@ function DestinationCard({
{/* Footer: edit button */}