diff --git a/apps/agent/Taskfile.yml b/apps/agent/Taskfile.yml index d39febc2a2..444ca5d876 100644 --- a/apps/agent/Taskfile.yml +++ b/apps/agent/Taskfile.yml @@ -25,10 +25,6 @@ tasks: cmds: - golangci-lint run - migrate: - cmds: - - goose -dir=./pkg/clickhouse/schema clickhouse "tcp://127.0.0.1:9000" up - generate: cmds: - go get github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen diff --git a/apps/agent/pkg/clickhouse/schema/000_README.md b/apps/agent/pkg/clickhouse/schema/000_README.md index 84e64014d2..95f3f26825 100644 --- a/apps/agent/pkg/clickhouse/schema/000_README.md +++ b/apps/agent/pkg/clickhouse/schema/000_README.md @@ -36,8 +36,8 @@ Examples: ### Aggregation Suffixes For aggregated or summary tables, use suffixes like: -- `_daily` -- `_monthly` +- `_per_day` +- `_per_month` - `_summary` ## Materialized View Naming Convention @@ -54,19 +54,19 @@ Format: `mv_[description]_[aggregation]` `raw_sales_transactions_v1` 2. Materialized View: - `mv_active_users_daily_v2` + `mv_active_users_per_day_v2` 3. Temporary Table: `tmp_andreas_user_analysis_v1` 4. Aggregated Table: - `mv_sales_summary_daily_v1` + `mv_sales_summary_per_hour_v1` ## Consistency Across Related Objects Maintain consistent naming across related tables, views, and other objects: - `raw_user_activity_v1` -- `mv_user_activity_daily_v1` +- `mv_user_activity_per_day_v1` By following these conventions, we ensure a clear, consistent, and scalable naming structure for our ClickHouse setup. diff --git a/apps/agent/pkg/clickhouse/schema/001_create_requests_table.sql b/apps/agent/pkg/clickhouse/schema/001_create_raw_api_requests_table.sql similarity index 100% rename from apps/agent/pkg/clickhouse/schema/001_create_requests_table.sql rename to apps/agent/pkg/clickhouse/schema/001_create_raw_api_requests_table.sql diff --git a/apps/agent/pkg/clickhouse/schema/002_create_key_verifications_table.sql b/apps/agent/pkg/clickhouse/schema/002_create_raw_key_verifications_table.sql similarity index 100% rename from apps/agent/pkg/clickhouse/schema/002_create_key_verifications_table.sql rename to apps/agent/pkg/clickhouse/schema/002_create_raw_key_verifications_table.sql diff --git a/apps/agent/pkg/clickhouse/schema/003_create_raw_telemetry_sdks_table.sql b/apps/agent/pkg/clickhouse/schema/003_create_raw_telemetry_sdks_table.sql new file mode 100644 index 0000000000..a318647fb1 --- /dev/null +++ b/apps/agent/pkg/clickhouse/schema/003_create_raw_telemetry_sdks_table.sql @@ -0,0 +1,19 @@ +-- +goose up +CREATE TABLE default.raw_telemetry_sdks_v1( + -- the api request id, so we can correlate the telemetry with traces and logs + request_id String, + + -- unix milli + time Int64, + + -- ie: node@20 + runtime String, + -- ie: vercel + platform String, + + -- ie: [ "@unkey/api@1.2.3", "@unkey/ratelimit@4.5.6" ] + versions Array(String) +) +ENGINE = MergeTree() +ORDER BY (request_id, time) +; diff --git a/apps/agent/pkg/clickhouse/schema/004_create_key_verifications_per_day.sql b/apps/agent/pkg/clickhouse/schema/004_create_key_verifications_per_day.sql new file mode 100644 index 0000000000..839a417ca7 --- /dev/null +++ b/apps/agent/pkg/clickhouse/schema/004_create_key_verifications_per_day.sql @@ -0,0 +1,14 @@ +-- +goose up +CREATE TABLE default.key_verifications_per_day_v1 +( + time DateTime, + workspace_id String, + key_space_id String, + identity_id String, + key_id String, + outcome LowCardinality(String), + count AggregateFunction(count, UInt64) +) +ENGINE = AggregatingMergeTree() +ORDER BY (workspace_id, key_space_id, time, identity_id, key_id) +; diff --git a/apps/agent/pkg/clickhouse/schema/005_create_mv_key_verifications_per_day.sql b/apps/agent/pkg/clickhouse/schema/005_create_mv_key_verifications_per_day.sql new file mode 100644 index 0000000000..efeddc0c6f --- /dev/null +++ b/apps/agent/pkg/clickhouse/schema/005_create_mv_key_verifications_per_day.sql @@ -0,0 +1,19 @@ +-- +goose up +CREATE MATERIALIZED VIEW default.mv_key_verifications_per_day_v1 TO default.key_verifications_per_day_v1 AS +SELECT + workspace_id, + key_space_id, + identity_id, + key_id, + outcome, + countState() as count, + toStartOfDay(fromUnixTimestamp64Milli(time)) AS time +FROM default.raw_key_verifications_v1 +GROUP BY + workspace_id, + key_space_id, + identity_id, + key_id, + outcome, + time +; diff --git a/apps/api/src/pkg/analytics.ts b/apps/api/src/pkg/analytics.ts index 66907d5b04..2cf78f3ea7 100644 --- a/apps/api/src/pkg/analytics.ts +++ b/apps/api/src/pkg/analytics.ts @@ -41,6 +41,19 @@ export class Analytics { this.clickhouse = opts.clickhouse ? new ch.Client({ url: opts.clickhouse.url }) : new ch.Noop(); } + public get insertSdkTelemetry() { + return this.clickhouse.insert({ + table: "default.raw_telemetry_sdks_v1", + schema: z.object({ + request_id: z.string(), + time: z.number().int(), + runtime: z.string(), + platform: z.string(), + versions: z.array(z.string()), + }), + }); + } + //tinybird, to be removed public get ingestSdkTelemetry() { return this.writeClient.buildIngestEndpoint({ datasource: "sdk_telemetry__v1", @@ -54,7 +67,8 @@ export class Analytics { }); } - public ingestUnkeyAuditLogs(logs: MaybeArray) { + //tinybird + public ingestUnkeyAuditLogsTinybird(logs: MaybeArray) { return this.writeClient.buildIngestEndpoint({ datasource: "audit_logs__v2", event: auditLogSchemaV1 @@ -78,7 +92,8 @@ export class Analytics { })(logs); } - public get ingestGenericAuditLogs() { + //tinybird + public get ingestGenericAuditLogsTinybird() { return this.writeClient.buildIngestEndpoint({ datasource: "audit_logs__v2", event: auditLogSchemaV1.transform((l) => ({ @@ -92,7 +107,7 @@ export class Analytics { })), }); } - + //tinybird public get ingestRatelimit() { return this.writeClient.buildIngestEndpoint({ datasource: "ratelimits__v2", @@ -146,7 +161,7 @@ export class Analytics { }), }); } - + // replaced by insertKeyVerification public get ingestKeyVerification() { return this.writeClient.buildIngestEndpoint({ datasource: "key_verifications__v2", diff --git a/apps/api/src/pkg/audit.ts b/apps/api/src/pkg/audit.ts new file mode 100644 index 0000000000..a77419be22 --- /dev/null +++ b/apps/api/src/pkg/audit.ts @@ -0,0 +1,106 @@ +import type { Context } from "@/pkg/hono/app"; +import { auditLogSchemaV1, unkeyAuditLogEvents } from "@unkey/schema/src/auditlog"; +import { type Transaction, schema } from "./db"; + +import { newId } from "@unkey/id"; +import { z } from "zod"; +import type { UnkeyAuditLog } from "./analytics"; + +export async function insertUnkeyAuditLog( + c: Context, + tx: Transaction | undefined, + auditLogs: UnkeyAuditLog | Array, +): Promise { + const schema = auditLogSchemaV1.merge( + z.object({ + event: unkeyAuditLogEvents, + auditLogId: z.string().default(newId("auditLog")), + bucket: z.string().default("unkey_mutations"), + time: z.number().default(Date.now()), + }), + ); + + const arr = Array.isArray(auditLogs) ? auditLogs : [auditLogs]; + return insertGenericAuditLogs( + c, + tx, + arr.map((l) => schema.parse(l)), + ); +} + +export async function insertGenericAuditLogs( + c: Context, + tx: Transaction | undefined, + auditLogs: z.infer | z.infer[], +): Promise { + const arr = Array.isArray(auditLogs) ? auditLogs : [auditLogs]; + + if (arr.length === 0) { + return; + } + + const { cache, logger, db } = c.get("services"); + + for (const log of arr) { + const { val: bucket, err } = await cache.auditLogBucketByWorkspaceIdAndName.swr( + [log.workspaceId, log.bucket].join(":"), + async () => { + const bucket = await (tx ?? db.primary).query.auditLogBucket.findFirst({ + where: (table, { eq, and }) => + and(eq(table.workspaceId, log.workspaceId), eq(table.name, log.bucket)), + }); + if (!bucket) { + return undefined; + } + return { + id: bucket.id, + }; + }, + ); + if (err) { + logger.error("Could not find audit log bucket for workspace", { + workspaceId: log.workspaceId, + error: err.message, + }); + continue; + } + + if (!bucket) { + logger.error("Could not find audit log bucket for workspace", { + workspaceId: log.workspaceId, + }); + continue; + } + + const auditLogId = newId("auditLog"); + await (tx ?? db.primary).insert(schema.auditLog).values({ + id: auditLogId, + workspaceId: log.workspaceId, + bucketId: bucket.id, + event: log.event, + time: log.time, + + display: log.description ?? "", + + remoteIp: log.context?.location, + + userAgent: log.context?.userAgent, + actorType: log.actor.type, + actorId: log.actor.id, + actorName: log.actor.name, + actorMeta: log.actor.meta, + }); + await (tx ?? db.primary).insert(schema.auditLogTarget).values( + log.resources.map((r) => ({ + workspaceId: log.workspaceId, + bucketId: bucket.id, + auditLogId, + displayName: r.name ?? "", + type: r.type, + id: r.id, + name: r.name, + meta: r.meta, + })), + ); + } +} diff --git a/apps/api/src/pkg/cache/index.ts b/apps/api/src/pkg/cache/index.ts index b78d75905e..28cd758b57 100644 --- a/apps/api/src/pkg/cache/index.ts +++ b/apps/api/src/pkg/cache/index.ts @@ -71,6 +71,9 @@ export function initCache(c: Context, metrics: Metrics): C(c.executionCtx, defaultOpts), }); } diff --git a/apps/api/src/pkg/cache/namespaces.ts b/apps/api/src/pkg/cache/namespaces.ts index 09dd93397d..42550b1871 100644 --- a/apps/api/src/pkg/cache/namespaces.ts +++ b/apps/api/src/pkg/cache/namespaces.ts @@ -64,6 +64,10 @@ export type CacheNamespaces = { total: number; }; identityByExternalId: Identity | null; + // uses a compound key of [workspaceId, name] + auditLogBucketByWorkspaceIdAndName: { + id: string; + }; }; export type CacheNamespace = keyof CacheNamespaces; diff --git a/apps/api/src/pkg/db.ts b/apps/api/src/pkg/db.ts index 359a3f038d..068bffa143 100644 --- a/apps/api/src/pkg/db.ts +++ b/apps/api/src/pkg/db.ts @@ -1,8 +1,7 @@ import { Client } from "@planetscale/database"; -import { type PlanetScaleDatabase, drizzle, schema } from "@unkey/db"; +import { type Database, drizzle, schema } from "@unkey/db"; import type { Logger } from "@unkey/worker-logging"; import { instrumentedFetch } from "./util/instrument-fetch"; -export type Database = PlanetScaleDatabase; type ConnectionOptions = { host: string; diff --git a/apps/api/src/pkg/key_migration/handler.ts b/apps/api/src/pkg/key_migration/handler.ts index 099058c7ad..d3875a8837 100644 --- a/apps/api/src/pkg/key_migration/handler.ts +++ b/apps/api/src/pkg/key_migration/handler.ts @@ -104,7 +104,7 @@ export async function migrateKey( environment: message.environment, }); - await analytics.ingestUnkeyAuditLogs({ + await analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: message.workspaceId, event: "key.create", actor: { @@ -149,7 +149,7 @@ export async function migrateKey( await tx.insert(schema.keysRoles).values(roleConnections); - await analytics.ingestUnkeyAuditLogs( + await analytics.ingestUnkeyAuditLogsTinybird( roleConnections.map((rc) => ({ workspaceId: message.workspaceId, actor: { type: "key", id: message.rootKeyId }, @@ -184,7 +184,7 @@ export async function migrateKey( await tx.insert(schema.keysPermissions).values(permissionConnections); - await analytics.ingestUnkeyAuditLogs( + await analytics.ingestUnkeyAuditLogsTinybird( permissionConnections.map((pc) => ({ workspaceId: message.workspaceId, actor: { type: "key", id: message.rootKeyId }, diff --git a/apps/api/src/pkg/middleware/metrics.ts b/apps/api/src/pkg/middleware/metrics.ts index f612c1d72d..87be7f43c4 100644 --- a/apps/api/src/pkg/middleware/metrics.ts +++ b/apps/api/src/pkg/middleware/metrics.ts @@ -53,7 +53,7 @@ export function metrics(): MiddlewareHandler { c.executionCtx.waitUntil( analytics.ingestSdkTelemetry(event).catch((err) => { - logger.error("Error ingesting SDK telemetry", { + logger.error("Error ingesting SDK telemetry into tinybird", { method: c.req.method, path: c.req.path, error: err.message, @@ -62,6 +62,22 @@ export function metrics(): MiddlewareHandler { }); }), ); + c.executionCtx.waitUntil( + analytics + .insertSdkTelemetry({ + ...event, + request_id: event.requestId, + }) + .catch((err) => { + logger.error("Error inserting SDK telemetry", { + method: c.req.method, + path: c.req.path, + error: err.message, + telemetry, + event, + }); + }), + ); } await next(); diff --git a/apps/api/src/routes/legacy_keys_createKey.ts b/apps/api/src/routes/legacy_keys_createKey.ts index 6f8f557019..96ef64f307 100644 --- a/apps/api/src/routes/legacy_keys_createKey.ts +++ b/apps/api/src/routes/legacy_keys_createKey.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -227,7 +228,28 @@ export const registerLegacyKeysCreate = (app: App) => deletedAt: null, }); - await analytics.ingestUnkeyAuditLogs({ + await analytics.ingestUnkeyAuditLogsTinybird({ + workspaceId: authorizedWorkspaceId, + actor: { type: "key", id: rootKeyId }, + event: "key.create", + description: `Created ${keyId}`, + resources: [ + { + type: "key", + id: keyId, + }, + { + type: "keyAuth", + id: api.keyAuthId!, + }, + { type: "api", id: api.id }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }); + await insertUnkeyAuditLog(c, tx, { workspaceId: authorizedWorkspaceId, actor: { type: "key", id: rootKeyId }, event: "key.create", diff --git a/apps/api/src/routes/v1_apis_createApi.ts b/apps/api/src/routes/v1_apis_createApi.ts index 313fd6de0d..811328063a 100644 --- a/apps/api/src/routes/v1_apis_createApi.ts +++ b/apps/api/src/routes/v1_apis_createApi.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -81,31 +82,51 @@ export const registerV1ApisCreateApi = (app: App) => */ const apiId = newId("api"); - await db.primary.insert(schema.apis).values({ - id: apiId, - name, - workspaceId: authorizedWorkspaceId, - authType: "key", - keyAuthId: keyAuth.id, - createdAt: new Date(), - deletedAt: null, - }); - await analytics.ingestUnkeyAuditLogs({ - workspaceId: authorizedWorkspaceId, - event: "api.create", - actor: { - type: "key", - id: rootKeyId, - }, - description: `Created ${apiId}`, - resources: [ - { - type: "api", - id: apiId, + await db.primary.transaction(async (tx) => { + await tx.insert(schema.apis).values({ + id: apiId, + name, + workspaceId: authorizedWorkspaceId, + authType: "key", + keyAuthId: keyAuth.id, + createdAt: new Date(), + deletedAt: null, + }); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: authorizedWorkspaceId, + event: "api.create", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Created ${apiId}`, + resources: [ + { + type: "api", + id: apiId, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + await analytics.ingestUnkeyAuditLogsTinybird({ + workspaceId: authorizedWorkspaceId, + event: "api.create", + actor: { + type: "key", + id: rootKeyId, }, - ], + description: `Created ${apiId}`, + resources: [ + { + type: "api", + id: apiId, + }, + ], - context: { location: c.get("location"), userAgent: c.get("userAgent") }, + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); }); return c.json({ diff --git a/apps/api/src/routes/v1_apis_deleteApi.ts b/apps/api/src/routes/v1_apis_deleteApi.ts index 189ae84330..c7eba212c7 100644 --- a/apps/api/src/routes/v1_apis_deleteApi.ts +++ b/apps/api/src/routes/v1_apis_deleteApi.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, errorSchemaFactory, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -96,7 +97,24 @@ export const registerV1ApisDeleteApi = (app: App) => await db.primary.transaction(async (tx) => { await tx.update(schema.apis).set({ deletedAt: new Date() }).where(eq(schema.apis.id, apiId)); - await analytics.ingestUnkeyAuditLogs({ + await analytics.ingestUnkeyAuditLogsTinybird({ + workspaceId: authorizedWorkspaceId, + event: "api.delete", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Deleted ${apiId}`, + resources: [ + { + type: "api", + id: apiId, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + await insertUnkeyAuditLog(c, tx, { workspaceId: authorizedWorkspaceId, event: "api.delete", actor: { @@ -126,7 +144,32 @@ export const registerV1ApisDeleteApi = (app: App) => .set({ deletedAt: new Date() }) .where(and(eq(schema.keys.keyAuthId, api.keyAuthId!))); - await analytics.ingestUnkeyAuditLogs( + await insertUnkeyAuditLog( + c, + tx, + keyIds.map((key) => ({ + workspaceId: authorizedWorkspaceId, + event: "key.delete", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Deleted ${key.id} as part of ${api.id} deletion`, + resources: [ + { + type: "keyAuth", + id: api.keyAuthId!, + }, + { + type: "key", + id: key.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + })), + ); + await analytics.ingestUnkeyAuditLogsTinybird( keyIds.map((key) => ({ workspaceId: authorizedWorkspaceId, event: "key.delete", diff --git a/apps/api/src/routes/v1_identities_createIdentity.ts b/apps/api/src/routes/v1_identities_createIdentity.ts index 593861eb64..cfcf154921 100644 --- a/apps/api/src/routes/v1_identities_createIdentity.ts +++ b/apps/api/src/routes/v1_identities_createIdentity.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { DatabaseError } from "@planetscale/database"; @@ -127,35 +128,35 @@ export const registerV1IdentitiesCreateIdentity = (app: App) => environment: "default", meta: req.meta, }; - await db.primary - .insert(schema.identities) - .values(identity) - .catch((e) => { - if (e instanceof DatabaseError && e.body.message.includes("desc = Duplicate entry")) { - throw new UnkeyApiError({ - code: "PRECONDITION_FAILED", - message: "Duplicate identity ", - }); - } - }); - - const ratelimits = req.ratelimits - ? req.ratelimits.map((r) => ({ - id: newId("ratelimit"), - identityId: identity.id, - workspaceId: auth.authorizedWorkspaceId, - name: r.name, - limit: r.limit, - duration: r.duration, - })) - : []; - - if (ratelimits.length > 0) { - await db.primary.insert(schema.ratelimits).values(ratelimits); - } - - c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ + await db.primary.transaction(async (tx) => { + await tx + .insert(schema.identities) + .values(identity) + .catch((e) => { + if (e instanceof DatabaseError && e.body.message.includes("desc = Duplicate entry")) { + throw new UnkeyApiError({ + code: "PRECONDITION_FAILED", + message: "Duplicate identity ", + }); + } + }); + + const ratelimits = req.ratelimits + ? req.ratelimits.map((r) => ({ + id: newId("ratelimit"), + identityId: identity.id, + workspaceId: auth.authorizedWorkspaceId, + name: r.name, + limit: r.limit, + duration: r.duration, + })) + : []; + + if (ratelimits.length > 0) { + await tx.insert(schema.ratelimits).values(ratelimits); + } + + await insertUnkeyAuditLog(c, tx, [ { workspaceId: authorizedWorkspaceId, event: "identity.create", @@ -197,8 +198,54 @@ export const registerV1IdentitiesCreateIdentity = (app: App) => context: { location: c.get("location"), userAgent: c.get("userAgent") }, })), - ]), - ); + ]); + + c.executionCtx.waitUntil( + analytics.ingestUnkeyAuditLogsTinybird([ + { + workspaceId: authorizedWorkspaceId, + event: "identity.create", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Created ${identity.id}`, + resources: [ + { + type: "identity", + id: identity.id, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }, + ...ratelimits.map((r) => ({ + workspaceId: authorizedWorkspaceId, + event: "ratelimit.create" as const, + actor: { + type: "key" as const, + id: rootKeyId, + }, + description: `Created ${r.id}`, + resources: [ + { + type: "identity" as const, + id: identity.id, + }, + { + type: "ratelimit" as const, + id: r.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + })), + ]), + ); + }); return c.json({ identityId: identity.id, diff --git a/apps/api/src/routes/v1_identities_deleteIdentity.ts b/apps/api/src/routes/v1_identities_deleteIdentity.ts index afbdb943ac..1913572ab6 100644 --- a/apps/api/src/routes/v1_identities_deleteIdentity.ts +++ b/apps/api/src/routes/v1_identities_deleteIdentity.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { eq, inArray, schema } from "@unkey/db"; @@ -86,7 +87,54 @@ export const registerV1IdentitiesDeleteIdentity = (app: App) => await tx.delete(schema.identities).where(eq(schema.identities.id, identity.id)); - await analytics.ingestUnkeyAuditLogs([ + await insertUnkeyAuditLog(c, tx, [ + { + workspaceId: authorizedWorkspaceId, + event: "identity.delete", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Deleted ${identity.id}`, + resources: [ + { + type: "identity", + id: identity.id, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }, + + ...deleteRatelimitIds.map((ratelimitId) => ({ + workspaceId: authorizedWorkspaceId, + event: "ratelimit.delete" as const, + actor: { + type: "key" as const, + id: rootKeyId, + }, + description: `Deleted ${ratelimitId}`, + resources: [ + { + type: "identity" as const, + id: identity.id, + }, + { + type: "ratelimit" as const, + id: ratelimitId, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ]); + await analytics.ingestUnkeyAuditLogsTinybird([ { workspaceId: authorizedWorkspaceId, event: "identity.delete", diff --git a/apps/api/src/routes/v1_identities_updateIdentity.ts b/apps/api/src/routes/v1_identities_updateIdentity.ts index 03a44eafa0..ff20ff12c8 100644 --- a/apps/api/src/routes/v1_identities_updateIdentity.ts +++ b/apps/api/src/routes/v1_identities_updateIdentity.ts @@ -2,6 +2,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; import type { UnkeyAuditLog } from "@/pkg/analytics"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { type Ratelimit, eq, schema } from "@unkey/db"; @@ -384,7 +385,8 @@ export const registerV1IdentitiesUpdateIdentity = (app: App) => return identityAfterUpdate!; }); - c.executionCtx.waitUntil(analytics.ingestUnkeyAuditLogs(auditLogs)); + await insertUnkeyAuditLog(c, undefined, auditLogs); + c.executionCtx.waitUntil(analytics.ingestUnkeyAuditLogsTinybird(auditLogs)); return c.json({ id: identity.id, diff --git a/apps/api/src/routes/v1_keys_addPermissions.ts b/apps/api/src/routes/v1_keys_addPermissions.ts index 8cf90cb9f6..5acbab8474 100644 --- a/apps/api/src/routes/v1_keys_addPermissions.ts +++ b/apps/api/src/routes/v1_keys_addPermissions.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -203,8 +204,58 @@ export const registerV1KeysAddPermissions = (app: App) => Promise.all([cache.keyById.remove(key.id), cache.keyByHash.remove(key.hash)]), ); + await insertUnkeyAuditLog(c, undefined, [ + ...createPermissions.map((p) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "permission.create" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Created ${p.id}`, + resources: [ + { + type: "permission" as const, + id: p.id, + meta: { + name: p.name, + }, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addPermissions.map((p) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_permission_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${p.id} and ${req.keyId}`, + resources: [ + { + type: "permission" as const, + id: p.id, + }, + { + type: "key" as const, + id: req.keyId, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ]); + c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ + analytics.ingestUnkeyAuditLogsTinybird([ ...createPermissions.map((p) => ({ workspaceId: auth.authorizedWorkspaceId, event: "permission.create" as const, diff --git a/apps/api/src/routes/v1_keys_addRoles.ts b/apps/api/src/routes/v1_keys_addRoles.ts index 1174c0fcf9..b19f2013fc 100644 --- a/apps/api/src/routes/v1_keys_addRoles.ts +++ b/apps/api/src/routes/v1_keys_addRoles.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -213,8 +214,58 @@ export const registerV1KeysAddRoles = (app: App) => Promise.all([cache.keyById.remove(key.id), cache.keyByHash.remove(key.hash)]), ); + await insertUnkeyAuditLog(c, undefined, [ + ...createRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "role.create" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Created ${r.id}`, + resources: [ + { + type: "role" as const, + id: r.id, + meta: { + name: r.name, + }, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_role_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${r.id} and ${req.keyId}`, + resources: [ + { + type: "role" as const, + id: r.id, + }, + { + type: "key" as const, + id: req.keyId, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ]); + c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ + analytics.ingestUnkeyAuditLogsTinybird([ ...createRoles.map((r) => ({ workspaceId: auth.authorizedWorkspaceId, event: "role.create" as const, diff --git a/apps/api/src/routes/v1_keys_createKey.ts b/apps/api/src/routes/v1_keys_createKey.ts index 8780846e95..4841dfb36c 100644 --- a/apps/api/src/routes/v1_keys_createKey.ts +++ b/apps/api/src/routes/v1_keys_createKey.ts @@ -2,6 +2,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; import type { UnkeyAuditLog } from "@/pkg/analytics"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import type { Database, Identity } from "@/pkg/db"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; @@ -431,80 +432,81 @@ export const registerV1KeysCreateKey = (app: App) => : Promise.resolve(), ]); - c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ - { - workspaceId: authorizedWorkspaceId, - event: "key.create", - actor: { + const auditLogs: UnkeyAuditLog[] = [ + { + workspaceId: authorizedWorkspaceId, + event: "key.create", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Created ${newKey.id} in ${api.keyAuthId}`, + resources: [ + { type: "key", - id: rootKeyId, + id: newKey.id, }, - description: `Created ${newKey.id} in ${api.keyAuthId}`, - resources: [ - { - type: "key", - id: newKey.id, - }, - { - type: "keyAuth", - id: api.keyAuthId!, - }, - ], - - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + { + type: "keyAuth", + id: api.keyAuthId!, }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), }, - ...roleIds.map( - (roleId) => - ({ - workspaceId: authorizedWorkspaceId, - actor: { type: "key", id: rootKeyId }, - event: "authorization.connect_role_and_key", - description: `Connected ${roleId} and ${newKey.id}`, - resources: [ - { - type: "key", - id: newKey.id, - }, - { - type: "role", - id: roleId, - }, - ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + }, + ...roleIds.map( + (roleId) => + ({ + workspaceId: authorizedWorkspaceId, + actor: { type: "key", id: rootKeyId }, + event: "authorization.connect_role_and_key", + description: `Connected ${roleId} and ${newKey.id}`, + resources: [ + { + type: "key", + id: newKey.id, }, - }) satisfies UnkeyAuditLog, - ), - ...permissionIds.map( - (permissionId) => - ({ - workspaceId: authorizedWorkspaceId, - actor: { type: "key", id: rootKeyId }, - event: "authorization.connect_permission_and_key", - description: `Connected ${permissionId} and ${newKey.id}`, - resources: [ - { - type: "key", - id: newKey.id, - }, - { - type: "permission", - id: permissionId, - }, - ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + { + type: "role", + id: roleId, }, - }) satisfies UnkeyAuditLog, - ), - ]), - ); + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }) satisfies UnkeyAuditLog, + ), + ...permissionIds.map( + (permissionId) => + ({ + workspaceId: authorizedWorkspaceId, + actor: { type: "key", id: rootKeyId }, + event: "authorization.connect_permission_and_key", + description: `Connected ${permissionId} and ${newKey.id}`, + resources: [ + { + type: "key", + id: newKey.id, + }, + { + type: "permission", + id: permissionId, + }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }) satisfies UnkeyAuditLog, + ), + ]; + + await insertUnkeyAuditLog(c, undefined, auditLogs); + c.executionCtx.waitUntil(analytics.ingestUnkeyAuditLogsTinybird(auditLogs)); // TODO: emit event to tinybird return c.json({ diff --git a/apps/api/src/routes/v1_keys_deleteKey.ts b/apps/api/src/routes/v1_keys_deleteKey.ts index 60b0c89ee7..e7ecb67ee5 100644 --- a/apps/api/src/routes/v1_keys_deleteKey.ts +++ b/apps/api/src/routes/v1_keys_deleteKey.ts @@ -2,6 +2,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; import { schema } from "@unkey/db"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { eq } from "@unkey/db"; @@ -122,12 +123,28 @@ export const registerV1KeysDeleteKey = (app: App) => const authorizedWorkspaceId = auth.authorizedWorkspaceId; const rootKeyId = auth.key.id; - await db.primary - .update(schema.keys) - .set({ deletedAt: new Date() }) - .where(eq(schema.keys.id, key.id)); + await db.primary.transaction(async (tx) => { + await tx.update(schema.keys).set({ deletedAt: new Date() }).where(eq(schema.keys.id, key.id)); - await analytics.ingestUnkeyAuditLogs({ + await insertUnkeyAuditLog(c, tx, { + workspaceId: authorizedWorkspaceId, + event: "key.delete", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Deleted ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + }); + await analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: authorizedWorkspaceId, event: "key.delete", actor: { diff --git a/apps/api/src/routes/v1_keys_removePermissions.ts b/apps/api/src/routes/v1_keys_removePermissions.ts index 2f300c146f..1bc26f3c7d 100644 --- a/apps/api/src/routes/v1_keys_removePermissions.ts +++ b/apps/api/src/routes/v1_keys_removePermissions.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, inArray, schema } from "@unkey/db"; @@ -118,19 +119,50 @@ export const registerV1KeysRemovePermissions = (app: App) => // We have nothing to do return c.json({}); } - await db.primary.delete(schema.keysPermissions).where( - and( - eq(schema.keysPermissions.workspaceId, auth.authorizedWorkspaceId), - eq(schema.keysPermissions.keyId, key.id), - inArray( - schema.keysPermissions.permissionId, - deletePermissions.map((r) => r.permissionId), + await db.primary.transaction(async (tx) => { + await tx.delete(schema.keysPermissions).where( + and( + eq(schema.keysPermissions.workspaceId, auth.authorizedWorkspaceId), + eq(schema.keysPermissions.keyId, key.id), + inArray( + schema.keysPermissions.permissionId, + deletePermissions.map((r) => r.permissionId), + ), ), - ), - ); + ); + + await insertUnkeyAuditLog( + c, + tx, + deletePermissions.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.disconnect_permission_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Disonnected ${r.permissionId} and ${req.keyId}`, + resources: [ + { + type: "permission" as const, + id: r.permissionId, + }, + { + type: "key" as const, + id: req.keyId, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ); + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs( + analytics.ingestUnkeyAuditLogsTinybird( deletePermissions.map((r) => ({ workspaceId: auth.authorizedWorkspaceId, event: "authorization.disconnect_permission_and_key" as const, diff --git a/apps/api/src/routes/v1_keys_removeRoles.ts b/apps/api/src/routes/v1_keys_removeRoles.ts index 5459075d27..9bead7da48 100644 --- a/apps/api/src/routes/v1_keys_removeRoles.ts +++ b/apps/api/src/routes/v1_keys_removeRoles.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, inArray, schema } from "@unkey/db"; @@ -118,19 +119,49 @@ export const registerV1KeysRemoveRoles = (app: App) => // We have nothing to do return c.json({}); } - await db.primary.delete(schema.keysRoles).where( - and( - eq(schema.keysRoles.workspaceId, auth.authorizedWorkspaceId), - eq(schema.keysRoles.keyId, key.id), - inArray( - schema.keysRoles.roleId, - deleteRoles.map((r) => r.roleId), + await db.primary.transaction(async (tx) => { + await tx.delete(schema.keysRoles).where( + and( + eq(schema.keysRoles.workspaceId, auth.authorizedWorkspaceId), + eq(schema.keysRoles.keyId, key.id), + inArray( + schema.keysRoles.roleId, + deleteRoles.map((r) => r.roleId), + ), ), - ), - ); + ); + await insertUnkeyAuditLog( + c, + tx, + deleteRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.disconnect_role_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Disonnected ${r.roleId} and ${req.keyId}`, + resources: [ + { + type: "role" as const, + id: r.roleId, + }, + { + type: "key" as const, + id: req.keyId, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ); + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs( + analytics.ingestUnkeyAuditLogsTinybird( deleteRoles.map((r) => ({ workspaceId: auth.authorizedWorkspaceId, event: "authorization.disconnect_role_and_key" as const, diff --git a/apps/api/src/routes/v1_keys_setPermissions.ts b/apps/api/src/routes/v1_keys_setPermissions.ts index 0fba42f856..e469010963 100644 --- a/apps/api/src/routes/v1_keys_setPermissions.ts +++ b/apps/api/src/routes/v1_keys_setPermissions.ts @@ -1,6 +1,7 @@ import type { App, Context } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, inArray, schema } from "@unkey/db"; @@ -289,105 +290,104 @@ export async function setPermissions( c.executionCtx.waitUntil( Promise.all([cache.keyById.remove(key.id), cache.keyByHash.remove(key.hash)]), ); - - c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ - ...disconnectPermissions.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.disconnect_permission_and_key" as const, - actor: { - type: "key" as const, - id: auth.key.id, - }, - description: `Disconnected ${r.permissionId} and ${key.id}`, - resources: [ - { - type: "permission" as const, - id: r.permissionId, - }, - { - type: "key" as const, - id: key.id, - }, - ], - - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + const auditLogs = [ + ...disconnectPermissions.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.disconnect_permission_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Disconnected ${r.permissionId} and ${key.id}`, + resources: [ + { + type: "permission" as const, + id: r.permissionId, }, - })), - ...addPermissions.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.connect_permission_and_key" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: key.id, }, - description: `Connected ${r.id} and ${keyId}`, - resources: [ - { - type: "permission" as const, - id: r.id, - }, - { - type: "key" as const, - id: keyId, - }, - ], + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addPermissions.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_permission_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${r.id} and ${keyId}`, + resources: [ + { + type: "permission" as const, + id: r.id, }, - })), - ...createPermissions.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "permission.create" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: keyId, }, - description: `Created ${r.id}`, - resources: [ - { - type: "permission" as const, - id: r.id, - meta: { - name: r.name, - }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...createPermissions.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "permission.create" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Created ${r.id}`, + resources: [ + { + type: "permission" as const, + id: r.id, + meta: { + name: r.name, }, - ], + }, + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addPermissions.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_permission_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${r.id} and ${keyId}`, + resources: [ + { + type: "permission" as const, + id: r.id, }, - })), - ...addPermissions.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.connect_permission_and_key" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: keyId, }, - description: `Connected ${r.id} and ${keyId}`, - resources: [ - { - type: "permission" as const, - id: r.id, - }, - { - type: "key" as const, - id: keyId, - }, - ], + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), - }, - })), - ]), - ); + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ]; + await insertUnkeyAuditLog(c, undefined, auditLogs); + c.executionCtx.waitUntil(analytics.ingestUnkeyAuditLogsTinybird(auditLogs)); return allPermissions; } diff --git a/apps/api/src/routes/v1_keys_setRoles.ts b/apps/api/src/routes/v1_keys_setRoles.ts index 4aeee596ff..09f72338ab 100644 --- a/apps/api/src/routes/v1_keys_setRoles.ts +++ b/apps/api/src/routes/v1_keys_setRoles.ts @@ -1,6 +1,7 @@ import type { App, Context } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, inArray, schema } from "@unkey/db"; @@ -284,104 +285,105 @@ export async function setRoles( Promise.all([cache.keyById.remove(key.id), cache.keyByHash.remove(key.hash)]), ); - c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs([ - ...disconnectRoles.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.disconnect_role_and_key" as const, - actor: { - type: "key" as const, - id: auth.key.id, - }, - description: `Disconnected ${r.roleId} and ${key.id}`, - resources: [ - { - type: "role" as const, - id: r.roleId, - }, - { - type: "key" as const, - id: key.id, - }, - ], - - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + const auditLogs = [ + ...disconnectRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.disconnect_role_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Disconnected ${r.roleId} and ${key.id}`, + resources: [ + { + type: "role" as const, + id: r.roleId, }, - })), - ...addRoles.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.connect_role_and_key" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: key.id, }, - description: `Connected ${r.id} and ${keyId}`, - resources: [ - { - type: "role" as const, - id: r.id, - }, - { - type: "key" as const, - id: keyId, - }, - ], + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_role_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${r.id} and ${keyId}`, + resources: [ + { + type: "role" as const, + id: r.id, }, - })), - ...createRoles.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "role.create" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: keyId, }, - description: `Created ${r.id}`, - resources: [ - { - type: "role" as const, - id: r.id, - meta: { - name: r.name, - }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...createRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "role.create" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Created ${r.id}`, + resources: [ + { + type: "role" as const, + id: r.id, + meta: { + name: r.name, }, - ], + }, + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ...addRoles.map((r) => ({ + workspaceId: auth.authorizedWorkspaceId, + event: "authorization.connect_role_and_key" as const, + actor: { + type: "key" as const, + id: auth.key.id, + }, + description: `Connected ${r.id} and ${keyId}`, + resources: [ + { + type: "role" as const, + id: r.id, }, - })), - ...addRoles.map((r) => ({ - workspaceId: auth.authorizedWorkspaceId, - event: "authorization.connect_role_and_key" as const, - actor: { + { type: "key" as const, - id: auth.key.id, + id: keyId, }, - description: `Connected ${r.id} and ${keyId}`, - resources: [ - { - type: "role" as const, - id: r.id, - }, - { - type: "key" as const, - id: keyId, - }, - ], + ], - context: { - location: c.get("location"), - userAgent: c.get("userAgent"), - }, - })), - ]), - ); + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ]; + await insertUnkeyAuditLog(c, undefined, auditLogs); + + c.executionCtx.waitUntil(analytics.ingestUnkeyAuditLogsTinybird(auditLogs)); return allRoles; } diff --git a/apps/api/src/routes/v1_keys_updateKey.ts b/apps/api/src/routes/v1_keys_updateKey.ts index df68785fca..a2c90fb72d 100644 --- a/apps/api/src/routes/v1_keys_updateKey.ts +++ b/apps/api/src/routes/v1_keys_updateKey.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -381,11 +382,40 @@ export const registerV1KeysUpdate = (app: App) => }) .where(eq(schema.keys.id, key.id)); + await insertUnkeyAuditLog(c, undefined, { + workspaceId: authorizedWorkspaceId, + event: "key.update", + actor: { + type: "key", + id: rootKeyId, + }, + description: `Updated key ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + meta: Object.entries(req) + .filter(([_key, value]) => typeof value !== "undefined") + .reduce( + (obj, [key, value]) => { + obj[key] = JSON.stringify(value); + + return obj; + }, + {} as Record, + ), + }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }); c.executionCtx.waitUntil(usageLimiter.revalidate({ keyId: key.id })); c.executionCtx.waitUntil(cache.keyByHash.remove(key.hash)); c.executionCtx.waitUntil(cache.keyById.remove(key.id)); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: authorizedWorkspaceId, event: "key.update", actor: { diff --git a/apps/api/src/routes/v1_keys_updateRemaining.ts b/apps/api/src/routes/v1_keys_updateRemaining.ts index 613645ed08..5da08ab14a 100644 --- a/apps/api/src/routes/v1_keys_updateRemaining.ts +++ b/apps/api/src/routes/v1_keys_updateRemaining.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { eq, schema, sql } from "@/pkg/db"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; @@ -169,8 +170,33 @@ export const registerV1KeysUpdateRemaining = (app: App) => message: "key not found after update, this should not happen", }); } + + await insertUnkeyAuditLog(c, undefined, { + actor: { + type: "key", + id: rootKeyId, + }, + event: "key.update", + workspaceId: authorizedWorkspaceId, + description: `Changed remaining to ${keyAfterUpdate.remaining}`, + resources: [ + { + type: "keyAuth", + id: key.keyAuthId, + }, + { + type: "key", + id: key.id, + }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }); + c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ actor: { type: "key", id: rootKeyId, diff --git a/apps/api/src/routes/v1_migrations_createKey.ts b/apps/api/src/routes/v1_migrations_createKey.ts index bd8a201f84..50914bc9d0 100644 --- a/apps/api/src/routes/v1_migrations_createKey.ts +++ b/apps/api/src/routes/v1_migrations_createKey.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { retry } from "@/pkg/util/retry"; @@ -496,7 +497,9 @@ export const registerV1MigrationsCreateKeys = (app: App) => await tx.insert(schema.keysPermissions).values(permissionConnections); } - await analytics.ingestUnkeyAuditLogs( + await insertUnkeyAuditLog( + c, + tx, keys.map((k) => ({ workspaceId: authorizedWorkspaceId, event: "key.create", @@ -523,7 +526,58 @@ export const registerV1MigrationsCreateKeys = (app: App) => })), ); - await analytics.ingestUnkeyAuditLogs( + await analytics.ingestUnkeyAuditLogsTinybird( + keys.map((k) => ({ + workspaceId: authorizedWorkspaceId, + event: "key.create", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Created ${k.id} in ${k.keyAuthId}`, + resources: [ + { + type: "key", + id: k.id, + }, + { + type: "keyAuth", + id: k.keyAuthId!, + }, + ], + + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ); + + await insertUnkeyAuditLog( + c, + tx, + roleConnections.map((rc) => ({ + workspaceId: authorizedWorkspaceId, + actor: { type: "key", id: rootKeyId }, + event: "authorization.connect_role_and_key", + description: `Connected ${rc.roleId} and ${rc.keyId}`, + resources: [ + { + type: "key", + id: rc.keyId, + }, + { + type: "role", + id: rc.roleId, + }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + })), + ); + await analytics.ingestUnkeyAuditLogsTinybird( roleConnections.map((rc) => ({ workspaceId: authorizedWorkspaceId, actor: { type: "key", id: rootKeyId }, diff --git a/apps/api/src/routes/v1_permissions_createPermission.ts b/apps/api/src/routes/v1_permissions_createPermission.ts index dd0ae01e65..a89c536059 100644 --- a/apps/api/src/routes/v1_permissions_createPermission.ts +++ b/apps/api/src/routes/v1_permissions_createPermission.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -82,18 +83,42 @@ export const registerV1PermissionsCreatePermission = (app: App) => name: req.name, description: req.description, }; - await db.primary - .insert(schema.permissions) - .values(permission) - .onDuplicateKeyUpdate({ - set: { - name: req.name, - description: req.description, + await db.primary.transaction(async (tx) => { + await tx + .insert(schema.permissions) + .values(permission) + .onDuplicateKeyUpdate({ + set: { + name: req.name, + description: req.description, + }, + }); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "permission.create", + actor: { + type: "key", + id: auth.key.id, }, + description: `Created ${permission.id}`, + resources: [ + { + type: "permission", + id: permission.id, + meta: { + name: permission.name, + description: permission.description, + }, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, }); + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: auth.authorizedWorkspaceId, event: "permission.create", actor: { diff --git a/apps/api/src/routes/v1_permissions_createRole.ts b/apps/api/src/routes/v1_permissions_createRole.ts index fcf668be09..e16834c72a 100644 --- a/apps/api/src/routes/v1_permissions_createRole.ts +++ b/apps/api/src/routes/v1_permissions_createRole.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { openApiErrorResponses } from "@/pkg/errors"; import { schema } from "@unkey/db"; @@ -83,18 +84,41 @@ export const registerV1PermissionsCreateRole = (app: App) => description: req.description, }; - await db.primary - .insert(schema.roles) - .values(role) - .onDuplicateKeyUpdate({ - set: { - name: req.name, - description: req.description, + await db.primary.transaction(async (tx) => { + await tx + .insert(schema.roles) + .values(role) + .onDuplicateKeyUpdate({ + set: { + name: req.name, + description: req.description, + }, + }); + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "role.create", + actor: { + type: "key", + id: auth.key.id, }, + description: `Created ${role.id}`, + resources: [ + { + type: "role", + id: role.id, + meta: { + name: role.name, + description: role.description, + }, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, }); + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: auth.authorizedWorkspaceId, event: "role.create", actor: { diff --git a/apps/api/src/routes/v1_permissions_deletePermission.ts b/apps/api/src/routes/v1_permissions_deletePermission.ts index b20823b35d..dda2fa5f05 100644 --- a/apps/api/src/routes/v1_permissions_deletePermission.ts +++ b/apps/api/src/routes/v1_permissions_deletePermission.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, schema } from "@unkey/db"; @@ -78,8 +79,25 @@ export const registerV1PermissionsDeletePermission = (app: App) => eq(schema.permissions.id, req.permissionId), ), ); + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "permission.delete", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Deleted ${permission.id}`, + resources: [ + { + type: "permission", + id: permission.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: auth.authorizedWorkspaceId, event: "permission.delete", actor: { diff --git a/apps/api/src/routes/v1_permissions_deleteRole.ts b/apps/api/src/routes/v1_permissions_deleteRole.ts index 27a9b24c08..ce0222ab49 100644 --- a/apps/api/src/routes/v1_permissions_deleteRole.ts +++ b/apps/api/src/routes/v1_permissions_deleteRole.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { and, eq, schema } from "@unkey/db"; @@ -69,17 +70,37 @@ export const registerV1PermissionsDeleteRole = (app: App) => }); } - await db.primary - .delete(schema.roles) - .where( - and( - eq(schema.roles.workspaceId, auth.authorizedWorkspaceId), - eq(schema.roles.id, req.roleId), - ), - ); + await db.primary.transaction(async (tx) => { + await tx + .delete(schema.roles) + .where( + and( + eq(schema.roles.workspaceId, auth.authorizedWorkspaceId), + eq(schema.roles.id, req.roleId), + ), + ); + + await insertUnkeyAuditLog(c, tx, { + workspaceId: auth.authorizedWorkspaceId, + event: "role.delete", + actor: { + type: "key", + id: auth.key.id, + }, + description: `Deleted ${role.id}`, + resources: [ + { + type: "role", + id: role.id, + }, + ], + + context: { location: c.get("location"), userAgent: c.get("userAgent") }, + }); + }); c.executionCtx.waitUntil( - analytics.ingestUnkeyAuditLogs({ + analytics.ingestUnkeyAuditLogsTinybird({ workspaceId: auth.authorizedWorkspaceId, event: "role.delete", actor: { diff --git a/apps/api/src/routes/v1_ratelimit_limit.ts b/apps/api/src/routes/v1_ratelimit_limit.ts index 27dbac433a..4aa10d11ed 100644 --- a/apps/api/src/routes/v1_ratelimit_limit.ts +++ b/apps/api/src/routes/v1_ratelimit_limit.ts @@ -1,6 +1,7 @@ import type { App } from "@/pkg/hono/app"; import { createRoute, z } from "@hono/zod-openapi"; +import { insertGenericAuditLogs, insertUnkeyAuditLog } from "@/pkg/audit"; import { rootKeyAuth } from "@/pkg/auth/root_key"; import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors"; import { match } from "@/pkg/util/wildcard"; @@ -46,9 +47,9 @@ const route = createRoute({ .default(1) .optional() .openapi({ - description: `Expensive requests may use up more tokens. You can specify a cost to the request here and we'll deduct this many tokens in the current window. + description: `Expensive requests may use up more tokens. You can specify a cost to the request here and we'll deduct this many tokens in the current window. If there are not enough tokens left, the request is denied. - + Set it to 0 to receive the current limit without changing anything.`, example: 2, default: 1, @@ -194,7 +195,26 @@ export const registerV1RatelimitLimit = (app: App) => }; try { await db.primary.insert(schema.ratelimitNamespaces).values(namespace); - await analytics.ingestUnkeyAuditLogs({ + await analytics.ingestUnkeyAuditLogsTinybird({ + workspaceId: rootKey.authorizedWorkspaceId, + actor: { + type: "key", + id: rootKey.key.id, + }, + event: "ratelimitNamespace.create", + description: `Created ${namespace.id}`, + resources: [ + { + type: "ratelimitNamespace", + id: namespace.id, + }, + ], + context: { + location: c.get("location"), + userAgent: c.get("userAgent"), + }, + }); + await insertUnkeyAuditLog(c, undefined, { workspaceId: rootKey.authorizedWorkspaceId, actor: { type: "key", @@ -363,7 +383,32 @@ export const registerV1RatelimitLimit = (app: App) => if (req.resources && req.resources.length > 0) { c.executionCtx.waitUntil( - analytics.ingestGenericAuditLogs({ + insertGenericAuditLogs(c, undefined, { + auditLogId: newId("auditLog"), + workspaceId: rootKey.authorizedWorkspaceId, + bucket: namespace.id, + actor: { + type: "key", + id: rootKey.key.id, + }, + description: "ratelimit", + event: ratelimitResponse.pass ? "ratelimit.success" : "ratelimit.denied", + meta: { + requestId: c.get("requestId"), + namespacId: namespace.id, + identifier: req.identifier, + success: ratelimitResponse.pass, + }, + time: Date.now(), + resources: req.resources ?? [], + context: { + location: c.req.header("True-Client-IP") ?? "", + userAgent: c.req.header("User-Agent") ?? "", + }, + }), + ); + c.executionCtx.waitUntil( + analytics.ingestGenericAuditLogsTinybird({ auditLogId: newId("auditLog"), workspaceId: rootKey.authorizedWorkspaceId, bucket: namespace.id, diff --git a/apps/dashboard/app/(app)/@breadcrumb/gateways/[...path]/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/gateways/[...path]/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/gateways/[...path]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/@breadcrumb/gateways/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/gateways/page.tsx deleted file mode 100644 index eaa3a82cbd..0000000000 --- a/apps/dashboard/app/(app)/@breadcrumb/gateways/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// No breadcrumb -export default function NoBreadcrumb() { - return null; -} diff --git a/apps/dashboard/app/(app)/gateways/new/form.tsx b/apps/dashboard/app/(app)/gateways/new/form.tsx deleted file mode 100644 index 50cd4f3f4e..0000000000 --- a/apps/dashboard/app/(app)/gateways/new/form.tsx +++ /dev/null @@ -1,293 +0,0 @@ -"use client"; -import { Loading } from "@/components/dashboard/loading"; -import { Accordion, AccordionContent } from "@/components/ui/accordion"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { toast } from "@/components/ui/toaster"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDown, Eye, EyeOff, X } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { z } from "zod"; - -const formSchema = z.object({ - subdomain: z.string().regex(/^[a-zA-Z0-9-]+$/), - origin: z.string().url(), - headerRewrites: z.array( - z.object({ - name: z.string().min(1), - value: z.string().min(1), - show: z.boolean().default(false).optional(), - }), - ), -}); -export const CreateGatewayForm: React.FC = () => { - const router = useRouter(); - const form = useForm>({ - resolver: zodResolver(formSchema), - mode: "all", - shouldFocusError: true, - }); - - const fields = useFieldArray({ - control: form.control, - name: "headerRewrites", - }); - const create = trpc.gateway.create.useMutation({ - onSuccess(_, variables) { - toast.success("Gateway Created", { - description: "Your Gateway has been created", - duration: 10_000, - action: { - label: "Go to", - onClick: () => { - router.push(`https://${variables.subdomain}.unkey.io`); - }, - }, - }); - }, - }); - - async function onSubmit(values: z.infer) { - create.mutate(values); - } - - // const snippet = `curl -XPOST '${process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev"}/v1/keys.verifyKey' \\ - // -H 'Content-Type: application/json' \\ - // -d '{ - // "key": "${key.data?.key}" - // }'`; - - const [newHeaderRewriteName, setNewHeadeRewriteName] = useState(""); - const [newHeaderRewriteValue, setNewHeadeRewriteValue] = useState(""); - - return ( -
- - - Configure Gateway - - - -
- - ( - - Domain - -
- - https:// - - - - .unkey.io - -
-
- - The domain where your gateway will be available - - -
- )} - /> - ( - - Origin - - - - The origin host - - - )} - /> - - - - - - - - - Header Rewrites - - - - -
-
- setNewHeadeRewriteName(v.currentTarget.value)} - /> - - Name - -
-
- setNewHeadeRewriteValue(v.currentTarget.value)} - /> - Value (encrypted at rest) - -
- - -
- - {fields.fields.length > 0 ? ( - - - - Name - Value - - - - - - {fields.fields.map((f, index) => ( - - - ( - - - - - - - )} - /> - - - ( - - - - - - - )} - /> - - - - - - - ))} - -
- ) : null} -
-
-
- -
- -
- - -
-
-
- ); -}; diff --git a/apps/dashboard/app/(app)/gateways/new/page.tsx b/apps/dashboard/app/(app)/gateways/new/page.tsx deleted file mode 100644 index f770fc85dd..0000000000 --- a/apps/dashboard/app/(app)/gateways/new/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; -import { Shuffle } from "lucide-react"; -import { CreateGatewayForm } from "./form"; - -export const dynamic = "force-dynamic"; -export default async function CreateGatewayPage() { - return ( -
- - -
-
-
- -
-

What are Gateways?

-

Gateways allow you to rewrite HTTP headers.

-
    -
  1. A user calls your gateway
  2. -
  3. The request gets validated
  4. -
  5. Unkey adds headers as configured and sends the request to the origin
  6. -
  7. Unkey returns the response from the origin back to your user
  8. -
-
- -
- -
-
-
- ); -} diff --git a/apps/dashboard/app/(app)/monitors/verifications/create-new-button.tsx b/apps/dashboard/app/(app)/monitors/verifications/create-new-button.tsx deleted file mode 100644 index 9ed8aeb53b..0000000000 --- a/apps/dashboard/app/(app)/monitors/verifications/create-new-button.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; -import { Loading } from "@/components/dashboard/loading"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { toast } from "@/components/ui/toaster"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import type { Workspace } from "@unkey/db"; -import { Plus } from "lucide-react"; -import ms from "ms"; -import { useRouter } from "next/navigation"; -import type React from "react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -type Props = { - workspace: { plan: Workspace["plan"] }; - keySpaces: Array<{ id: string; api: { name: string } }>; -}; - -export const CreateNewMonitorButton: React.FC = ({ workspace, keySpaces }) => { - const [isOpen, setOpen] = useState(false); - const formSchema = z.object({ - interval: z.enum( - workspace.plan === "free" - ? ["15m", "1h", "24h"] - : ["1m", "5m", "15m", "30m", "1h", "6h", "12h", "24h"], - ), - webhookUrl: z.string().url(), - keySpaceId: z.string(), - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - - const create = trpc.monitor.verification.create.useMutation({ - onSuccess(_res) { - toast.success("Your webhook has been created"); - setOpen(false); - router.refresh(); - router.push("/webhooks"); - }, - }); - async function onSubmit(values: z.infer) { - create.mutate({ - webhookUrl: values.webhookUrl, - keySpaceId: values.keySpaceId, - interval: ms(values.interval), - }); - } - const router = useRouter(); - - return ( - <> - - - - - -
- - ( - - Interval - - - - )} - /> - ( - - API - - - - )} - /> - ( - - Webhook Destination - - - - )} - /> - - - - - - -
-
- - ); -}; diff --git a/apps/dashboard/app/(app)/monitors/verifications/loading.tsx b/apps/dashboard/app/(app)/monitors/verifications/loading.tsx deleted file mode 100644 index eb0de19b6f..0000000000 --- a/apps/dashboard/app/(app)/monitors/verifications/loading.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton"; - -export default function Loading() { - return ( -
-
- - - -
-
- - -
-
- - - -
- - -
-
- - - -
- - -
-
- - - -
- - -
-
-
-
- ); -} diff --git a/apps/dashboard/app/(app)/monitors/verifications/page.tsx b/apps/dashboard/app/(app)/monitors/verifications/page.tsx deleted file mode 100644 index 7bda9f92ac..0000000000 --- a/apps/dashboard/app/(app)/monitors/verifications/page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; - -import { CopyButton } from "@/components/dashboard/copy-button"; -import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Code } from "@/components/ui/code"; -import { Separator } from "@/components/ui/separator"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { BookOpen, ChevronRight, Scan, Webhook } from "lucide-react"; -import ms from "ms"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { Suspense } from "react"; -import { CreateNewMonitorButton } from "./create-new-button"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function RatelimitOverviewPage() { - const tenantId = getTenantId(); - const workspace = await db.query.workspaces.findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), - with: { - apis: { - where: (table, { isNotNull }) => isNotNull(table.keyAuthId), - with: { - keyAuth: true, - }, - }, - verificationMonitors: true, - webhooks: { - columns: { - id: true, - destination: true, - }, - }, - }, - }); - - if (!workspace) { - return redirect("/new"); - } - - return ( -
- ({ - id: api.keyAuth!.id, - api: { - id: api.id, - name: api.name, - }, - }))} - />, - ]} - /> - - - {workspace.verificationMonitors.length > 0 ? ( -
-
-

Reporter

-
- - {Intl.NumberFormat().format(workspace.verificationMonitors.length)} /{" "} - {Intl.NumberFormat().format(Number.POSITIVE_INFINITY)} used{" "} - - {/* Create New Role} /> */} -
-
- -
    - {workspace.verificationMonitors.map((ur) => ( - -
    - - {ur.nextExecution === 0 ? "Pending" : new Date(ur.nextExecution).toISOString()} - -
    - -
    - every {ms(ur.interval)} -
    - -
    - -
    - - ))} -
-
- ) : ( - - - - - No Usage reporters found - - You haven't created any reporter yet. - - -
- - - -
-
- )} -
- ); -} diff --git a/apps/dashboard/app/(app)/secrets/new/form.tsx b/apps/dashboard/app/(app)/secrets/new/form.tsx deleted file mode 100644 index 17fd321ea0..0000000000 --- a/apps/dashboard/app/(app)/secrets/new/form.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; -import { Loading } from "@/components/dashboard/loading"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; -import { toast } from "@/components/ui/toaster"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -export const CreateSecretForm: React.FC = () => { - const formSchema = z.object({ - name: z.string(), - value: z.string(), - comment: z.string().optional(), - }); - - const router = useRouter(); - const form = useForm>({ - resolver: zodResolver(formSchema), - mode: "all", - shouldFocusError: true, - }); - - const create = trpc.secrets.create.useMutation({ - onSuccess() { - toast.success("Secret created"); - router.refresh(); - }, - }); - - async function onSubmit(values: z.infer) { - create.mutate(values); - } - - return ( -
- - - -
- ( - - Name - - - - - - )} - /> - ( - - Value - - - - - - )} - /> -
- - ( - - Comment - - - - - - - )} - /> - - - -
- -
- - - ); -}; diff --git a/apps/dashboard/app/(app)/secrets/new/page.tsx b/apps/dashboard/app/(app)/secrets/new/page.tsx deleted file mode 100644 index 3f99c9fb8f..0000000000 --- a/apps/dashboard/app/(app)/secrets/new/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { notFound } from "next/navigation"; -import { CreateSecretForm } from "./form"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function SecretsPage() { - const tenantId = getTenantId(); - - const workspace = await db.query.workspaces.findFirst({ - where: (table, { eq, isNull, and }) => - and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), - }); - if (!workspace) { - return notFound(); - } - - return ( - <> - - - - ); -} diff --git a/apps/dashboard/app/(app)/secrets/page.tsx b/apps/dashboard/app/(app)/secrets/page.tsx deleted file mode 100644 index 78c3f7f0ef..0000000000 --- a/apps/dashboard/app/(app)/secrets/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; -import { Button } from "@/components/ui/button"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { Secrets } from "./secrets"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function SecretsPage() { - const tenantId = getTenantId(); - - const workspace = await db.query.workspaces.findFirst({ - where: (table, { eq, isNull, and }) => - and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), - with: { - secrets: true, - }, - }); - if (!workspace) { - return notFound(); - } - - return ( - <> - - - , - ]} - /> - - - ); -} diff --git a/apps/dashboard/app/(app)/secrets/secrets.tsx b/apps/dashboard/app/(app)/secrets/secrets.tsx deleted file mode 100644 index a07af8b876..0000000000 --- a/apps/dashboard/app/(app)/secrets/secrets.tsx +++ /dev/null @@ -1,266 +0,0 @@ -"use client"; -import { CopyButton } from "@/components/dashboard/copy-button"; -import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; -import { Loading } from "@/components/dashboard/loading"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/toaster"; -import type { Secret } from "@/lib/db"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Eye, EyeOff, Loader2, MoreHorizontal, Settings, VenetianMask } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -type Props = { - secrets: Secret[]; -}; - -export const Secrets: React.FC = ({ secrets }) => { - if (secrets.length === 0) { - return ( - - - - - No secrets found - - ); - } - - return ( -
    - {secrets.map((s) => ( - - ))} -
- ); -}; - -const Row: React.FC<{ secret: Secret }> = ({ secret }) => { - const [isEditMode, setIsEditMode] = useState(false); - - const formSchema = z.object({ - name: z.string(), - value: z.string(), - comment: z.string().optional(), - }); - - const router = useRouter(); - const form = useForm>({ - resolver: zodResolver(formSchema), - mode: "all", - shouldFocusError: true, - defaultValues: { - name: secret.name, - comment: secret.comment ?? "", - }, - }); - - const decrypt = trpc.secrets.decrypt.useMutation({ - onSuccess: ({ value }) => { - form.setValue("value", value); - }, - }); - useEffect(() => { - if (isEditMode && !decrypt.data) { - decrypt.mutate({ secretId: secret.id }); - } - }, [isEditMode]); - - const update = trpc.secrets.update.useMutation({ - onSuccess() { - toast.success("Secret updated"); - router.refresh(); - }, - }); - - async function onSubmit(values: z.infer) { - update.mutate({ - secretId: secret.id, - name: values.name, - value: values.value, - comment: values.comment === "" ? null : values.comment, - }); - } - - return ( -
  • -
    -
    - {secret.name} -
    {secret.id}
    -
    - -
    - -
    - -
    - - - - - - { - setIsEditMode(!isEditMode); - }} - > - - Edit - - - -
    -
    - - {isEditMode ? ( -
    - - - -
    - ( - - Name - - - - - - )} - /> - ( - - Value - - {decrypt.isLoading ? : } - - - - )} - /> -
    - - ( - - Comment - - - - - - - )} - /> - - - -
    - - -
    - - - ) : null} -
  • - ); -}; - -const Value: React.FC<{ secretId: string }> = ({ secretId }) => { - const decrypt = trpc.secrets.decrypt.useMutation({ - onError(err) { - console.error(err); - toast.error(err.message); - }, - }); - - if (decrypt.isSuccess && decrypt.data) { - return ( -
    - - - {decrypt.data.value} - - -
    - ); - } - - if (decrypt.isLoading) { - return ( - - ); - } - - return ( -
    - -
    (encrypted)
    -
    - ); -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/delete-webhook.tsx b/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/delete-webhook.tsx deleted file mode 100644 index 9824996304..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/delete-webhook.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; -import { Button } from "@/components/ui/button"; -import type React from "react"; -import { useState } from "react"; - -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/toaster"; - -import { Loading } from "@/components/dashboard/loading"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { Err } from "@unkey/error"; -import { revalidate } from "../../../apis/[apiId]/settings/actions"; - -type Props = { - webhook: { - destination: string; - id: string; - }; -}; - -const intent = "delete my webhook"; - -export const DeleteWebhook: React.FC = ({ webhook }) => { - const [open, setOpen] = useState(false); - - const formSchema = z.object({ - intent: z.string().refine((v) => v === intent, "Please confirm your intent"), - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - const router = useRouter(); - - const deleteWebhook = trpc.webhook.delete.useMutation({ - async onSuccess() { - await revalidate(); - - router.push("/settings/webhooks"); - }, - }); - - const isValid = form.watch("intent") === intent; - - async function onSubmit(_values: z.infer) { - toast.promise(deleteWebhook.mutateAsync({ webhookId: webhook.id }), { - loading: "Deleting your webhook...", - success: "Your webhook has been deleted", - error: (err) => (err instanceof Error && err.message) || "Something went wrong", - }); - } - - return ( - setOpen(o)}> - - - - - - Delete webhook - - This webhook will be deleted, along with all of its events. This action cannot be - undone. - - -
    - - - Warning - This action is not reversible. Please be certain. - - - ( - - - To verify, type {intent}{" "} - below: - - - - - - - - )} - /> - - - - - - - -
    -
    - ); -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/duration.tsx b/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/duration.tsx deleted file mode 100644 index a02f1b2d5c..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/duration.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import ms from "ms"; -import { useEffect, useState } from "react"; -type Props = { - time: number; -}; - -export const Until: React.FC = ({ time }) => { - const [duration, setDuration] = useState(time - Date.now()); - - useEffect(() => { - const interval = setInterval(() => { - setDuration(time - Date.now()); - }, 1000); - - return () => clearInterval(interval); - }, [time]); - - if (duration < 0) { - return now; - } - - return {ms(duration)}; -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/page.tsx b/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/page.tsx deleted file mode 100644 index 8976d92980..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/page.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { BackLink } from "@/components/back"; -import { CopyButton } from "@/components/dashboard/copy-button"; -import { PageHeader } from "@/components/dashboard/page-header"; -import { Badge } from "@/components/ui/badge"; -import { Code } from "@/components/ui/code"; -import { Separator } from "@/components/ui/separator"; -import { type Event, db } from "@/lib/db"; -import { cn } from "@/lib/utils"; -import { Check, CheckCircle } from "lucide-react"; -import ms from "ms"; -import { Solitreo } from "next/font/google"; -import Link from "next/link"; -import { notFound, redirect } from "next/navigation"; -import { DeleteWebhook } from "./delete-webhook"; -import { Until } from "./duration"; -import { ToggleWebhookButton } from "./toggle-webhook-button"; -type Props = { - params: { - webhookId: string; - }; - searchParams: { - eventId?: string; - }; -}; - -export default async function Page(props: Props) { - const webhook = await db.query.webhooks.findFirst({ - where: (table, { eq }) => eq(table.id, props.params.webhookId), - columns: { - id: true, - enabled: true, - destination: true, - }, - with: { - events: { - limit: 50, - orderBy: (table, { desc }) => desc(table.time), - with: { - deliveryAttempts: { - orderBy: (table, { desc }) => desc(table.time), - }, - }, - }, - }, - }); - if (!webhook) { - return notFound(); - } - - const selectedEvent = - webhook.events.find((event) => event.id === props.searchParams.eventId) ?? webhook.events.at(0); - return ( -
    - - - {selectedEvent.id} - - - ) : null, - , - , - ]} - /> - -
    -
      - {webhook.events.map((event) => ( - -
      - {event.event} - -
      - - - - ))} -
    - -
    - {selectedEvent ? ( -
    - -
    -
      - {selectedEvent.deliveryAttempts.map((attempt, i) => ( -
    1. -
      -
      -
      -
      -
      -
      - -
      - -
      - {attempt.internalError ? ( - <> -
      Error
      -
      - {attempt.internalError} -
      - - ) : null} - {attempt.responseStatus ? ( - <> -
      Status
      -
      - {attempt.responseStatus} -
      - - ) : null} - {attempt.nextAttemptAt && attempt.nextAttemptAt > Date.now() ? ( - <> -
      Retrying in
      -
      - -
      - - ) : null} -
      - {attempt.responseBody ? ( - {JSON.stringify(attempt.responseBody)} - ) : null} -
      -
    2. - ))} -
    -
    - - -
    -

    Request

    - - {JSON.stringify(selectedEvent.payload, null, 2)} -
    -
    - ) : null} -
    -
    -
    - ); -} - -const StateIndicator: React.FC<{ state: Event["state"] }> = ({ state }) => { - return ( - - - {state} - - ); -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/toggle-webhook-button.tsx b/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/toggle-webhook-button.tsx deleted file mode 100644 index 7ca7945ee5..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/[webhookId]/toggle-webhook-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { toast } from "@/components/ui/toaster"; -import { trpc } from "@/lib/trpc/client"; -import { useRouter } from "next/navigation"; -import type React from "react"; -import { useState } from "react"; - -type Props = { - webhook: { - id: string; - enabled: boolean; - }; -}; - -export const ToggleWebhookButton: React.FC = ({ webhook }) => { - const router = useRouter(); - const [optimisticEnabled, setOptimisticEnabled] = useState(webhook.enabled); - - const toggle = trpc.webhook.toggle.useMutation({ - onMutate(variables) { - setOptimisticEnabled(variables.enabled); - }, - onSuccess() { - router.refresh(); - }, - }); - - async function action(enabled: boolean) { - toast.promise(toggle.mutateAsync({ webhookId: webhook.id, enabled }), { - loading: `${enabled ? "Enabling" : "Disabling"} your webhook...`, - success: (res) => `Your webhook has been ${res.enabled ? "enabled" : "disabled"}`, - }); - } - return ( -
    - - -
    - ); -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/create-webhook-button.tsx b/apps/dashboard/app/(app)/settings/webhooks/create-webhook-button.tsx deleted file mode 100644 index 4e2b68e249..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/create-webhook-button.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; -import { Loading } from "@/components/dashboard/loading"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogTrigger } from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Select } from "@/components/ui/select"; -import { toast } from "@/components/ui/toaster"; -import { trpc } from "@/lib/trpc/client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Plus } from "lucide-react"; -import { useRouter } from "next/navigation"; -import type React from "react"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -const formSchema = z.object({ - destination: z.string().url(), -}); - -export const CreateWebhookButton = ({ ...rest }: React.ButtonHTMLAttributes) => { - const [isOpen, setOpen] = useState(false); - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - - const create = trpc.webhook.create.useMutation({ - onSuccess(_res) { - toast.success("Your webhook has been created"); - setOpen(false); - router.refresh(); - router.push("/webhooks"); - }, - }); - async function onSubmit(values: z.infer) { - create.mutate(values); - } - const router = useRouter(); - - return ( - <> - - - - - -
    - - ( - - Name - - - - The url where the webhook will send the data. - - - )} - /> - - - - - - -
    -
    - - ); -}; diff --git a/apps/dashboard/app/(app)/settings/webhooks/loading.tsx b/apps/dashboard/app/(app)/settings/webhooks/loading.tsx deleted file mode 100644 index eb0de19b6f..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/loading.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton"; - -export default function Loading() { - return ( -
    -
    - - - -
    -
    - - -
    -
    - - - -
    - - -
    -
    - - - -
    - - -
    -
    - - - -
    - - -
    -
    -
    -
    - ); -} diff --git a/apps/dashboard/app/(app)/settings/webhooks/page.tsx b/apps/dashboard/app/(app)/settings/webhooks/page.tsx deleted file mode 100644 index d7d93a869d..0000000000 --- a/apps/dashboard/app/(app)/settings/webhooks/page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { PageHeader } from "@/components/dashboard/page-header"; - -import { CopyButton } from "@/components/dashboard/copy-button"; -import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Code } from "@/components/ui/code"; -import { Separator } from "@/components/ui/separator"; -import { getTenantId } from "@/lib/auth"; -import { db } from "@/lib/db"; -import { BookOpen, ChevronRight, Scan, Webhook } from "lucide-react"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { Suspense } from "react"; -import { CreateWebhookButton } from "./create-webhook-button"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -export default async function RatelimitOverviewPage() { - const tenantId = getTenantId(); - const workspace = await db.query.workspaces.findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), - with: { - webhooks: { - with: { - events: { - where: (table, { gt }) => gt(table.time, Date.now() - 7 * 24 * 60 * 60 * 1000), - columns: {}, - with: { - deliveryAttempts: { - columns: { success: true }, - }, - }, - }, - }, - }, - }, - }); - - if (!workspace) { - return redirect("/new"); - } - - return ( -
    - ]} /> - {workspace.webhooks.length > 0 ? ( -
    - {workspace.webhooks.length === 0 ? ( - - - - - No webhooks found - Create your first webhook - {/* Create New Role} /> */} - - ) : ( -
      - {workspace.webhooks.map((wh) => { - const errors = wh.events - .flatMap((e) => e.deliveryAttempts) - .reduce((acc, attempt) => { - if (!attempt.success) { - return acc + 1; - } - return acc; - }, 0); - - return ( - -
      - {wh.destination} -
      - -
      - 0 ? "alert" : "secondary"}>{errors} Errors -
      -
      - - {wh.enabled ? "Enabled" : "Disabled"} - -
      - -
      - -
      - - ); - })} -
    - )} -
    - ) : ( - - - - - No Webhooks found - - You haven't created any webhooks yet. - - -
    - - - -
    -
    - )} -
    - ); -} diff --git a/apps/dashboard/app/new/page.tsx b/apps/dashboard/app/new/page.tsx index c418307b3a..68aaaf0ea4 100644 --- a/apps/dashboard/app/new/page.tsx +++ b/apps/dashboard/app/new/page.tsx @@ -1,8 +1,9 @@ import { PageHeader } from "@/components/dashboard/page-header"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { auth } from "@clerk/nextjs"; import { newId } from "@unkey/id"; import { ArrowRight, DatabaseZap, GlobeLock, KeySquare } from "lucide-react"; @@ -216,19 +217,41 @@ export default async function (props: Props) { // if no personal workspace exists, we create one if (!personalWorkspace) { const workspaceId = newId("workspace"); - await db.insert(schema.workspaces).values({ - id: workspaceId, - tenantId: userId, - name: "Personal", - plan: "free", - stripeCustomerId: null, - stripeSubscriptionId: null, - features: {}, - betaFeatures: {}, - subscriptions: null, - createdAt: new Date(), + await db.transaction(async (tx) => { + await tx.insert(schema.workspaces).values({ + id: workspaceId, + tenantId: userId, + name: "Personal", + plan: "free", + stripeCustomerId: null, + stripeSubscriptionId: null, + features: {}, + betaFeatures: {}, + subscriptions: null, + createdAt: new Date(), + }); + await insertAuditLogs(tx, { + workspaceId: workspaceId, + event: "workspace.create", + actor: { + type: "user", + id: userId, + }, + description: `Created ${workspaceId}`, + resources: [ + { + type: "workspace", + id: workspaceId, + }, + ], + + context: { + userAgent: headers().get("user-agent") ?? undefined, + location: headers().get("x-forwarded-for") ?? process.env.VERCEL_REGION ?? "unknown", + }, + }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspaceId, event: "workspace.create", actor: { diff --git a/apps/dashboard/lib/audit/index.ts b/apps/dashboard/lib/audit/index.ts new file mode 100644 index 0000000000..45288db0ad --- /dev/null +++ b/apps/dashboard/lib/audit/index.ts @@ -0,0 +1,82 @@ +import { type Database, type Transaction, schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import type { UnkeyAuditLog } from "../tinybird"; +import type { MaybeArray } from "../types"; + +const BUCKET_NAME = "unkey_mutations"; + +type Key = `${string}::${string}`; +type BucketId = string; +const bucketCache = new Map(); + +export async function insertAuditLogs( + db: Transaction | Database, + logOrLogs: MaybeArray, +) { + const logs = Array.isArray(logOrLogs) ? logOrLogs : [logOrLogs]; + + if (logs.length === 0) { + return Promise.resolve(); + } + + for (const log of logs) { + // 1. Get the bucketId or create one if necessary + const key: Key = `${log.workspaceId}::${BUCKET_NAME}`; + let bucketId = ""; + const cachedBucketId = bucketCache.get(key); + if (cachedBucketId) { + bucketId = cachedBucketId; + } else { + const bucket = await db.query.auditLogBucket.findFirst({ + where: (table, { eq, and }) => + and(eq(table.workspaceId, log.workspaceId), eq(table.name, BUCKET_NAME)), + columns: { + id: true, + }, + }); + if (bucket) { + bucketId = bucket.id; + } else { + bucketId = newId("auditLogBucket"); + await db.insert(schema.auditLogBucket).values({ + id: bucketId, + workspaceId: log.workspaceId, + name: BUCKET_NAME, + }); + } + } + bucketCache.set(key, bucketId); + + // 2. Insert the log + + const auditLogId = newId("auditLog"); + await db.insert(schema.auditLog).values({ + id: auditLogId, + workspaceId: log.workspaceId, + bucketId, + event: log.event, + time: Date.now(), + display: log.description, + remoteIp: log.context.location, + userAgent: log.context.userAgent, + actorType: log.actor.type, + actorId: log.actor.id, + actorName: log.actor.name, + actorMeta: log.actor.meta, + }); + if (log.resources.length > 0) { + await db.insert(schema.auditLogTarget).values( + log.resources.map((r) => ({ + workspaceId: log.workspaceId, + auditLogId, + bucketId, + displayName: "", + type: r.type, + id: r.id, + name: "", + meta: r.meta, + })), + ); + } + } +} diff --git a/apps/dashboard/lib/tinybird.ts b/apps/dashboard/lib/tinybird.ts index 2f75f35551..ffbfef3c68 100644 --- a/apps/dashboard/lib/tinybird.ts +++ b/apps/dashboard/lib/tinybird.ts @@ -473,6 +473,7 @@ export type UnkeyAuditLog = { type: "user" | "key"; name?: string; id: string; + meta?: Record; }; resources: Array<{ type: @@ -491,7 +492,8 @@ export type UnkeyAuditLog = { | "webhook" | "reporter" | "secret" - | "identity"; + | "identity" + | "auditLogBucket"; id: string; meta?: Record; @@ -502,7 +504,7 @@ export type UnkeyAuditLog = { }; }; -export function ingestAuditLogs(logs: MaybeArray) { +export function ingestAuditLogsTinybird(logs: MaybeArray) { if (Array.isArray(logs) && logs.length === 0) { return Promise.resolve(); } diff --git a/apps/dashboard/lib/trpc/routers/api/create.ts b/apps/dashboard/lib/trpc/routers/api/create.ts index 5f18aaf05a..043fde9a65 100644 --- a/apps/dashboard/lib/trpc/routers/api/create.ts +++ b/apps/dashboard/lib/trpc/routers/api/create.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { newId } from "@unkey/id"; @@ -50,43 +51,64 @@ export const createApi = rateLimitedProcedure(ratelimit.create) const apiId = newId("api"); - await db - .insert(schema.apis) - .values({ - id: apiId, - name: input.name, - workspaceId: ws.id, - keyAuthId, - authType: "key", - ipWhitelist: null, - createdAt: new Date(), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create the API. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .insert(schema.apis) + .values({ + id: apiId, + name: input.name, + workspaceId: ws.id, + keyAuthId, + authType: "key", + ipWhitelist: null, + createdAt: new Date(), + }) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to create the API. Please contact support using support@unkey.dev", + }); }); - }); - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "api.create", - description: `Created ${apiId}`, - resources: [ - { - type: "api", - id: apiId, + await insertAuditLogs(tx, { + workspaceId: ws.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "api.create", + description: `Created ${apiId}`, + resources: [ + { + type: "api", + id: apiId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ + workspaceId: ws.id, + actor: { + type: "user", + id: ctx.user.id, }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, + event: "api.create", + description: `Created ${apiId}`, + resources: [ + { + type: "api", + id: apiId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }); return { diff --git a/apps/dashboard/lib/trpc/routers/api/delete.ts b/apps/dashboard/lib/trpc/routers/api/delete.ts index 6d39f97355..965f0cd3d0 100644 --- a/apps/dashboard/lib/trpc/routers/api/delete.ts +++ b/apps/dashboard/lib/trpc/routers/api/delete.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const deleteApi = rateLimitedProcedure(ratelimit.delete) @@ -46,8 +47,26 @@ export const deleteApi = rateLimitedProcedure(ratelimit.delete) .update(schema.apis) .set({ deletedAt: new Date() }) .where(eq(schema.apis.id, input.apiId)); - - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: api.workspaceId, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "api.delete", + description: `Deleted ${api.id}`, + resources: [ + { + type: "api", + id: api.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: api.workspaceId, actor: { type: "user", @@ -65,9 +84,6 @@ export const deleteApi = rateLimitedProcedure(ratelimit.delete) location: ctx.audit.location, userAgent: ctx.audit.userAgent, }, - }).catch((err) => { - tx.rollback(); - throw err; }); const keyIds = await tx.query.keys.findMany({ @@ -80,7 +96,33 @@ export const deleteApi = rateLimitedProcedure(ratelimit.delete) .update(schema.keys) .set({ deletedAt: new Date() }) .where(eq(schema.keys.keyAuthId, api.keyAuthId!)); - await ingestAuditLogs( + await insertAuditLogs( + tx, + keyIds.map(({ id }) => ({ + workspaceId: api.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.delete", + description: `Deleted ${id} as part of the ${api.id} deletion`, + resources: [ + { + type: "api", + id: api.id, + }, + { + type: "key", + id: id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + })), + ); + await ingestAuditLogsTinybird( keyIds.map(({ id }) => ({ workspaceId: api.workspace.id, actor: { diff --git a/apps/dashboard/lib/trpc/routers/api/updateDeleteProtection.ts b/apps/dashboard/lib/trpc/routers/api/updateDeleteProtection.ts index d9ba23a642..6780e7194a 100644 --- a/apps/dashboard/lib/trpc/routers/api/updateDeleteProtection.ts +++ b/apps/dashboard/lib/trpc/routers/api/updateDeleteProtection.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const updateAPIDeleteProtection = rateLimitedProcedure(ratelimit.update) @@ -36,20 +37,46 @@ export const updateAPIDeleteProtection = rateLimitedProcedure(ratelimit.update) }); } - await db - .update(schema.apis) - .set({ - deleteProtection: input.enabled, - }) - .where(eq(schema.apis.id, input.apiId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update the API. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .update(schema.apis) + .set({ + deleteProtection: input.enabled, + }) + .where(eq(schema.apis.id, input.apiId)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update the API. Please contact support using support@unkey.dev.", + }); }); + await insertAuditLogs(tx, { + workspaceId: api.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "api.update", + description: `API ${api.name} delete protection is now ${ + input.enabled ? "enabled" : "disabled" + }.}`, + resources: [ + { + type: "api", + id: api.id, + meta: { + deleteProtection: input.enabled, + }, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: api.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/api/updateIpWhitelist.ts b/apps/dashboard/lib/trpc/routers/api/updateIpWhitelist.ts index 028cb536c8..27b26d86f3 100644 --- a/apps/dashboard/lib/trpc/routers/api/updateIpWhitelist.ts +++ b/apps/dashboard/lib/trpc/routers/api/updateIpWhitelist.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const updateApiIpWhitelist = rateLimitedProcedure(ratelimit.update) @@ -53,21 +54,41 @@ export const updateApiIpWhitelist = rateLimitedProcedure(ratelimit.update) const newIpWhitelist = input.ipWhitelist === null ? null : input.ipWhitelist.join(","); - await db - .update(schema.apis) - .set({ - ipWhitelist: newIpWhitelist, - }) - .where(eq(schema.apis.id, input.apiId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update the API whitelist. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.apis) + .set({ + ipWhitelist: newIpWhitelist, + }) + .where(eq(schema.apis.id, input.apiId)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update the API whitelist. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: api.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "api.update", + description: `Changed ${api.id} IP whitelist from ${api.ipWhitelist} to ${newIpWhitelist}`, + resources: [ + { + type: "api", + id: api.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: api.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/api/updateName.ts b/apps/dashboard/lib/trpc/routers/api/updateName.ts index 35ccb48c4b..f493de2330 100644 --- a/apps/dashboard/lib/trpc/routers/api/updateName.ts +++ b/apps/dashboard/lib/trpc/routers/api/updateName.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const updateApiName = rateLimitedProcedure(ratelimit.update) @@ -36,21 +37,41 @@ export const updateApiName = rateLimitedProcedure(ratelimit.update) "We are unable to find the correct API. Please contact support using support@unkey.dev.", }); } - - await db - .update(schema.apis) - .set({ - name: input.name, - }) - .where(eq(schema.apis.id, input.apiId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update the API name. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .update(schema.apis) + .set({ + name: input.name, + }) + .where(eq(schema.apis.id, input.apiId)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update the API name. Please contact support using support@unkey.dev.", + }); }); + await insertAuditLogs(tx, { + workspaceId: api.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "api.update", + description: `Changed ${api.id} name from ${api.name} to ${input.name}`, + resources: [ + { + type: "api", + id: api.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: api.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/gateway/create.ts b/apps/dashboard/lib/trpc/routers/gateway/create.ts deleted file mode 100644 index 1d8e3659c9..0000000000 --- a/apps/dashboard/lib/trpc/routers/gateway/create.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { db } from "@/lib/db"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { newId } from "@unkey/id"; -import { z } from "zod"; - -export const createGateway = rateLimitedProcedure(ratelimit.create) - .input( - z.object({ - subdomain: z - .string() - .min(1, "Workspace names must contain at least 3 characters") - .max(50, "Workspace names must contain at most 50 characters"), - origin: z - .string() - .url() - .transform((url) => url.replace("https://", "").replace("http://", "")), - - headerRewrites: z.array( - z.object({ - name: z.string(), - value: z.string(), - }), - ), - }), - ) - .mutation(async ({ ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to create to create a gateway. Please contact support using support@unkey.dev.", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - - // // const encryptionKey = getEncryptionKeyFromEnv(env()); - // // if (encryptionKey.err) { - // // throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "encryption key missing" }); - // // } - - // // const aes = await AesGCM.withBase64Key(encryptionKey.val.key); - - const gatewayId = newId("gateway"); - // await db.insert(schema.gateways).values({ - // id: gatewayId, - // name: input.subdomain, - // workspaceId: ws.id, - // origin: input.origin, - // }); - - // const rewrites = await Promise.all( - // input.headerRewrites.map(async ({ name, value }) => { - // // const secret = //await aes.encrypt(value); - - // return { - // secretId: newId("secret"), - // secret, - // name, - // }; - // }), - // ); - - // if (rewrites.length > 0) { - // await db.insert(schema.secrets).values( - // rewrites.map(({ name, secretId, secret }) => ({ - // id: secretId, - // algorithm: "AES-GCM" as any, - // ciphertext: secret.ciphertext, - // iv: secret.iv, - // name: `${input.subdomain}_${name}`, - // workspaceId: ws.id, - // keyVersion: encryptionKey.val.version, - // createdAt: new Date(), - // })), - // ); - // await db.insert(schema.gatewayHeaderRewrites).values( - // rewrites.map(({ name, secretId }) => ({ - // id: newId("headerRewrite"), - // name, - // secretId, - // createdAt: new Date(), - // workspaceId: ws.id, - // gatewayId: gatewayId, - // })), - // ); - // } - - // await ingestAuditLogs({ - // workspaceId: ws.id, - // actor: { - // type: "user", - // id: ctx.user.id, - // }, - // event: "gateway.create", - // description: `Created ${gatewayId}`, - // resources: [ - // { - // type: "gateway", - // id: gatewayId, - // }, - // ], - // context: { - // location: ctx.audit.location, - // userAgent: ctx.audit.userAgent, - // }, - // }); - - return { - id: gatewayId, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index f5873f80a3..7691902d8c 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -4,7 +4,6 @@ import { deleteApi } from "./api/delete"; import { updateAPIDeleteProtection } from "./api/updateDeleteProtection"; import { updateApiIpWhitelist } from "./api/updateIpWhitelist"; import { updateApiName } from "./api/updateName"; -import { createGateway } from "./gateway/create"; import { createKey } from "./key/create"; import { createRootKey } from "./key/createRootKey"; import { deleteKeys } from "./key/delete"; @@ -19,7 +18,6 @@ import { updateKeyRemaining } from "./key/updateRemaining"; import { updateRootKeyName } from "./key/updateRootKeyName"; import { createLlmGateway } from "./llmGateway/create"; import { deleteLlmGateway } from "./llmGateway/delete"; -import { createVerificationMonitor } from "./monitor/verification/create"; import { createPlainIssue } from "./plain"; import { createNamespace } from "./ratelimit/createNamespace"; import { createOverride } from "./ratelimit/createOverride"; @@ -39,29 +37,13 @@ import { disconnectRoleFromKey } from "./rbac/disconnectRoleFromKey"; import { removePermissionFromRootKey } from "./rbac/removePermissionFromRootKey"; import { updatePermission } from "./rbac/updatePermission"; import { updateRole } from "./rbac/updateRole"; -import { createSecret } from "./secrets/create"; -import { decryptSecret } from "./secrets/decrypt"; -import { updateSecret } from "./secrets/update"; import { vercelRouter } from "./vercel"; -import { createWebhook } from "./webhook/create"; -import { deleteWebhook } from "./webhook/delete"; -import { toggleWebhook } from "./webhook/toggle"; import { changeWorkspaceName } from "./workspace/changeName"; import { changeWorkspacePlan } from "./workspace/changePlan"; import { createWorkspace } from "./workspace/create"; import { optWorkspaceIntoBeta } from "./workspace/optIntoBeta"; export const router = t.router({ - webhook: t.router({ - create: createWebhook, - toggle: toggleWebhook, - delete: deleteWebhook, - }), - monitor: t.router({ - verification: t.router({ - create: createVerificationMonitor, - }), - }), key: t.router({ create: createKey, delete: deleteKeys, @@ -75,18 +57,11 @@ export const router = t.router({ remaining: updateKeyRemaining, }), }), - gateway: t.router({ - create: createGateway, - }), + llmGateway: t.router({ create: createLlmGateway, delete: deleteLlmGateway, }), - secrets: t.router({ - create: createSecret, - decrypt: decryptSecret, - update: updateSecret, - }), rootKey: t.router({ create: createRootKey, delete: deleteRootKeys, diff --git a/apps/dashboard/lib/trpc/routers/key/create.ts b/apps/dashboard/lib/trpc/routers/key/create.ts index 864fffb8e0..445188ef6c 100644 --- a/apps/dashboard/lib/trpc/routers/key/create.ts +++ b/apps/dashboard/lib/trpc/routers/key/create.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; @@ -82,31 +83,48 @@ export const createKey = rateLimitedProcedure(ratelimit.create) prefix: input.prefix, byteLength: input.bytes, }); - await db - .insert(schema.keys) - .values({ - id: keyId, - keyAuthId: keyAuth.id, - name: input.name, - hash, - start, - ownerId: input.ownerId, - meta: JSON.stringify(input.meta ?? {}), - workspaceId: workspace.id, - forWorkspaceId: null, - expires: input.expires ? new Date(input.expires) : null, - createdAt: new Date(), - ratelimitAsync: input.ratelimit?.async, - ratelimitLimit: input.ratelimit?.limit, - ratelimitDuration: input.ratelimit?.duration, - remaining: input.remaining, - refillInterval: input.refill?.interval ?? null, - refillAmount: input.refill?.amount ?? null, - lastRefillAt: input.refill?.interval ? new Date() : null, - deletedAt: null, - enabled: input.enabled, - environment: input.environment, + .transaction(async (tx) => { + await tx.insert(schema.keys).values({ + id: keyId, + keyAuthId: keyAuth.id, + name: input.name, + hash, + start, + ownerId: input.ownerId, + meta: JSON.stringify(input.meta ?? {}), + workspaceId: workspace.id, + forWorkspaceId: null, + expires: input.expires ? new Date(input.expires) : null, + createdAt: new Date(), + ratelimitAsync: input.ratelimit?.async, + ratelimitLimit: input.ratelimit?.limit, + ratelimitDuration: input.ratelimit?.duration, + remaining: input.remaining, + refillInterval: input.refill?.interval ?? null, + refillAmount: input.refill?.amount ?? null, + lastRefillAt: input.refill?.interval ? new Date() : null, + deletedAt: null, + enabled: input.enabled, + environment: input.environment, + }); + + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "key.create", + description: `Created ${keyId}`, + resources: [ + { + type: "key", + id: keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) .catch((_err) => { throw new TRPCError({ @@ -116,7 +134,7 @@ export const createKey = rateLimitedProcedure(ratelimit.create) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "key.create", diff --git a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts index 73189ddb23..bc845eb2eb 100644 --- a/apps/dashboard/lib/trpc/routers/key/createRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/key/createRootKey.ts @@ -1,6 +1,6 @@ import { db, eq, schema } from "@/lib/db"; import { env } from "@/lib/env"; -import { type UnkeyAuditLog, ingestAuditLogs } from "@/lib/tinybird"; +import { type UnkeyAuditLog, ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; @@ -8,6 +8,7 @@ import { newKey } from "@unkey/keys"; import { unkeyPermissionValidation } from "@unkey/rbac"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { upsertPermissions } from "../rbac"; export const createRootKey = rateLimitedProcedure(ratelimit.create) @@ -186,6 +187,7 @@ export const createRootKey = rateLimitedProcedure(ratelimit.create) workspaceId: env().UNKEY_WORKSPACE_ID, })), ); + await insertAuditLogs(tx, auditLogs); }); } catch (_err) { throw new TRPCError({ @@ -195,7 +197,7 @@ export const createRootKey = rateLimitedProcedure(ratelimit.create) }); } - await ingestAuditLogs(auditLogs); + await ingestAuditLogsTinybird(auditLogs); return { key, keyId }; }); diff --git a/apps/dashboard/lib/trpc/routers/key/delete.ts b/apps/dashboard/lib/trpc/routers/key/delete.ts index edaca43a78..0d7dbb56c6 100644 --- a/apps/dashboard/lib/trpc/routers/key/delete.ts +++ b/apps/dashboard/lib/trpc/routers/key/delete.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, inArray, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -41,17 +42,39 @@ export const deleteKeys = rateLimitedProcedure(ratelimit.delete) } await db - .update(schema.keys) - .set({ deletedAt: new Date() }) - .where( - and( - eq(schema.keys.workspaceId, workspace.id), - inArray( - schema.keys.id, - workspace.keys.map((k) => k.id), - ), - ), - ) + .transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ deletedAt: new Date() }) + .where( + and( + eq(schema.keys.workspaceId, workspace.id), + inArray( + schema.keys.id, + workspace.keys.map((k) => k.id), + ), + ), + ); + insertAuditLogs( + tx, + workspace.keys.map((key) => ({ + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "key.delete", + description: `Deleted ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + })), + ); + }) .catch((_err) => { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -60,7 +83,7 @@ export const deleteKeys = rateLimitedProcedure(ratelimit.delete) }); }); - await ingestAuditLogs( + await ingestAuditLogsTinybird( workspace.keys.map((key) => ({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, diff --git a/apps/dashboard/lib/trpc/routers/key/deleteRootKey.ts b/apps/dashboard/lib/trpc/routers/key/deleteRootKey.ts index 61a27cbf18..7c57580139 100644 --- a/apps/dashboard/lib/trpc/routers/key/deleteRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/key/deleteRootKey.ts @@ -1,6 +1,7 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, inArray, schema } from "@/lib/db"; import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -45,16 +46,37 @@ export const deleteRootKeys = rateLimitedProcedure(ratelimit.delete) id: true, }, }); - await db - .update(schema.keys) - .set({ deletedAt: new Date() }) - .where( - inArray( - schema.keys.id, - rootKeys.map((k) => k.id), - ), - ) + .transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ deletedAt: new Date() }) + .where( + inArray( + schema.keys.id, + rootKeys.map((k) => k.id), + ), + ); + await insertAuditLogs( + tx, + rootKeys.map((key) => ({ + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "key.delete", + description: `Deleted ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + })), + ); + }) .catch((_err) => { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -63,7 +85,7 @@ export const deleteRootKeys = rateLimitedProcedure(ratelimit.delete) }); }); - await ingestAuditLogs( + await ingestAuditLogsTinybird( rootKeys.map((key) => ({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, diff --git a/apps/dashboard/lib/trpc/routers/key/updateEnabled.ts b/apps/dashboard/lib/trpc/routers/key/updateEnabled.ts index e8c68d3c70..5389ca2cca 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateEnabled.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateEnabled.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -35,21 +36,41 @@ export const updateKeyEnabled = rateLimitedProcedure(ratelimit.update) }); } - await db - .update(schema.keys) - .set({ - enabled: input.enabled, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update enabled on this key. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + enabled: input.enabled, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update enabled on this key. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `${input.enabled ? "Enabled" : "Disabled"} ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateExpiration.ts b/apps/dashboard/lib/trpc/routers/key/updateExpiration.ts index 76df731310..d99250e33a 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateExpiration.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateExpiration.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -55,22 +56,46 @@ export const updateKeyExpiration = rateLimitedProcedure(ratelimit.update) code: "NOT_FOUND", }); } - - await db - .update(schema.keys) - .set({ - expires, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update expiration on this key. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + expires, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update expiration on this key. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `${ + input.expiration + ? `Changed expiration of ${key.id} to ${input.expiration.toUTCString()}` + : `Disabled expiration for ${key.id}` + }`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateMetadata.ts b/apps/dashboard/lib/trpc/routers/key/updateMetadata.ts index b2f8c316fb..d8ae50db04 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateMetadata.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateMetadata.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -48,22 +49,42 @@ export const updateKeyMetadata = rateLimitedProcedure(ratelimit.update) code: "NOT_FOUND", }); } - - await db - .update(schema.keys) - .set({ - meta: meta ? JSON.stringify(meta) : null, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update metadata on this key. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + meta: meta ? JSON.stringify(meta) : null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update metadata on this key. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Updated metadata of ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateName.ts b/apps/dashboard/lib/trpc/routers/key/updateName.ts index 33ef712e23..bcf372b9eb 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateName.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateName.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -36,21 +37,42 @@ export const updateKeyName = rateLimitedProcedure(ratelimit.update) code: "NOT_FOUND", }); } - await db - .update(schema.keys) - .set({ - name: input.name ?? null, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update name on this key. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + name: input.name ?? null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update name on this key. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed name of ${key.id} to ${input.name}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateOwnerId.ts b/apps/dashboard/lib/trpc/routers/key/updateOwnerId.ts index fa15f4fbcc..41c5574777 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateOwnerId.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateOwnerId.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -36,21 +37,42 @@ export const updateKeyOwnerId = rateLimitedProcedure(ratelimit.update) }); } - await db - .update(schema.keys) - .set({ - ownerId: input.ownerId ?? null, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update ownerId on this key. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + ownerId: input.ownerId ?? null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update ownerId on this key. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed ownerId of ${key.id} to ${input.ownerId}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateRatelimit.ts b/apps/dashboard/lib/trpc/routers/key/updateRatelimit.ts index 3d20ca9331..9c74a9f0b2 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRatelimit.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRatelimit.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -47,13 +48,12 @@ export const updateKeyRatelimit = rateLimitedProcedure(ratelimit.update) typeof ratelimitDuration !== "number" ) { throw new TRPCError({ - message: - "Invalid input. Please refer to the docs at https://www.unkey.com/docs/api-reference/keys/update for clarification.", + message: "Invalid input.", code: "BAD_REQUEST", }); } - try { - await db + await db.transaction(async (tx) => { + await tx .update(schema.keys) .set({ ratelimitAsync, @@ -61,15 +61,33 @@ export const updateKeyRatelimit = rateLimitedProcedure(ratelimit.update) ratelimitDuration, }) .where(eq(schema.keys.id, key.id)); - } catch (_err) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update ratelimit on this key. Please contact support using support@unkey.dev", + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed ratelimit of ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + meta: { + "ratelimit.async": ratelimitAsync, + "ratelimit.limit": ratelimitLimit, + "ratelimit.duration": ratelimitDuration, + }, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - } + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", @@ -92,17 +110,48 @@ export const updateKeyRatelimit = rateLimitedProcedure(ratelimit.update) location: ctx.audit.location, userAgent: ctx.audit.userAgent, }, + }).catch((err) => { + console.error(err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update ratelimit on this key. Please contact support using support@unkey.dev", + }); }); } else { await db - .update(schema.keys) - .set({ - ratelimitAsync: null, - ratelimitLimit: null, - ratelimitDuration: null, + .transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + ratelimitAsync: null, + ratelimitLimit: null, + ratelimitDuration: null, + }) + .where(eq(schema.keys.id, key.id)); + + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Disabled ratelimit of ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -110,7 +159,7 @@ export const updateKeyRatelimit = rateLimitedProcedure(ratelimit.update) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: key.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts index d0589f9988..6dde718bfa 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRemaining.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -27,70 +28,95 @@ export const updateKeyRemaining = rateLimitedProcedure(ratelimit.update) input.refill = undefined; } - const key = await db.query.keys - .findFirst({ - where: (table, { eq, and, isNull }) => - and(eq(table.id, input.keyId), isNull(table.deletedAt)), - with: { - workspace: true, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update remaining limits on this key. Please contact support using support@unkey.dev", - }); - }); - if (!key || key.workspace.tenantId !== ctx.tenant.id) { - throw new TRPCError({ - message: - "We are unable to find the correct key. Please contact support using support@unkey.dev.", - code: "NOT_FOUND", - }); - } await db - .update(schema.keys) - .set({ - remaining: input.remaining ?? null, - refillInterval: - input.refill?.interval === "none" || input.refill?.interval === undefined - ? null - : input.refill?.interval, - refillAmount: input.refill?.amount ?? null, - lastRefillAt: input.refill?.interval ? new Date() : null, + .transaction(async (tx) => { + const key = await tx.query.keys.findFirst({ + where: (table, { eq, and, isNull }) => + and(eq(table.id, input.keyId), isNull(table.deletedAt)), + with: { + workspace: true, + }, + }); + + if (!key || key.workspace.tenantId !== ctx.tenant.id) { + throw new TRPCError({ + message: + "We are unable to find the correct key. Please contact support using support@unkey.dev.", + code: "NOT_FOUND", + }); + } + await tx + .update(schema.keys) + .set({ + remaining: input.remaining ?? null, + refillInterval: + input.refill?.interval === "none" || input.refill?.interval === undefined + ? null + : input.refill?.interval, + refillAmount: input.refill?.amount ?? null, + lastRefillAt: input.refill?.interval ? new Date() : null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update remaining on this key. Please contact support using support@unkey.dev", + }); + }); + await insertAuditLogs(tx, { + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: input.limitEnabled + ? `Changed remaining for ${key.id} to remaining=${input.remaining}, refill=${ + input.refill ? `${input.refill.amount}@${input.refill.interval}` : "none" + }` + : `Disabled limit for ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: input.limitEnabled + ? `Changed remaining for ${key.id} to remaining=${input.remaining}, refill=${ + input.refill ? `${input.refill.amount}@${input.refill.interval}` : "none" + }` + : `Disabled limit for ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: - "We were unable to update remaining on this key. Please contact support using support@unkey.dev", + "We were unable to update remaining limits on this key. Please contact support using support@unkey.dev", }); }); - - await ingestAuditLogs({ - workspaceId: key.workspace.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "key.update", - description: input.limitEnabled - ? `Changed remaining for ${key.id} to remaining=${input.remaining}, refill=${ - input.refill ? `${input.refill.amount}@${input.refill.interval}` : "none" - }` - : `Disabled limit for ${key.id}`, - resources: [ - { - type: "key", - id: key.id, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - return true; }); diff --git a/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts b/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts index 3d2db9049a..36f4b4c3f0 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts @@ -1,6 +1,7 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { auth, t } from "../../trpc"; @@ -48,21 +49,42 @@ export const updateRootKeyName = t.procedure }); } - await db - .update(schema.keys) - .set({ - name: input.name ?? null, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update root key name. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + name: input.name ?? null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update root key name. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed name of ${key.id} to ${input.name}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", @@ -81,5 +103,4 @@ export const updateRootKeyName = t.procedure userAgent: ctx.audit.userAgent, }, }); - return true; }); diff --git a/apps/dashboard/lib/trpc/routers/llmGateway/create.ts b/apps/dashboard/lib/trpc/routers/llmGateway/create.ts index b823a68fcd..53366df2af 100644 --- a/apps/dashboard/lib/trpc/routers/llmGateway/create.ts +++ b/apps/dashboard/lib/trpc/routers/llmGateway/create.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { DatabaseError } from "@planetscale/database"; import { TRPCError } from "@trpc/server"; @@ -36,12 +37,32 @@ export const createLlmGateway = rateLimitedProcedure(ratelimit.create) const llmGatewayId = newId("llmGateway"); await db - .insert(schema.llmGateways) - .values({ - id: llmGatewayId, - subdomain: input.subdomain, - name: input.subdomain, - workspaceId: ws.id, + .transaction(async (tx) => { + await tx.insert(schema.llmGateways).values({ + id: llmGatewayId, + subdomain: input.subdomain, + name: input.subdomain, + workspaceId: ws.id, + }); + await insertAuditLogs(tx, { + workspaceId: ws.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "llmGateway.create", + description: `Created ${llmGatewayId}`, + resources: [ + { + type: "gateway", + id: llmGatewayId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) .catch((err) => { if (err instanceof DatabaseError && err.body.message.includes("Duplicate entry")) { @@ -57,7 +78,7 @@ export const createLlmGateway = rateLimitedProcedure(ratelimit.create) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: ws.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/llmGateway/delete.ts b/apps/dashboard/lib/trpc/routers/llmGateway/delete.ts index 32d1752e8a..991eba0cd0 100644 --- a/apps/dashboard/lib/trpc/routers/llmGateway/delete.ts +++ b/apps/dashboard/lib/trpc/routers/llmGateway/delete.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const deleteLlmGateway = rateLimitedProcedure(ratelimit.delete) @@ -35,17 +36,38 @@ export const deleteLlmGateway = rateLimitedProcedure(ratelimit.delete) }); } - await db - .delete(schema.llmGateways) - .where(eq(schema.llmGateways.id, input.gatewayId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to delete the LLM gateway. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .delete(schema.llmGateways) + .where(eq(schema.llmGateways.id, input.gatewayId)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to delete the LLM gateway. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: llmGateway.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "llmGateway.delete", + description: `Deleted ${llmGateway.id}`, + resources: [ + { + type: "gateway", + id: llmGateway.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: llmGateway.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/monitor/verification/create.ts b/apps/dashboard/lib/trpc/routers/monitor/verification/create.ts deleted file mode 100644 index cd2a7e601d..0000000000 --- a/apps/dashboard/lib/trpc/routers/monitor/verification/create.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { type Webhook, db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError, createCallerFactory } from "@trpc/server"; -import { newId } from "@unkey/id"; -import { z } from "zod"; -import { router } from "../.."; - -export const createVerificationMonitor = rateLimitedProcedure(ratelimit.create) - .input( - z.object({ - interval: z - .number() - .min(60 * 1000) - .max(30 * 24 * 60 * 60 * 1000), - webhookUrl: z.string(), - keySpaceId: z.string(), - }), - ) - .mutation(async ({ input, ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - with: { - keySpaces: { - where: (table, { eq }) => eq(table.id, input.keySpaceId), - }, - webhooks: { - where: (table, { eq }) => eq(table.destination, input.webhookUrl), - }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create the reporter. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - if (ws.keySpaces.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct keyspace. Please contact support using support@unkey.dev.", - }); - } - - let webhookId: string; - if (ws.webhooks.length > 0) { - webhookId = ws.webhooks[0].id; - } else { - const trpc = createCallerFactory()(router)(ctx); - const { id } = await trpc.webhook.create({ - destination: input.webhookUrl, - }); - webhookId = id; - } - - const reporterId = newId("reporter"); - - await db - .insert(schema.verificationMonitors) - .values({ - id: reporterId, - interval: input.interval, - keySpaceId: input.keySpaceId, - nextExecution: 0, - webhookId, - workspaceId: ws.id, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create the reporter. Please contact support using support@unkey.dev.", - }); - }); - - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { type: "user", id: ctx.user.id }, - event: "reporter.create", - description: `Created ${reporterId}`, - resources: [ - { - type: "reporter", - id: reporterId, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return { - id: reporterId, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts b/apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts index 7850622b94..0489b3b451 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { DatabaseError } from "@planetscale/database"; import { newId } from "@unkey/id"; @@ -35,28 +36,50 @@ export const createNamespace = rateLimitedProcedure(ratelimit.create) } const namespaceId = newId("ratelimitNamespace"); - try { - await db.insert(schema.ratelimitNamespaces).values({ - id: namespaceId, - name: input.name, - workspaceId: ws.id, + await db + .transaction(async (tx) => { + await tx.insert(schema.ratelimitNamespaces).values({ + id: namespaceId, + name: input.name, + workspaceId: ws.id, - createdAt: new Date(), - }); - } catch (e) { - if (e instanceof DatabaseError && e.body.message.includes("desc = Duplicate entry")) { + createdAt: new Date(), + }); + await insertAuditLogs(tx, { + workspaceId: ws.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitNamespace.create", + description: `Created ${namespaceId}`, + resources: [ + { + type: "ratelimitNamespace", + id: namespaceId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((e) => { + if (e instanceof DatabaseError && e.body.message.includes("desc = Duplicate entry")) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "duplicate namespace name. Please use a unique name for each namespace.", + }); + } throw new TRPCError({ - code: "PRECONDITION_FAILED", - message: "duplicate namespace name. Please use a unique name for each namespace.", + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to create namspace. Please contact support using support@unkey.dev", }); - } - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "We are unable to create namspace. Please contact support using support@unkey.dev", }); - } - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: ws.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts index bf9bf1cf65..23d1855ed9 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/createOverride.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, isNull, schema, sql } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { newId } from "@unkey/id"; @@ -47,7 +48,6 @@ export const createOverride = rateLimitedProcedure(ratelimit.create) } const id = newId("ratelimitOverride"); - await db .transaction(async (tx) => { const existing = await tx @@ -81,6 +81,29 @@ export const createOverride = rateLimitedProcedure(ratelimit.create) createdAt: new Date(), async: input.async, }); + await insertAuditLogs(tx, { + workspaceId: namespace.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitOverride.create", + description: `Created ${input.identifier}`, + resources: [ + { + type: "ratelimitNamespace", + id: input.namespaceId, + }, + { + type: "ratelimitOverride", + id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) .catch((_err) => { throw new TRPCError({ @@ -90,7 +113,7 @@ export const createOverride = rateLimitedProcedure(ratelimit.create) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: namespace.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts b/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts index 90b1a7d09b..f15b66c6b5 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/deleteNamespace.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const deleteNamespace = rateLimitedProcedure(ratelimit.delete) @@ -47,7 +48,26 @@ export const deleteNamespace = rateLimitedProcedure(ratelimit.delete) .set({ deletedAt: new Date() }) .where(eq(schema.ratelimitNamespaces.id, input.namespaceId)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: namespace.workspaceId, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitNamespace.delete", + description: `Deleted ${namespace.id}`, + resources: [ + { + type: "ratelimitNamespace", + id: namespace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: namespace.workspaceId, actor: { type: "user", @@ -65,9 +85,6 @@ export const deleteNamespace = rateLimitedProcedure(ratelimit.delete) location: ctx.audit.location, userAgent: ctx.audit.userAgent, }, - }).catch((err) => { - tx.rollback(); - throw err; }); const overrides = await tx.query.ratelimitOverrides.findMany({ @@ -87,7 +104,33 @@ export const deleteNamespace = rateLimitedProcedure(ratelimit.delete) "We are unable to delete the namespaces. Please contact support using support@unkey.dev", }); }); - await ingestAuditLogs( + await insertAuditLogs( + tx, + overrides.map(({ id }) => ({ + workspaceId: namespace.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitOverride.delete", + description: `Deleted ${id} as part of the ${namespace.id} deletion`, + resources: [ + { + type: "ratelimitNamespace", + id: namespace.id, + }, + { + type: "ratelimitOverride", + id: id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + })), + ); + await ingestAuditLogsTinybird( overrides.map(({ id }) => ({ workspaceId: namespace.workspace.id, actor: { diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts index 625ddb4c26..d5e432fe65 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/deleteOverride.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const deleteOverride = rateLimitedProcedure(ratelimit.create) @@ -47,19 +48,44 @@ export const deleteOverride = rateLimitedProcedure(ratelimit.create) }); } - await db - .update(schema.ratelimitOverrides) - .set({ deletedAt: new Date() }) - .where(eq(schema.ratelimitOverrides.id, override.id)) - .catch((_err) => { - throw new TRPCError({ - message: - "We are unable to delete the override. Please contact support using support@unkey.dev", - code: "INTERNAL_SERVER_ERROR", + await db.transaction(async (tx) => { + await tx + .update(schema.ratelimitOverrides) + .set({ deletedAt: new Date() }) + .where(eq(schema.ratelimitOverrides.id, override.id)) + .catch((_err) => { + throw new TRPCError({ + message: + "We are unable to delete the override. Please contact support using support@unkey.dev", + code: "INTERNAL_SERVER_ERROR", + }); }); + await insertAuditLogs(tx, { + workspaceId: override.namespace.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitOverride.delete", + description: `Deleted ${override.id}`, + resources: [ + { + type: "ratelimitNamespace", + id: override.namespace.id, + }, + { + type: "ratelimitOverride", + id: override.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: override.namespace.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/updateNamespaceName.ts b/apps/dashboard/lib/trpc/routers/ratelimit/updateNamespaceName.ts index bae3131167..3dc1d50d0c 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/updateNamespaceName.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/updateNamespaceName.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const updateNamespaceName = rateLimitedProcedure(ratelimit.update) @@ -49,20 +50,41 @@ export const updateNamespaceName = rateLimitedProcedure(ratelimit.update) }); } - await db - .update(schema.ratelimitNamespaces) - .set({ - name: input.name, - }) - .where(eq(schema.ratelimitNamespaces.id, input.namespaceId)) - .catch((_err) => { - throw new TRPCError({ - message: - "We are unable to update the namespace name. Please contact support using support@unkey.dev", - code: "INTERNAL_SERVER_ERROR", + await db.transaction(async (tx) => { + await tx + .update(schema.ratelimitNamespaces) + .set({ + name: input.name, + }) + .where(eq(schema.ratelimitNamespaces.id, input.namespaceId)) + .catch((_err) => { + throw new TRPCError({ + message: + "We are unable to update the namespace name. Please contact support using support@unkey.dev", + code: "INTERNAL_SERVER_ERROR", + }); }); + await insertAuditLogs(tx, { + workspaceId: ws.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitNamespace.update", + description: `Changed ${namespace.id} name from ${namespace.name} to ${input.name}`, + resources: [ + { + type: "ratelimitNamespace", + id: namespace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); - await ingestAuditLogs({ + }); + await ingestAuditLogsTinybird({ workspaceId: ws.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts b/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts index b7097519fd..1685fac9ce 100644 --- a/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts +++ b/apps/dashboard/lib/trpc/routers/ratelimit/updateOverride.ts @@ -1,8 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; export const updateOverride = rateLimitedProcedure(ratelimit.update) @@ -50,24 +51,49 @@ export const updateOverride = rateLimitedProcedure(ratelimit.update) }); } - await db - .update(schema.ratelimitOverrides) - .set({ - limit: input.limit, - duration: input.duration, - updatedAt: new Date(), - async: input.async, - }) - .where(eq(schema.ratelimitOverrides.id, override.id)) - .catch((_err) => { - throw new TRPCError({ - message: - "We are unable to update the override. Please contact support using support@unkey.dev.", - code: "INTERNAL_SERVER_ERROR", + await db.transaction(async (tx) => { + await tx + .update(schema.ratelimitOverrides) + .set({ + limit: input.limit, + duration: input.duration, + updatedAt: new Date(), + async: input.async, + }) + .where(eq(schema.ratelimitOverrides.id, override.id)) + .catch((_err) => { + throw new TRPCError({ + message: + "We are unable to update the override. Please contact support using support@unkey.dev.", + code: "INTERNAL_SERVER_ERROR", + }); }); + await insertAuditLogs(tx, { + workspaceId: override.namespace.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "ratelimitOverride.update", + description: `Changed ${override.id} limits from ${override.limit}/${override.duration} to ${input.limit}/${input.duration}`, + resources: [ + { + type: "ratelimitNamespace", + id: override.namespace.id, + }, + { + type: "ratelimitOverride", + id: override.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: override.namespace.workspace.id, actor: { type: "user", diff --git a/apps/dashboard/lib/trpc/routers/rbac.ts b/apps/dashboard/lib/trpc/routers/rbac.ts index 6e6b1980c7..dc0e3cc0d1 100644 --- a/apps/dashboard/lib/trpc/routers/rbac.ts +++ b/apps/dashboard/lib/trpc/routers/rbac.ts @@ -1,10 +1,12 @@ import { type Permission, and, db, eq, schema } from "@/lib/db"; -import { type UnkeyAuditLog, ingestAuditLogs } from "@/lib/tinybird"; +import { insertAuditLogs } from "@/lib/audit"; +import { type UnkeyAuditLog, ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; import { unkeyPermissionValidation } from "@unkey/rbac"; +import { Text } from "@visx/text"; import { z } from "zod"; import type { Context } from "../context"; import { t } from "../trpc"; @@ -66,15 +68,18 @@ export const rbacRouter = t.router({ const { permissions, auditLogs } = await upsertPermissions(ctx, rootKey.workspaceId, [ permission.data, ]); - await db - .insert(schema.keysPermissions) - .values({ - keyId: rootKey.id, - permissionId: permissions[0].id, - workspaceId: permissions[0].workspaceId, - }) - .onDuplicateKeyUpdate({ set: { permissionId: permissions[0].id } }); - await ingestAuditLogs(auditLogs); + await db.transaction(async (tx) => { + await tx + .insert(schema.keysPermissions) + .values({ + keyId: rootKey.id, + permissionId: permissions[0].id, + workspaceId: permissions[0].workspaceId, + }) + .onDuplicateKeyUpdate({ set: { permissionId: permissions[0].id } }); + await insertAuditLogs(tx, auditLogs); + }); + await ingestAuditLogsTinybird(auditLogs); }), removePermissionFromRootKey: rateLimitedProcedure(ratelimit.update) .input( @@ -123,15 +128,37 @@ export const rbacRouter = t.router({ }); } - await db - .delete(schema.keysPermissions) - .where( - and( - eq(schema.keysPermissions.keyId, permissionRelation.keyId), - eq(schema.keysPermissions.workspaceId, permissionRelation.workspaceId), - eq(schema.keysPermissions.permissionId, permissionRelation.permissionId), - ), - ); + await db.transaction(async (tx) => { + await tx + .delete(schema.keysPermissions) + .where( + and( + eq(schema.keysPermissions.keyId, permissionRelation.keyId), + eq(schema.keysPermissions.workspaceId, permissionRelation.workspaceId), + eq(schema.keysPermissions.permissionId, permissionRelation.permissionId), + ), + ); + await insertAuditLogs(tx, { + workspaceId: permissionRelation.workspaceId, + actor: { type: "user", id: ctx.user!.id }, + event: "authorization.disconnect_permission_and_key", + description: `Disconnected ${permissionRelation.keyId} from ${permissionRelation.permissionId}`, + resources: [ + { + type: "permission", + id: permissionRelation.permissionId, + }, + { + type: "key", + id: permissionRelation.keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }); }), connectPermissionToRole: rateLimitedProcedure(ratelimit.update) .input( @@ -179,12 +206,34 @@ export const rbacRouter = t.router({ permissionId: permission.id, roleId: role.id, }; - await db - .insert(schema.rolesPermissions) - .values({ ...tuple, createdAt: new Date() }) - .onDuplicateKeyUpdate({ - set: { ...tuple, updatedAt: new Date() }, + await db.transaction(async (tx) => { + await tx + .insert(schema.rolesPermissions) + .values({ ...tuple, createdAt: new Date() }) + .onDuplicateKeyUpdate({ + set: { ...tuple, updatedAt: new Date() }, + }); + await insertAuditLogs(tx, { + workspaceId: tuple.workspaceId, + actor: { type: "user", id: ctx.user!.id }, + event: "authorization.connect_role_and_permission", + description: `Connected ${tuple.roleId} to ${tuple.permissionId}`, + resources: [ + { + type: "permission", + id: tuple.permissionId, + }, + { + type: "role", + id: tuple.roleId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); }), disconnectPermissionToRole: rateLimitedProcedure(ratelimit.update) .input( @@ -204,15 +253,37 @@ export const rbacRouter = t.router({ message: "workspace not found", }); } - await db - .delete(schema.rolesPermissions) - .where( - and( - eq(schema.rolesPermissions.workspaceId, workspace.id), - eq(schema.rolesPermissions.roleId, input.roleId), - eq(schema.rolesPermissions.permissionId, input.permissionId), - ), - ); + await db.transaction(async (tx) => { + await tx + .delete(schema.rolesPermissions) + .where( + and( + eq(schema.rolesPermissions.workspaceId, workspace.id), + eq(schema.rolesPermissions.roleId, input.roleId), + eq(schema.rolesPermissions.permissionId, input.permissionId), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user!.id }, + event: "authorization.disconnect_role_and_permissions", + description: `Disconnected ${input.roleId} from ${input.permissionId}`, + resources: [ + { + type: "permission", + id: input.permissionId, + }, + { + type: "role", + id: input.roleId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }); }), connectRoleToKey: rateLimitedProcedure(ratelimit.update) .input( @@ -260,12 +331,34 @@ export const rbacRouter = t.router({ keyId: key.id, roleId: role.id, }; - await db - .insert(schema.keysRoles) - .values({ ...tuple, createdAt: new Date() }) - .onDuplicateKeyUpdate({ - set: { ...tuple, updatedAt: new Date() }, + await db.transaction(async (tx) => { + await tx + .insert(schema.keysRoles) + .values({ ...tuple, createdAt: new Date() }) + .onDuplicateKeyUpdate({ + set: { ...tuple, updatedAt: new Date() }, + }); + await insertAuditLogs(tx, { + workspaceId: tuple.workspaceId, + actor: { type: "user", id: ctx.user!.id }, + event: "authorization.connect_role_and_key", + description: `Connected ${tuple.roleId} with ${tuple.keyId}`, + resources: [ + { + type: "key", + id: tuple.keyId, + }, + { + type: "role", + id: tuple.roleId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); }), disconnectRoleFromKey: rateLimitedProcedure(ratelimit.update) .input( @@ -316,65 +409,111 @@ export const rbacRouter = t.router({ }); } const roleId = newId("role"); - await db.insert(schema.roles).values({ - id: roleId, - name: input.name, - description: input.description, - workspaceId: workspace.id, - }); - await ingestAuditLogs({ - workspaceId: workspace.id, - event: "role.create", - actor: { - type: "user", - id: ctx.user.id, - }, - description: `Created ${roleId}`, - resources: [ - { - type: "role", - id: roleId, + await db.transaction(async (tx) => { + await tx.insert(schema.roles).values({ + id: roleId, + name: input.name, + description: input.description, + workspaceId: workspace.id, + }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "role.create", + actor: { + type: "user", + id: ctx.user.id, }, - ], - - context: { - userAgent: ctx.audit.userAgent, - location: ctx.audit.location, - }, - }); - - if (input.permissionIds && input.permissionIds.length > 0) { - await db.insert(schema.rolesPermissions).values( - input.permissionIds.map((permissionId) => ({ - permissionId, - roleId: roleId, - workspaceId: workspace.id, - })), - ); - await ingestAuditLogs( - input.permissionIds.map((permissionId) => ({ - workspaceId: workspace.id, - event: "authorization.connect_role_and_permission", - actor: { - type: "user", - id: ctx.user.id, + description: `Created ${roleId}`, + resources: [ + { + type: "role", + id: roleId, }, - description: `Connected ${roleId} and ${permissionId}`, - resources: [ - { type: "role", id: roleId }, - { - type: "permission", - id: permissionId, - }, - ], + ], - context: { - userAgent: ctx.audit.userAgent, - location: ctx.audit.location, + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + await ingestAuditLogsTinybird({ + workspaceId: workspace.id, + event: "role.create", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Created ${roleId}`, + resources: [ + { + type: "role", + id: roleId, }, - })), - ); - } + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + + if (input.permissionIds && input.permissionIds.length > 0) { + await tx.insert(schema.rolesPermissions).values( + input.permissionIds.map((permissionId) => ({ + permissionId, + roleId: roleId, + workspaceId: workspace.id, + })), + ); + await insertAuditLogs( + tx, + input.permissionIds.map((permissionId) => ({ + workspaceId: workspace.id, + event: "authorization.connect_role_and_permission", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Connected ${roleId} and ${permissionId}`, + resources: [ + { type: "role", id: roleId }, + { + type: "permission", + id: permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + })), + ); + await ingestAuditLogsTinybird( + input.permissionIds.map((permissionId) => ({ + workspaceId: workspace.id, + event: "authorization.connect_role_and_permission", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Connected ${roleId} and ${permissionId}`, + resources: [ + { type: "role", id: roleId }, + { + type: "permission", + id: permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + })), + ); + } + }); return { roleId }; }), updateRole: rateLimitedProcedure(ratelimit.update) @@ -408,7 +547,24 @@ export const rbacRouter = t.router({ message: "role not found", }); } - await db.update(schema.roles).set(input).where(eq(schema.roles.id, input.id)); + await db.transaction(async (tx) => { + await tx.update(schema.roles).set(input).where(eq(schema.roles.id, input.id)); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "role.update", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Updated ${input.id}`, + resources: [{ type: "role", id: input.id }], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + }); }), deleteRole: rateLimitedProcedure(ratelimit.delete) .input( @@ -439,9 +595,28 @@ export const rbacRouter = t.router({ message: "role not found", }); } - await db - .delete(schema.roles) - .where(and(eq(schema.roles.id, input.roleId), eq(schema.roles.workspaceId, workspace.id))); + await db.transaction(async (tx) => { + await tx + .delete(schema.roles) + .where( + and(eq(schema.roles.id, input.roleId), eq(schema.roles.workspaceId, workspace.id)), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "role.delete", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Deleted ${input.roleId}`, + resources: [{ type: "role", id: input.roleId }], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + }); }), createPermission: rateLimitedProcedure(ratelimit.create) .input( @@ -463,13 +638,35 @@ export const rbacRouter = t.router({ }); } const permissionId = newId("permission"); - await db.insert(schema.permissions).values({ - id: permissionId, - name: input.name, - description: input.description, - workspaceId: workspace.id, + await db.transaction(async (tx) => { + await tx.insert(schema.permissions).values({ + id: permissionId, + name: input.name, + description: input.description, + workspaceId: workspace.id, + }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "permission.create", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Created ${permissionId}`, + resources: [ + { + type: "permission", + id: permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, event: "permission.create", actor: { @@ -523,14 +720,36 @@ export const rbacRouter = t.router({ message: "permission not found", }); } - await db - .update(schema.permissions) - .set({ - name: input.name, - description: input.description, - updatedAt: new Date(), - }) - .where(eq(schema.permissions.id, input.id)); + await db.transaction(async (tx) => { + await tx + .update(schema.permissions) + .set({ + name: input.name, + description: input.description, + updatedAt: new Date(), + }) + .where(eq(schema.permissions.id, input.id)); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "permission.update", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Updated ${input.id}`, + resources: [ + { + type: "permission", + id: input.id, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + }); }), deletePermission: rateLimitedProcedure(ratelimit.delete) .input( @@ -561,14 +780,36 @@ export const rbacRouter = t.router({ message: "permission not found", }); } - await db - .delete(schema.permissions) - .where( - and( - eq(schema.permissions.id, input.permissionId), - eq(schema.permissions.workspaceId, workspace.id), - ), - ); + await db.transaction(async (tx) => { + await tx + .delete(schema.permissions) + .where( + and( + eq(schema.permissions.id, input.permissionId), + eq(schema.permissions.workspaceId, workspace.id), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "permission.delete", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Deleted ${input.permissionId}`, + resources: [ + { + type: "permission", + id: input.permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); + }); }), }); diff --git a/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts b/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts index 149c10b452..cd7e195653 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { unkeyPermissionValidation } from "@unkey/rbac"; @@ -65,23 +66,47 @@ export const addPermissionToRootKey = rateLimitedProcedure(ratelimit.create) permission.data, ]); const p = permissions[0]; - await db - .insert(schema.keysPermissions) - .values({ - keyId: rootKey.id, - permissionId: p.id, - workspaceId: p.workspaceId, - }) - .onDuplicateKeyUpdate({ set: { permissionId: p.id } }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to add permission to the root key. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .insert(schema.keysPermissions) + .values({ + keyId: rootKey.id, + permissionId: p.id, + workspaceId: p.workspaceId, + }) + .onDuplicateKeyUpdate({ set: { permissionId: p.id } }) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to add permission to the root key. Please contact support using support@unkey.dev.", + }); }); - }); - - await ingestAuditLogs([ + await insertAuditLogs(tx, [ + ...auditLogs, + { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.connect_permission_and_key", + description: `Attached ${p.id} to ${rootKey.id}`, + resources: [ + { + type: "key", + id: rootKey.id, + }, + { + type: "permission", + id: p.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }, + ]); + }); + await ingestAuditLogsTinybird([ ...auditLogs, { workspaceId: workspace.id, diff --git a/apps/dashboard/lib/trpc/routers/rbac/connectPermissionToRole.ts b/apps/dashboard/lib/trpc/routers/rbac/connectPermissionToRole.ts index 02f77b4838..a291cb529f 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/connectPermissionToRole.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/connectPermissionToRole.ts @@ -1,3 +1,4 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; @@ -60,17 +61,39 @@ export const connectPermissionToRole = rateLimitedProcedure(ratelimit.update) permissionId: permission.id, roleId: role.id, }; - await db - .insert(schema.rolesPermissions) - .values({ ...tuple, createdAt: new Date() }) - .onDuplicateKeyUpdate({ - set: { ...tuple, updatedAt: new Date() }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to connect the permission to the role. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .insert(schema.rolesPermissions) + .values({ ...tuple, createdAt: new Date() }) + .onDuplicateKeyUpdate({ + set: { ...tuple, updatedAt: new Date() }, + }) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to connect the permission to the role. Please contact support using support@unkey.dev.", + }); }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.connect_role_and_permission", + description: `Connect role ${role.id} to ${permission.id}`, + resources: [ + { + type: "role", + id: role.id, + }, + { + type: "permission", + id: permission.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); }); diff --git a/apps/dashboard/lib/trpc/routers/rbac/connectRoleToKey.ts b/apps/dashboard/lib/trpc/routers/rbac/connectRoleToKey.ts index 10b13c913b..631ad8e7f8 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/connectRoleToKey.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/connectRoleToKey.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -61,21 +62,43 @@ export const connectRoleToKey = rateLimitedProcedure(ratelimit.update) keyId: key.id, roleId: role.id, }; - await db - .insert(schema.keysRoles) - .values({ ...tuple, createdAt: new Date() }) - .onDuplicateKeyUpdate({ - set: { ...tuple, updatedAt: new Date() }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to connect the role and key. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .insert(schema.keysRoles) + .values({ ...tuple, createdAt: new Date() }) + .onDuplicateKeyUpdate({ + set: { ...tuple, updatedAt: new Date() }, + }) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to connect the role and key. Please contact support using support@unkey.dev.", + }); }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.connect_role_and_key", + description: `Connect role ${role.id} to ${key.id}`, + resources: [ + { + type: "role", + id: role.id, + }, + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "authorization.connect_role_and_key", diff --git a/apps/dashboard/lib/trpc/routers/rbac/createPermission.ts b/apps/dashboard/lib/trpc/routers/rbac/createPermission.ts index 240a683912..5bf352c8e9 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/createPermission.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/createPermission.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; @@ -43,12 +44,33 @@ export const createPermission = rateLimitedProcedure(ratelimit.create) } const permissionId = newId("permission"); await db - .insert(schema.permissions) - .values({ - id: permissionId, - name: input.name, - description: input.description, - workspaceId: workspace.id, + .transaction(async (tx) => { + await tx.insert(schema.permissions).values({ + id: permissionId, + name: input.name, + description: input.description, + workspaceId: workspace.id, + }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "permission.create", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Created ${permissionId}`, + resources: [ + { + type: "permission", + id: permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); }) .catch((_err) => { throw new TRPCError({ @@ -57,7 +79,7 @@ export const createPermission = rateLimitedProcedure(ratelimit.create) "We are unable to create a permission. Please contact support using support@unkey.dev.", }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, event: "permission.create", actor: { diff --git a/apps/dashboard/lib/trpc/routers/rbac/createRole.ts b/apps/dashboard/lib/trpc/routers/rbac/createRole.ts index 931d245119..6714ec0a22 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/createRole.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/createRole.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; @@ -42,73 +43,119 @@ export const createRole = rateLimitedProcedure(ratelimit.create) }); } const roleId = newId("role"); - await db - .insert(schema.roles) - .values({ - id: roleId, - name: input.name, - description: input.description, - workspaceId: workspace.id, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create a role. Please contact support using support@unkey.dev.", + await db.transaction(async (tx) => { + await tx + .insert(schema.roles) + .values({ + id: roleId, + name: input.name, + description: input.description, + workspaceId: workspace.id, + }) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to create a role. Please contact support using support@unkey.dev.", + }); }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + event: "role.create", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Created ${roleId}`, + resources: [ + { + type: "role", + id: roleId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, }); - await ingestAuditLogs({ - workspaceId: workspace.id, - event: "role.create", - actor: { - type: "user", - id: ctx.user.id, - }, - description: `Created ${roleId}`, - resources: [ - { - type: "role", - id: roleId, + await ingestAuditLogsTinybird({ + workspaceId: workspace.id, + event: "role.create", + actor: { + type: "user", + id: ctx.user.id, }, - ], + description: `Created ${roleId}`, + resources: [ + { + type: "role", + id: roleId, + }, + ], - context: { - userAgent: ctx.audit.userAgent, - location: ctx.audit.location, - }, - }); + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + }); - if (input.permissionIds && input.permissionIds.length > 0) { - await db.insert(schema.rolesPermissions).values( - input.permissionIds.map((permissionId) => ({ - permissionId, - roleId: roleId, - workspaceId: workspace.id, - })), - ); - await ingestAuditLogs( - input.permissionIds.map((permissionId) => ({ - workspaceId: workspace.id, - event: "authorization.connect_role_and_permission", - actor: { - type: "user", - id: ctx.user.id, - }, - description: `Connected ${roleId} and ${permissionId}`, - resources: [ - { type: "role", id: roleId }, - { - type: "permission", - id: permissionId, + if (input.permissionIds && input.permissionIds.length > 0) { + await tx.insert(schema.rolesPermissions).values( + input.permissionIds.map((permissionId) => ({ + permissionId, + roleId: roleId, + workspaceId: workspace.id, + })), + ); + await insertAuditLogs( + tx, + input.permissionIds.map((permissionId) => ({ + workspaceId: workspace.id, + event: "authorization.connect_role_and_permission", + actor: { + type: "user", + id: ctx.user.id, }, - ], + description: `Connected ${roleId} and ${permissionId}`, + resources: [ + { type: "role", id: roleId }, + { + type: "permission", + id: permissionId, + }, + ], - context: { - userAgent: ctx.audit.userAgent, - location: ctx.audit.location, - }, - })), - ); - } + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + })), + ); + await ingestAuditLogsTinybird( + input.permissionIds.map((permissionId) => ({ + workspaceId: workspace.id, + event: "authorization.connect_role_and_permission", + actor: { + type: "user", + id: ctx.user.id, + }, + description: `Connected ${roleId} and ${permissionId}`, + resources: [ + { type: "role", id: roleId }, + { + type: "permission", + id: permissionId, + }, + ], + + context: { + userAgent: ctx.audit.userAgent, + location: ctx.audit.location, + }, + })), + ); + } + }); return { roleId }; }); diff --git a/apps/dashboard/lib/trpc/routers/rbac/deletePermission.ts b/apps/dashboard/lib/trpc/routers/rbac/deletePermission.ts index 371e243fd8..de290538c1 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/deletePermission.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/deletePermission.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -44,14 +45,34 @@ export const deletePermission = rateLimitedProcedure(ratelimit.delete) }); } await db - .delete(schema.permissions) - .where( - and( - eq(schema.permissions.id, input.permissionId), - eq(schema.permissions.workspaceId, workspace.id), - ), - ) - .catch((_err) => { + .transaction(async (tx) => { + await tx + .delete(schema.permissions) + .where( + and( + eq(schema.permissions.id, input.permissionId), + eq(schema.permissions.workspaceId, workspace.id), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "permission.delete", + description: `Deleted permission ${input.permissionId}`, + resources: [ + { + type: "permission", + id: input.permissionId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -59,7 +80,7 @@ export const deletePermission = rateLimitedProcedure(ratelimit.delete) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "permission.delete", diff --git a/apps/dashboard/lib/trpc/routers/rbac/deleteRole.ts b/apps/dashboard/lib/trpc/routers/rbac/deleteRole.ts index 0e249a90a7..845e93f9e8 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/deleteRole.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/deleteRole.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -42,18 +43,36 @@ export const deleteRole = rateLimitedProcedure(ratelimit.delete) "We are unable to find the correct role. Please contact support using support@unkey.dev.", }); } - await db - .delete(schema.roles) - .where(and(eq(schema.roles.id, input.roleId), eq(schema.roles.workspaceId, workspace.id))) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to delete the role. Please contact support using support@unkey.dev", + await db.transaction(async (tx) => { + await tx + .delete(schema.roles) + .where(and(eq(schema.roles.id, input.roleId), eq(schema.roles.workspaceId, workspace.id))) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to delete the role. Please contact support using support@unkey.dev", + }); }); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "role.delete", + description: `Deleted role ${input.roleId}`, + resources: [ + { + type: "role", + id: input.roleId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, }); + }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "role.delete", diff --git a/apps/dashboard/lib/trpc/routers/rbac/disconnectPermissionFromRole.ts b/apps/dashboard/lib/trpc/routers/rbac/disconnectPermissionFromRole.ts index 5f9e8436d1..d483bf7780 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/disconnectPermissionFromRole.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/disconnectPermissionFromRole.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -32,15 +33,39 @@ export const disconnectPermissionFromRole = rateLimitedProcedure(ratelimit.updat }); } await db - .delete(schema.rolesPermissions) - .where( - and( - eq(schema.rolesPermissions.workspaceId, workspace.id), - eq(schema.rolesPermissions.roleId, input.roleId), - eq(schema.rolesPermissions.permissionId, input.permissionId), - ), - ) - .catch((_err) => { + .transaction(async (tx) => { + await tx + .delete(schema.rolesPermissions) + .where( + and( + eq(schema.rolesPermissions.workspaceId, workspace.id), + eq(schema.rolesPermissions.roleId, input.roleId), + eq(schema.rolesPermissions.permissionId, input.permissionId), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.disconnect_role_and_permissions", + description: `Disconnect role ${input.roleId} from permission ${input.permissionId}`, + resources: [ + { + type: "role", + id: input.roleId, + }, + { + type: "permission", + id: input.permissionId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -48,7 +73,7 @@ export const disconnectPermissionFromRole = rateLimitedProcedure(ratelimit.updat }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "authorization.disconnect_role_and_permissions", diff --git a/apps/dashboard/lib/trpc/routers/rbac/disconnectRoleFromKey.ts b/apps/dashboard/lib/trpc/routers/rbac/disconnectRoleFromKey.ts index 4a101c17ef..6f8e3c1a4a 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/disconnectRoleFromKey.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/disconnectRoleFromKey.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -32,15 +33,39 @@ export const disconnectRoleFromKey = rateLimitedProcedure(ratelimit.update) }); } await db - .delete(schema.keysRoles) - .where( - and( - eq(schema.keysRoles.workspaceId, workspace.id), - eq(schema.keysRoles.roleId, input.roleId), - eq(schema.keysRoles.keyId, input.keyId), - ), - ) - .catch((_err) => { + .transaction(async (tx) => { + await tx + .delete(schema.keysRoles) + .where( + and( + eq(schema.keysRoles.workspaceId, workspace.id), + eq(schema.keysRoles.roleId, input.roleId), + eq(schema.keysRoles.keyId, input.keyId), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.disconnect_role_and_key", + description: `Disconnect role ${input.roleId} from ${input.keyId}`, + resources: [ + { + type: "role", + id: input.roleId, + }, + { + type: "key", + id: input.keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -48,7 +73,7 @@ export const disconnectRoleFromKey = rateLimitedProcedure(ratelimit.update) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "authorization.disconnect_role_and_key", diff --git a/apps/dashboard/lib/trpc/routers/rbac/removePermissionFromRootKey.ts b/apps/dashboard/lib/trpc/routers/rbac/removePermissionFromRootKey.ts index 90a9b67042..5e055cbd47 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/removePermissionFromRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/removePermissionFromRootKey.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { and, db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -33,64 +34,81 @@ export const removePermissionFromRootKey = rateLimitedProcedure(ratelimit.update }); } - const key = await db.query.keys - .findFirst({ - where: (table, { and, eq, isNull }) => - and( - eq(schema.keys.forWorkspaceId, workspace.id), - eq(schema.keys.id, input.rootKeyId), - isNull(table.deletedAt), - ), - with: { - permissions: { - with: { - permission: true, + await db + .transaction(async (tx) => { + const key = await tx.query.keys.findFirst({ + where: (table, { and, eq, isNull }) => + and( + eq(schema.keys.forWorkspaceId, workspace.id), + eq(schema.keys.id, input.rootKeyId), + isNull(table.deletedAt), + ), + with: { + permissions: { + with: { + permission: true, + }, }, }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to remove permission from the root key. Please contact support using support@unkey.dev", }); - }); - if (!key) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Key ${input.rootKeyId} not found`, - }); - } - const permissionRelation = key.permissions.find( - (kp) => kp.permission.name === input.permissionName, - ); - if (!permissionRelation) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Key ${input.rootKeyId} did not have permission ${input.permissionName}`, - }); - } + if (!key) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Key ${input.rootKeyId} not found`, + }); + } - await db - .delete(schema.keysPermissions) - .where( - and( - eq(schema.keysPermissions.keyId, permissionRelation.keyId), - eq(schema.keysPermissions.workspaceId, permissionRelation.workspaceId), - eq(schema.keysPermissions.permissionId, permissionRelation.permissionId), - ), - ) - .catch((_err) => { + const permissionRelation = key.permissions.find( + (kp) => kp.permission.name === input.permissionName, + ); + if (!permissionRelation) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Key ${input.rootKeyId} did not have permission ${input.permissionName}`, + }); + } + + await tx + .delete(schema.keysPermissions) + .where( + and( + eq(schema.keysPermissions.keyId, permissionRelation.keyId), + eq(schema.keysPermissions.workspaceId, permissionRelation.workspaceId), + eq(schema.keysPermissions.permissionId, permissionRelation.permissionId), + ), + ); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "authorization.disconnect_permission_and_key", + description: `Disconnect ${input.permissionName} from ${input.rootKeyId}`, + resources: [ + { + type: "permission", + id: input.permissionName, + }, + { + type: "key", + id: input.rootKeyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: - "We are unable to remove the permission from the key. Please contact support using support@unkey.dev", + "We are unable to remove permission from the root key. Please contact support using support@unkey.dev", }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "authorization.disconnect_permission_and_key", diff --git a/apps/dashboard/lib/trpc/routers/rbac/updatePermission.ts b/apps/dashboard/lib/trpc/routers/rbac/updatePermission.ts index fe1c3b9267..480e6ad3f3 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/updatePermission.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/updatePermission.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -55,21 +56,42 @@ export const updatePermission = rateLimitedProcedure(ratelimit.update) } await db - .update(schema.permissions) - .set({ - name: input.name, - description: input.description, - updatedAt: new Date(), + .transaction(async (tx) => { + await tx + .update(schema.permissions) + .set({ + name: input.name, + description: input.description, + updatedAt: new Date(), + }) + .where(eq(schema.permissions.id, input.id)); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "permission.update", + description: `Update permission ${input.id}`, + resources: [ + { + type: "permission", + id: input.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) - .where(eq(schema.permissions.id, input.id)) - .catch((_err) => { + + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "We are unable to update the permission. Please contact support using support@unkey.dev.", }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "permission.update", diff --git a/apps/dashboard/lib/trpc/routers/rbac/updateRole.ts b/apps/dashboard/lib/trpc/routers/rbac/updateRole.ts index 2787e57507..455166eefc 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/updateRole.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/updateRole.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -31,7 +32,8 @@ export const updateRole = rateLimitedProcedure(ratelimit.update) }, }, }) - .catch((_err) => { + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -54,10 +56,27 @@ export const updateRole = rateLimitedProcedure(ratelimit.update) }); } await db - .update(schema.roles) - .set(input) - .where(eq(schema.roles.id, input.id)) - .catch((_err) => { + .transaction(async (tx) => { + await tx.update(schema.roles).set(input).where(eq(schema.roles.id, input.id)); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "role.update", + description: `Updated role ${input.id}`, + resources: [ + { + type: "role", + id: input.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + }) + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: @@ -65,7 +84,7 @@ export const updateRole = rateLimitedProcedure(ratelimit.update) }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "role.update", diff --git a/apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts b/apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts index 8a1d859335..028bb33ab9 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/upsertPermission.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { type Permission, db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; import type { Context } from "../../context"; @@ -45,7 +46,23 @@ export async function upsertPermission( "We are unable to upsert the permission. Please contact support using support@unkey.dev.", }); }); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId, + actor: { type: "user", id: ctx.user!.id }, + event: "permission.create", + description: `Created ${permission.id}`, + resources: [ + { + type: "permission", + id: permission.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId, actor: { type: "user", id: ctx.user!.id }, event: "permission.create", diff --git a/apps/dashboard/lib/trpc/routers/secrets/create.ts b/apps/dashboard/lib/trpc/routers/secrets/create.ts deleted file mode 100644 index aa2ff1955b..0000000000 --- a/apps/dashboard/lib/trpc/routers/secrets/create.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { db, schema } from "@/lib/db"; -import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { DatabaseError } from "@planetscale/database"; -import { TRPCError } from "@trpc/server"; -import { AesGCM } from "@unkey/encryption"; -import { newId } from "@unkey/id"; -import { z } from "zod"; - -export const createSecret = rateLimitedProcedure(ratelimit.create) - .input( - z.object({ - name: z.string(), - value: z.string(), - comment: z.string().optional(), - }), - ) - .mutation(async ({ ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "We are unable to create secret. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - - // const vault = connectVault(); - - // const encrypted = await vault.encrypt({ - // keyring: ws.id, - // data: input.value, - // }); - - const secretId = newId("secret"); - // await db - // .insert(schema.secrets) - // .values({ - // id: secretId, - // comment: input.comment, - // name: input.name, - // workspaceId: ws.id, - // encrypted: encrypted.encrypted, - // encryptionKeyId: encrypted.keyId, - // }) - // .catch((err) => { - // if (err instanceof DatabaseError && err.body.message.includes("desc = Duplicate entry")) { - // throw new TRPCError({ - // code: "PRECONDITION_FAILED", - // message: "Secrets must have unique names", - // }); - // } - // throw err; - // }); - - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "secret.create", - description: `Created ${secretId}`, - resources: [ - { - type: "secret", - id: secretId, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return { - secretId, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/secrets/decrypt.ts b/apps/dashboard/lib/trpc/routers/secrets/decrypt.ts deleted file mode 100644 index 1c46ffb01c..0000000000 --- a/apps/dashboard/lib/trpc/routers/secrets/decrypt.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { db } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; - -export const decryptSecret = rateLimitedProcedure(ratelimit.update) - .input( - z.object({ - secretId: z.string(), - }), - ) - .output( - z.object({ - value: z.string(), - }), - ) - .mutation(async ({ input, ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - with: { - secrets: { - where: (table, { eq }) => eq(table.id, input.secretId), - }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to decrypt the secret. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - const secret = ws.secrets.at(0); - if (!secret) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct secrets. Please contact support using support@unkey.dev.", - }); - } - - // const vault = connectVault(); - // const decrypted = await vault.decrypt({ - // keyring: ws.id, - // encrypted: secret.encrypted, - // }); - - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "secret.decrypt", - description: `Decrypted ${secret.id}`, - resources: [ - { - type: "secret", - id: secret.id, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return { - value: "", //decrypted.plaintext, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/secrets/update.ts b/apps/dashboard/lib/trpc/routers/secrets/update.ts deleted file mode 100644 index cd11b3ec13..0000000000 --- a/apps/dashboard/lib/trpc/routers/secrets/update.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { type Secret, db, eq, schema } from "@/lib/db"; -import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; - -export const updateSecret = rateLimitedProcedure(ratelimit.update) - .input( - z.object({ - secretId: z.string(), - name: z.string().optional(), - value: z.string().optional(), - comment: z.string().optional().nullable(), - }), - ) - .mutation(async ({ input, ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - with: { - secrets: { - where: (table, { eq }) => eq(table.id, input.secretId), - }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "We are unable to update secret. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - const secret = ws.secrets.at(0); - if (!secret) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct secrets. Please contact support using support@unkey.dev.", - }); - } - - const update: Partial = {}; - if (typeof input.name !== "undefined") { - update.name = input.name; - } - - if (typeof input.value !== "undefined") { - // const vault = connectVault(); - // const encrypted = await vault.encrypt({ - // keyring: ws.id, - // data: input.value, - // }); - // update.encrypted = encrypted.encrypted; - // update.encryptionKeyId = encrypted.keyId; - } - - if (typeof input.comment !== "undefined") { - update.comment = input.comment; - } - - if (Object.keys(update).length === 0) { - throw new TRPCError({ - code: "PRECONDITION_FAILED", - message: "No change detected", - }); - } - - await db - .update(schema.secrets) - .set(update) - .where(eq(schema.secrets.id, secret.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update the secret. Please contact support using support@unkey.dev.", - }); - }); - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "secret.update", - description: `Updated ${secret.id}`, - resources: [ - { - type: "secret", - id: secret.id, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return; - }); diff --git a/apps/dashboard/lib/trpc/routers/vercel.ts b/apps/dashboard/lib/trpc/routers/vercel.ts index fcce052d25..625ccafda2 100644 --- a/apps/dashboard/lib/trpc/routers/vercel.ts +++ b/apps/dashboard/lib/trpc/routers/vercel.ts @@ -1,6 +1,7 @@ +import { insertAuditLogs } from "@/lib/audit"; import { type VercelBinding, and, db, eq, schema } from "@/lib/db"; import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { TRPCError } from "@trpc/server"; import { newId } from "@unkey/id"; import { newKey } from "@unkey/keys"; @@ -74,7 +75,27 @@ export const vercelRouter = t.router({ remaining: null, deletedAt: null, }); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "key.create", + description: `Created ${keyId}`, + resources: [ + { + type: "key", + id: keyId, + }, + { + type: "vercelIntegration", + id: integration.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "key.create", @@ -124,7 +145,27 @@ export const vercelRouter = t.router({ workspaceId: integration.workspace.id, integrationId: integration.id, }); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.create", + description: `Created ${vercelBindingId} for ${keyId}`, + resources: [ + { + type: "vercelBinding", + id: vercelBindingId, + }, + { + type: "key", + id: keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.create", @@ -175,7 +216,7 @@ export const vercelRouter = t.router({ workspaceId: integration.workspace.id, integrationId: integration.id, }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.create", @@ -255,7 +296,30 @@ export const vercelRouter = t.router({ lastEditedBy: ctx.user.id, }) .where(eq(schema.vercelBindings.id, existingBinding.id)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.update", + description: `Updated ${existingBinding.id}`, + resources: [ + { + type: "vercelBinding", + id: existingBinding.id, + meta: { + vercelEnvironment: res.val.created.id, + }, + }, + { + type: "api", + id: input.apiId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.update", @@ -295,7 +359,7 @@ export const vercelRouter = t.router({ workspaceId: integration.workspace.id, integrationId: integration.id, }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.create", @@ -375,7 +439,23 @@ export const vercelRouter = t.router({ remaining: null, deletedAt: null, }); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "key.create", + description: `Created ${keyId}`, + resources: [ + { + type: "key", + id: keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "key.create", @@ -420,7 +500,30 @@ export const vercelRouter = t.router({ lastEditedBy: ctx.user.id, }) .where(eq(schema.vercelBindings.id, existingBinding.id)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.update", + description: `Updated ${existingBinding.id}`, + resources: [ + { + type: "vercelBinding", + id: existingBinding.id, + meta: { + vercelEnvironment: res.val.created.id, + }, + }, + { + type: "key", + id: keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.update", @@ -460,7 +563,36 @@ export const vercelRouter = t.router({ workspaceId: integration.workspace.id, integrationId: integration.id, }); - await ingestAuditLogs({ + + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.create", + description: `Created ${vercelBindingId} for ${keyId}`, + resources: [ + { + type: "vercelIntegration", + id: integration.id, + }, + { + type: "vercelBinding", + id: vercelBindingId, + meta: { + environment: input.environment, + projectId: input.projectId, + }, + }, + { + type: "key", + id: keyId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.create", @@ -527,7 +659,23 @@ export const vercelRouter = t.router({ .update(schema.vercelBindings) .set({ deletedAt: new Date() }) .where(eq(schema.vercelBindings.id, binding.id)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: binding.vercelIntegrations.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.delete", + description: `Deleted ${binding.id}`, + resources: [ + { + type: "vercelBinding", + id: binding.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: binding.vercelIntegrations.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.delete", @@ -584,7 +732,28 @@ export const vercelRouter = t.router({ .update(schema.vercelBindings) .set({ deletedAt: new Date() }) .where(eq(schema.vercelBindings.id, binding.id)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: integration.workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "vercelBinding.delete", + description: `Deleted ${binding.id}`, + resources: [ + { + type: "vercelBinding", + id: binding.id, + meta: { + vercelProjectId: binding.projectId, + vercelEnvironment: binding.environment, + vercelEnvironmentVariableId: binding.vercelEnvId, + }, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: integration.workspace.id, actor: { type: "user", id: ctx.user.id }, event: "vercelBinding.delete", diff --git a/apps/dashboard/lib/trpc/routers/webhook/create.ts b/apps/dashboard/lib/trpc/routers/webhook/create.ts deleted file mode 100644 index da78246de1..0000000000 --- a/apps/dashboard/lib/trpc/routers/webhook/create.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { db, schema } from "@/lib/db"; -import { env } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { AesGCM } from "@unkey/encryption"; -import { sha256 } from "@unkey/hash"; -import { newId } from "@unkey/id"; -import { KeyV1, newKey } from "@unkey/keys"; -import { z } from "zod"; - -export const createWebhook = rateLimitedProcedure(ratelimit.create) - .input( - z.object({ - destination: z.string().url(), - }), - ) - .mutation(async ({ ctx }) => { - const { UNKEY_WORKSPACE_ID, UNKEY_WEBHOOK_KEYS_API_ID } = env(); - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create a webhook. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - - const { - key: _key, - hash, - start, - } = await newKey({ - prefix: "whsec", - byteLength: 16, - }); - const api = await db.query.apis - .findFirst({ - where: (table, { eq }) => eq(table.id, UNKEY_WEBHOOK_KEYS_API_ID), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create a webhook. Please contact support using support@unkey.dev", - }); - }); - if (!api?.keyAuthId) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Key space for webhooks is not configured", - }); - } - - const webhookId = newId("webhook"); - - const keyId = newId("key"); - await db - .insert(schema.keys) - .values({ - id: keyId, - keyAuthId: api.keyAuthId, - hash, - start, - meta: JSON.stringify({ - webhookId, - }), - workspaceId: UNKEY_WORKSPACE_ID, - createdAt: new Date(), - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create webhook. Please contact support using support@unkey.dev", - }); - }); - - const permissionId = newId("permission"); - await db - .insert(schema.permissions) - .values({ - id: permissionId, - name: `webhook.${webhookId}.verify`, - workspaceId: UNKEY_WORKSPACE_ID, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create webhook. Please contact support using support@unkey.dev", - }); - }); - await db - .insert(schema.keysPermissions) - .values({ - keyId, - permissionId, - workspaceId: UNKEY_WORKSPACE_ID, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to create webhook. Please contact support using support@unkey.dev", - }); - }); - - await ingestAuditLogs({ - workspaceId: UNKEY_WORKSPACE_ID, - actor: { type: "user", id: ctx.user.id }, - event: "key.create", - description: `Created ${keyId}`, - resources: [ - { type: "webhook", id: webhookId }, - { - type: "key", - id: keyId, - }, - { - type: "keyAuth", - id: api.keyAuthId, - }, - { - type: "api", - id: api.id, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - // const vault = connectVault(); - // const encrypted = await vault.encrypt({ - // keyring: ws.id, - // data: key, - // }); - // await db.insert(schema.webhooks).values({ - // id: webhookId, - // workspaceId: ws.id, - // destination: input.destination, - // encrypted: encrypted.encrypted, - // encryptionKeyId: encrypted.keyId, - // }); - - await ingestAuditLogs({ - workspaceId: UNKEY_WORKSPACE_ID, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "webhook.create", - description: `Created ${webhookId}`, - resources: [ - { - type: "webhook", - id: webhookId, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return { - id: webhookId, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/webhook/delete.ts b/apps/dashboard/lib/trpc/routers/webhook/delete.ts deleted file mode 100644 index e0b00eb4bb..0000000000 --- a/apps/dashboard/lib/trpc/routers/webhook/delete.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; - -export const deleteWebhook = rateLimitedProcedure(ratelimit.delete) - .input( - z.object({ - webhookId: z.string(), - }), - ) - .mutation(async ({ input, ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - with: { - webhooks: { - where: (table, { eq }) => eq(table.id, input.webhookId), - }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to delete webhook. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - - if (ws.webhooks.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct webhook. Please contact support using support@unkey.dev.", - }); - } - - await db - .delete(schema.webhooks) - .where(eq(schema.webhooks.id, input.webhookId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to delete the webhook. Please contact support using support@unkey.dev", - }); - }); - - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { type: "user", id: ctx.user.id }, - event: "webhook.delete", - description: `Deleted ${input.webhookId}`, - resources: [{ type: "webhook", id: input.webhookId }], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return; - }); diff --git a/apps/dashboard/lib/trpc/routers/webhook/toggle.ts b/apps/dashboard/lib/trpc/routers/webhook/toggle.ts deleted file mode 100644 index edfd1403da..0000000000 --- a/apps/dashboard/lib/trpc/routers/webhook/toggle.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; -import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; - -export const toggleWebhook = rateLimitedProcedure(ratelimit.update) - .input( - z.object({ - webhookId: z.string(), - enabled: z.boolean(), - }), - ) - .mutation(async ({ input, ctx }) => { - const ws = await db.query.workspaces - .findFirst({ - where: (table, { and, eq, isNull }) => - and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), - with: { - webhooks: { - where: (table, { eq }) => eq(table.id, input.webhookId), - }, - }, - }) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update webhook. Please contact support using support@unkey.dev", - }); - }); - if (!ws) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", - }); - } - - if (ws.webhooks.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: - "We are unable to find the correct webhook. Please contact support using support@unkey.dev.", - }); - } - - await db - .update(schema.webhooks) - .set({ - enabled: input.enabled, - }) - .where(eq(schema.webhooks.id, input.webhookId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to update the webhook. Please contact support using support@unkey.dev", - }); - }); - - await ingestAuditLogs({ - workspaceId: ws.id, - actor: { type: "user", id: ctx.user.id }, - event: "webhook.update", - description: `${input.enabled ? "Enabled" : "Disabled"} ${input.webhookId}`, - resources: [{ type: "webhook", id: input.webhookId }], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, - }); - - return { - enabled: input.enabled, - }; - }); diff --git a/apps/dashboard/lib/trpc/routers/workspace/changeName.ts b/apps/dashboard/lib/trpc/routers/workspace/changeName.ts index 1820a4f277..a980db2607 100644 --- a/apps/dashboard/lib/trpc/routers/workspace/changeName.ts +++ b/apps/dashboard/lib/trpc/routers/workspace/changeName.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { clerkClient } from "@clerk/nextjs"; import { TRPCError } from "@trpc/server"; @@ -42,7 +43,23 @@ export const changeWorkspaceName = rateLimitedProcedure(ratelimit.update) "We are unable to update the workspace name. Please contact support using support@unkey.dev", }); }); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: ws.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.update", + description: `Changed name from ${ws.name} to ${input.name}`, + resources: [ + { + type: "workspace", + id: ws.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: ws.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.update", diff --git a/apps/dashboard/lib/trpc/routers/workspace/changePlan.ts b/apps/dashboard/lib/trpc/routers/workspace/changePlan.ts index 1182aa15fd..55f3f26989 100644 --- a/apps/dashboard/lib/trpc/routers/workspace/changePlan.ts +++ b/apps/dashboard/lib/trpc/routers/workspace/changePlan.ts @@ -1,6 +1,7 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; import { stripeEnv } from "@/lib/env"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { defaultProSubscriptions } from "@unkey/billing"; @@ -75,15 +76,25 @@ export const changeWorkspacePlan = rateLimitedProcedure(ratelimit.update) .set({ planDowngradeRequest: null, }) - .where(eq(schema.workspaces.id, input.workspaceId)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We are unable to change the plan on your workspace. Please contact support using support@unkey.dev", - }); - }); - await ingestAuditLogs({ + .where(eq(schema.workspaces.id, input.workspaceId)); + + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.update", + description: "Removed downgrade request", + resources: [ + { + type: "workspace", + id: workspace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.update", @@ -100,7 +111,8 @@ export const changeWorkspacePlan = rateLimitedProcedure(ratelimit.update) }, }); }) - .catch((_err) => { + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to change your plan. Please contact support using support@unkey.dev", @@ -126,7 +138,23 @@ export const changeWorkspacePlan = rateLimitedProcedure(ratelimit.update) planDowngradeRequest: "free", }) .where(eq(schema.workspaces.id, input.workspaceId)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.update", + description: "Requested downgrade to 'free'", + resources: [ + { + type: "workspace", + id: workspace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.update", @@ -175,7 +203,23 @@ export const changeWorkspacePlan = rateLimitedProcedure(ratelimit.update) planDowngradeRequest: null, }) .where(eq(schema.workspaces.id, input.workspaceId)); - await ingestAuditLogs({ + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.update", + description: "Changed plan to 'pro'", + resources: [ + { + type: "workspace", + id: workspace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.update", diff --git a/apps/dashboard/lib/trpc/routers/workspace/create.ts b/apps/dashboard/lib/trpc/routers/workspace/create.ts index f12562fede..d90b2908e9 100644 --- a/apps/dashboard/lib/trpc/routers/workspace/create.ts +++ b/apps/dashboard/lib/trpc/routers/workspace/create.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { type Workspace, db, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { clerkClient } from "@clerk/nextjs"; import { TRPCError } from "@trpc/server"; @@ -50,8 +51,51 @@ export const createWorkspace = rateLimitedProcedure(ratelimit.create) deleteProtection: true, }; await db - .insert(schema.workspaces) - .values(workspace) + .transaction(async (tx) => { + await tx.insert(schema.workspaces).values(workspace); + + const auditLogBucketId = newId("auditLogBucket"); + await tx.insert(schema.auditLogBucket).values({ + id: auditLogBucketId, + workspaceId: workspace.id, + name: "unkey_mutations", + deleteProtection: true, + }); + await insertAuditLogs(tx, [ + { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.create", + description: `Created ${workspace.id}`, + resources: [ + { + type: "workspace", + id: workspace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }, + { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "auditLogBucket.create", + description: `Created ${auditLogBucketId}`, + resources: [ + { + type: "auditLogBucket", + id: auditLogBucketId, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }, + ]); + }) .catch((_err) => { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -59,7 +103,7 @@ export const createWorkspace = rateLimitedProcedure(ratelimit.create) "We are unable to create the workspace. Please contact support using support@unkey.dev", }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.create", diff --git a/apps/dashboard/lib/trpc/routers/workspace/optIntoBeta.ts b/apps/dashboard/lib/trpc/routers/workspace/optIntoBeta.ts index 74c7333bc5..6c79828814 100644 --- a/apps/dashboard/lib/trpc/routers/workspace/optIntoBeta.ts +++ b/apps/dashboard/lib/trpc/routers/workspace/optIntoBeta.ts @@ -1,5 +1,6 @@ +import { insertAuditLogs } from "@/lib/audit"; import { db, eq, schema } from "@/lib/db"; -import { ingestAuditLogs } from "@/lib/tinybird"; +import { ingestAuditLogsTinybird } from "@/lib/tinybird"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; @@ -46,18 +47,38 @@ export const optWorkspaceIntoBeta = rateLimitedProcedure(ratelimit.update) } } await db - .update(schema.workspaces) - .set({ - betaFeatures: workspace.betaFeatures, + .transaction(async (tx) => { + await tx + .update(schema.workspaces) + .set({ + betaFeatures: workspace.betaFeatures, + }) + .where(eq(schema.workspaces.id, workspace.id)); + await insertAuditLogs(tx, { + workspaceId: workspace.id, + actor: { type: "user", id: ctx.user.id }, + event: "workspace.opt_in", + description: `Opted ${workspace.id} into beta: ${input.feature}`, + resources: [ + { + type: "workspace", + id: workspace.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); }) - .where(eq(schema.workspaces.id, workspace.id)) - .catch((_err) => { + .catch((err) => { + console.error(err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update workspace, please contact support using support@unkey.dev.", }); }); - await ingestAuditLogs({ + await ingestAuditLogsTinybird({ workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "workspace.opt_in", diff --git a/apps/semantic-cache/src/pkg/db.ts b/apps/semantic-cache/src/pkg/db.ts index 910196c076..534f02f0c8 100644 --- a/apps/semantic-cache/src/pkg/db.ts +++ b/apps/semantic-cache/src/pkg/db.ts @@ -1,7 +1,6 @@ import { Client } from "@planetscale/database"; -import { type PlanetScaleDatabase, drizzle, schema } from "@unkey/db"; +import { type Database, drizzle, schema } from "@unkey/db"; import type { Env } from "./env"; -export type Database = PlanetScaleDatabase; export function createConnection( env: Pick, diff --git a/clickhouse b/clickhouse deleted file mode 100755 index 9ceaa12e5e..0000000000 Binary files a/clickhouse and /dev/null differ diff --git a/deployment/docker-compose.yaml b/deployment/docker-compose.yaml index 40656f9a90..9b2bd55eaf 100644 --- a/deployment/docker-compose.yaml +++ b/deployment/docker-compose.yaml @@ -28,7 +28,6 @@ services: ] depends_on: - mysql - - s3 ports: - 3900:3900 diff --git a/internal/db/package.json b/internal/db/package.json index d6437e85bc..7e2a6b4758 100644 --- a/internal/db/package.json +++ b/internal/db/package.json @@ -23,6 +23,7 @@ "dependencies": { "@planetscale/database": "^1.16.0", "@unkey/billing": "workspace:^", + "@unkey/id": "workspace:^", "drizzle-orm": "^0.31.2" } } diff --git a/internal/db/src/index.ts b/internal/db/src/index.ts index 5db8acac7b..1152a94de2 100644 --- a/internal/db/src/index.ts +++ b/internal/db/src/index.ts @@ -1,9 +1,18 @@ export * from "./types"; +import type { ExtractTablesWithRelations } from "drizzle-orm"; +import type { + PlanetScaleDatabase, + PlanetScaleTransaction, +} from "drizzle-orm/planetscale-serverless"; import * as schema from "./schema"; export { schema }; export * from "drizzle-orm"; -export { - drizzle, - type PlanetScaleDatabase, -} from "drizzle-orm/planetscale-serverless"; +export { drizzle } from "drizzle-orm/planetscale-serverless"; export { drizzle as mysqlDrizzle } from "drizzle-orm/mysql2"; + +export type Database = PlanetScaleDatabase; + +export type Transaction = PlanetScaleTransaction< + typeof schema, + ExtractTablesWithRelations +>; diff --git a/internal/db/src/schema/audit_logs.ts b/internal/db/src/schema/audit_logs.ts new file mode 100644 index 0000000000..5ed62da666 --- /dev/null +++ b/internal/db/src/schema/audit_logs.ts @@ -0,0 +1,126 @@ +import { relations } from "drizzle-orm"; +import { + bigint, + index, + int, + json, + mysqlTable, + primaryKey, + uniqueIndex, + varchar, +} from "drizzle-orm/mysql-core"; +import { deleteProtection } from "./util/delete_protection"; +import { lifecycleDates } from "./util/lifecycle_dates"; +import { workspaces } from "./workspaces"; + +import { newId } from "@unkey/id"; +export const auditLogBucket = mysqlTable( + "audit_log_bucket", + { + id: varchar("id", { length: 256 }) + .primaryKey() + .$defaultFn(() => newId("auditLogBucket")), + workspaceId: varchar("workspace_id", { length: 256 }).notNull(), + /** + * Buckets are used as namespaces for different logs belonging to a single workspace + */ + name: varchar("name", { length: 256 }).notNull(), + /** + * null means we don't automatically remove logs + */ + retentionDays: int("retention_days"), + ...lifecycleDates, + ...deleteProtection, + }, + (table) => ({ + uniqueNamePerWorkspace: uniqueIndex("unique_name_per_workspace_idx").on( + table.workspaceId, + table.name, + ), + }), +); + +export const auditLog = mysqlTable( + "audit_log", + { + id: varchar("id", { length: 256 }) + .primaryKey() + .$defaultFn(() => newId("auditLog")), + + workspaceId: varchar("workspace_id", { length: 256 }).notNull(), + + bucketId: varchar("bucket_id", { length: 256 }).notNull(), + event: varchar("event", { length: 256 }).notNull(), + + // When the event happened + time: bigint("time", { mode: "number" }) + .notNull() + .$defaultFn(() => Date.now()), + // A human readable description of the event + display: varchar("display", { length: 256 }).notNull(), + + remoteIp: varchar("remote_ip", { length: 256 }), + userAgent: varchar("user_agent", { length: 256 }), + actorType: varchar("actor_type", { length: 256 }).notNull(), + actorId: varchar("actor_id", { length: 256 }).notNull(), + actorName: varchar("actor_name", { length: 256 }), + actorMeta: json("actor_meta"), + + ...lifecycleDates, + }, + (table) => ({ + workspaceId: index("workspace_id_idx").on(table.workspaceId), + }), +); + +export const auditLogRelations = relations(auditLog, ({ one, many }) => ({ + workspace: one(workspaces, { + fields: [auditLog.workspaceId], + references: [workspaces.id], + }), + bucket: one(auditLogBucket, { + fields: [auditLog.bucketId], + references: [auditLogBucket.id], + }), + targets: many(auditLogTarget), +})); + +export const auditLogTarget = mysqlTable( + "audit_log_target", + { + workspaceId: varchar("workspace_id", { length: 256 }).notNull(), + bucketId: varchar("bucket_id", { length: 256 }).notNull(), + auditLogId: varchar("audit_log_id", { length: 256 }).notNull(), + + // A human readable name to display in the UI + displayName: varchar("display_name", { length: 256 }).notNull(), + // the type of the target + type: varchar("type", { length: 256 }).notNull(), + // the id of the target + id: varchar("id", { length: 256 }).notNull(), + // the name of the target + name: varchar("name", { length: 256 }), + // the metadata of the target + meta: json("meta"), + + ...lifecycleDates, + }, + (table) => ({ + pk: primaryKey({ columns: [table.auditLogId, table.id] }), + }), +); + +export const auditLogTargetRelations = relations(auditLogTarget, ({ one }) => ({ + workspace: one(workspaces, { + fields: [auditLogTarget.workspaceId], + references: [workspaces.id], + }), + bucket: one(auditLogBucket, { + fields: [auditLogTarget.bucketId], + references: [auditLogBucket.id], + }), + log: one(auditLog, { + fields: [auditLogTarget.auditLogId], + references: [auditLog.id], + }), +})); diff --git a/internal/db/src/schema/index.ts b/internal/db/src/schema/index.ts index e79e1e28e8..7e4e778ad6 100644 --- a/internal/db/src/schema/index.ts +++ b/internal/db/src/schema/index.ts @@ -13,3 +13,4 @@ export * from "./llm-gateway"; export * from "./key_migrations"; export * from "./llm-gateway"; export * from "./identity"; +export * from "./audit_logs"; diff --git a/internal/db/src/schema/workspaces.ts b/internal/db/src/schema/workspaces.ts index d993839b2d..95e483d101 100644 --- a/internal/db/src/schema/workspaces.ts +++ b/internal/db/src/schema/workspaces.ts @@ -10,6 +10,7 @@ import { varchar, } from "drizzle-orm/mysql-core"; import { apis } from "./apis"; +import { auditLogBucket } from "./audit_logs"; import { gateways } from "./gateway"; import { identities } from "./identity"; import { keyAuth } from "./keyAuth"; @@ -132,4 +133,5 @@ export const workspacesRelations = relations(workspaces, ({ many }) => ({ verificationMonitors: many(verificationMonitors), keySpaces: many(keyAuth), identities: many(identities), + auditLogBuckets: many(auditLogBucket), })); diff --git a/internal/id/src/generate.ts b/internal/id/src/generate.ts index c79764b44b..f159c19e23 100644 --- a/internal/id/src/generate.ts +++ b/internal/id/src/generate.ts @@ -12,7 +12,6 @@ const prefixes = { vercelBinding: "vb", role: "role", test: "test", // for tests only - auditLog: "log", ratelimitNamespace: "rlns", ratelimitOverride: "rlor", permission: "perm", @@ -26,6 +25,8 @@ const prefixes = { webhookDelivery: "whd", identity: "id", ratelimit: "rl", + auditLogBucket: "buk", + auditLog: "log", } as const; export function newId(prefix: TPrefix) { diff --git a/internal/schema/src/auditlog.ts b/internal/schema/src/auditlog.ts index d081cf51ba..c9a52ad9cf 100644 --- a/internal/schema/src/auditlog.ts +++ b/internal/schema/src/auditlog.ts @@ -51,6 +51,7 @@ export const unkeyAuditLogEvents = z.enum([ "ratelimit.create", "ratelimit.update", "ratelimit.delete", + "auditLogBucket.create", ]); export const auditLogSchemaV1 = z.object({ @@ -66,7 +67,7 @@ export const auditLogSchemaV1 = z.object({ auditLogId: z.string(), event: z.string(), description: z.string().optional(), - time: z.number(), + time: z.number().default(() => Date.now()), meta: z .record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])) .optional(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fde27c54a6..7bc78a8c4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1147,6 +1147,9 @@ importers: '@unkey/billing': specifier: workspace:^ version: link:../billing + '@unkey/id': + specifier: workspace:^ + version: link:../id drizzle-orm: specifier: ^0.31.2 version: 0.31.2(@planetscale/database@1.18.0)(mysql2@3.10.3)(react@18.3.1) diff --git a/tools/local/src/db.ts b/tools/local/src/db.ts index b8adaae709..1e2341bae1 100644 --- a/tools/local/src/db.ts +++ b/tools/local/src/db.ts @@ -1,6 +1,7 @@ import { exec } from "node:child_process"; import path from "node:path"; import { mysqlDrizzle, schema } from "@unkey/db"; +import { newId } from "@unkey/id"; import mysql from "mysql2/promise"; import { task } from "./util"; @@ -57,35 +58,16 @@ export async function prepareDatabase(url?: string): Promise<{ s.message("Created root workspace"); - // root key space - await db - .insert(schema.keyAuth) + .insert(schema.auditLogBucket) .values({ - id: ROW_IDS.webhookKeySpace, + id: newId("auditLogBucket"), workspaceId: ROW_IDS.rootWorkspace, - createdAt: new Date(), + name: "unkey_mutations", + deleteProtection: true, }) - .onDuplicateKeyUpdate({ set: { createdAt: new Date() } }); - s.message("Created webhook key space"); - - /** - * Set up an api for webhook keys - */ - await db - .insert(schema.apis) - .values({ - id: ROW_IDS.webhookApi, - name: "Unkey Webhooks", - workspaceId: ROW_IDS.rootWorkspace, - authType: "key", - keyAuthId: ROW_IDS.webhookKeySpace, - createdAt: new Date(), - deletedAt: null, - ipWhitelist: null, - }) - .onDuplicateKeyUpdate({ set: { createdAt: new Date() } }); - s.message("Created webhook api"); + .onDuplicateKeyUpdate({ set: { createdAt: Date.now() } }); + s.message("Created audit log bucket"); await db .insert(schema.keyAuth) diff --git a/tools/migrate/auditlog-import.ts b/tools/migrate/auditlog-import.ts new file mode 100644 index 0000000000..c37e187410 --- /dev/null +++ b/tools/migrate/auditlog-import.ts @@ -0,0 +1,105 @@ +import { and, asc, eq, gt, isNotNull, mysqlDrizzle, schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import mysql from "mysql2/promise"; + +async function main() { + const conn = await mysql.createConnection( + `mysql://${process.env.DATABASE_USERNAME}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:3306/unkey`, + ); + + await conn.ping(); + const db = mysqlDrizzle(conn, { schema, mode: "default" }); + + const decoder = new TextDecoder(); + let buffer = ""; + + const exportFile = Bun.file("./export_audit_log.json"); + + const stream = exportFile.stream(); + for await (const chunk of stream) { + buffer += decoder.decode(chunk); + + const lines = buffer.split("\n"); + while (lines.length > 1) { + const line = lines.shift(); + const log = JSON.parse(line!) as { + workspaceId: string; + bucket: string; + auditLogId: string; + event: string; + time: number; + actorType: string; + actorId: string; + actorName: string | null; + actorMeta: string | null; + description: string; + resources: string; + userAgent: string; + location: string; + meta: string | null; + }; + + console.log(log); + let bucketId = ""; + const bucket = await db.query.auditLogBucket.findFirst({ + where: (table, { eq, and }) => + and(eq(table.workspaceId, log.workspaceId), eq(table.name, "unkey_mutations")), + }); + if (bucket) { + bucketId = bucket.id; + } else { + bucketId = newId("auditLogBucket"); + await db.insert(schema.auditLogBucket).values({ + id: bucketId, + workspaceId: log.workspaceId, + name: "unkey_mutations", + }); + } + + const auditLogId = newId("auditLog"); + await db + .insert(schema.auditLog) + .values({ + id: auditLogId, + workspaceId: log.workspaceId, + bucketId, + event: log.event, + time: log.time, + // A human readable description of the event + display: log.description, + remoteIp: log.location, + userAgent: log.userAgent, + actorType: log.actorType, + actorId: log.actorId, + actorName: log.actorName, + actorMeta: log.actorMeta ? JSON.parse(log.actorMeta) : null, + }) + .onDuplicateKeyUpdate({ set: { updatedAt: Date.now() } }); + + const resources: Array<{ + type: string; + id: string; + name: string | null; + meta: unknown | null; + }> = log.resources ? JSON.parse(log.resources) : []; + + await db.insert(schema.auditLogTarget).values( + resources.map((r) => ({ + workspaceId: log.workspaceId, + bucketId, + auditLogId, + displayName: r.name ?? "", + type: r.type, + id: r.id, + name: r.name, + meta: r.meta, + })), + ); + } + buffer = lines[0]; + } + + console.log("END"); +} + +main(); diff --git a/tools/migrate/tinybird-export.ts b/tools/migrate/tinybird-export.ts new file mode 100644 index 0000000000..248cf2e276 --- /dev/null +++ b/tools/migrate/tinybird-export.ts @@ -0,0 +1,26 @@ +async function main() { + const exportFile = Bun.file("./export_audit_log.json"); + const writer = exportFile.writer(); + let cursor = ""; + do { + console.log({ cursor }); + const res = await fetch( + `https://api.tinybird.co/v0/pipes/export_audit_log.json?cursor=${cursor}`, + { + headers: { + Authorization: `Bearer ${process.env.TINYBIRD_TOKEN}`, + }, + }, + ); + const body = (await res.json()) as { data: { auditLogId: string }[] }; + console.log("found", body.data.length); + for (const row of body.data) { + writer.write(JSON.stringify(row)); + writer.write("\n"); + } + + cursor = body.data.at(-1)?.auditLogId ?? ""; + } while (cursor); +} + +main();