Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions apps/dashboard/lib/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,30 @@ export async function insertAuditLogs(
const logs = Array.isArray(logOrLogs) ? logOrLogs : [logOrLogs];

if (logs.length === 0) {
console.info("No audit logs to insert");
return Promise.resolve();
}

console.info({
message: "Inserting audit logs",
count: logs.length,
events: logs.map((log) => log.event),
workspaceIds: [...new Set(logs.map((log) => log.workspaceId))],
});

for (const log of logs) {
const auditLogId = newId("auditLog");

console.info({
message: "Inserting audit log entry",
auditLogId,
workspaceId: log.workspaceId,
event: log.event,
actorType: log.actor.type,
actorId: log.actor.id,
resourceCount: log.resources.length,
});

await db.insert(schema.auditLog).values({
id: auditLogId,
workspaceId: log.workspaceId,
Expand All @@ -122,7 +141,15 @@ export async function insertAuditLogs(
actorName: log.actor.name,
actorMeta: log.actor.meta,
});

if (log.resources.length > 0) {
console.info({
message: "Inserting audit log resources",
auditLogId,
resourceCount: log.resources.length,
resourceTypes: log.resources.map((r) => r.type),
});

await db.insert(schema.auditLogTarget).values(
log.resources.map((r) => ({
workspaceId: log.workspaceId,
Expand All @@ -138,4 +165,9 @@ export async function insertAuditLogs(
);
}
}

console.info({
message: "Successfully inserted all audit logs",
count: logs.length,
});
}
208 changes: 153 additions & 55 deletions apps/dashboard/lib/trpc/routers/key/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,78 +9,176 @@ export const deleteKeys = t.procedure
.use(requireWorkspace)
.input(
z.object({
keyIds: z.array(z.string()),
keyIds: z.array(z.string()).min(1, "At least one key ID must be provided"),
}),
)
.mutation(async ({ ctx, input }) => {
const workspace = await db.query.workspaces
.findFirst({
const { keyIds } = input;
const userId = ctx.user.id;
const tenantId = ctx.tenant.id;

console.info({
message: "Attempting to delete keys",
userId,
tenantId,
keyCount: keyIds.length,
keyIds,
});

if (keyIds.length === 0) {
console.warn({
message: "No key IDs provided for deletion",
userId,
tenantId,
});
throw new TRPCError({
code: "BAD_REQUEST",
message: "No keys were provided for deletion",
});
}

try {
const workspace = await db.query.workspaces.findFirst({
where: (table, { and, eq, isNull }) =>
and(eq(table.orgId, ctx.tenant.id), isNull(table.deletedAtM)),
and(eq(table.orgId, tenantId), isNull(table.deletedAtM)),
with: {
keys: {
where: (table, { and, inArray, isNull }) =>
and(isNull(table.deletedAtM), inArray(table.id, input.keyIds)),
and(isNull(table.deletedAtM), inArray(table.id, keyIds)),
columns: {
id: true,
},
},
},
})
.catch((_err) => {
});

if (!workspace) {
console.error({
message: "Workspace not found",
userId,
tenantId,
});
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"We were unable to delete this key. Please try again or contact support@unkey.dev.",
code: "NOT_FOUND",
message: "Workspace not found. Please verify your workspace selection and try again.",
});
});
if (!workspace) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"We are unable to find the correct workspace. Please try again or contact support@unkey.dev.",
});
}
}

await db
.transaction(async (tx) => {
await tx
.update(schema.keys)
.set({ deletedAtM: Date.now() })
.where(
and(
eq(schema.keys.workspaceId, workspace.id),
inArray(
schema.keys.id,
workspace.keys.map((k) => k.id),
),
),
);
insertAuditLogs(
tx,
workspace.keys.map((key) => ({
// Check if keys were found in the workspace
if (workspace.keys.length === 0) {
console.warn({
message: "No keys found for deletion",
userId,
tenantId,
keyIds,
});
throw new TRPCError({
code: "NOT_FOUND",
message: "None of the specified keys could be found in your workspace.",
});
}

try {
await db.transaction(async (tx) => {
console.info({
message: "Starting key deletion transaction",
userId,
tenantId,
workspaceId: workspace.id,
actor: { type: "user", id: ctx.user.id },
event: "key.delete",
description: `Deleted ${key.id}`,
resources: [
{
type: "key",
id: key.id,
keysToDelete: workspace.keys.map((k) => k.id),
});

await tx
.update(schema.keys)
.set({ deletedAtM: Date.now() })
.where(
and(
eq(schema.keys.workspaceId, workspace.id),
inArray(
schema.keys.id,
workspace.keys.map((k) => k.id),
),
),
);

await insertAuditLogs(
tx,
workspace.keys.map((key) => ({
workspaceId: workspace.id,
actor: { type: "user", id: userId },
event: "key.delete",
description: `Deleted key ${key.id}`,
resources: [
{
type: "key",
id: key.id,
},
],
context: {
location: ctx.audit.location,
userAgent: ctx.audit.userAgent,
},
],
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 delete the key. Please try again or contact support@unkey.dev",
})),
);
});
} catch (txErr) {
console.error({
message: "Transaction failed during key deletion",
userId,
tenantId,
workspaceId: workspace.id,
error: txErr instanceof Error ? txErr.message : String(txErr),
stack: txErr instanceof Error ? txErr.stack : undefined,
});

// Check for specific error types
const errorMessage = String(txErr);
if (errorMessage.includes("Forbidden")) {
throw new TRPCError({
code: "FORBIDDEN",
message:
"Permission denied. The system doesn't have sufficient access to complete this operation.",
});
}
if (errorMessage.includes("auditLog")) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Unable to create audit logs. Please contact support@unkey.dev.",
});
}
throw txErr; // Re-throw to be caught by outer catch
}

console.info({
message: "Keys deleted successfully",
userId,
tenantId,
keyCount: workspace.keys.length,
deletedKeyIds: workspace.keys.map((k) => k.id),
});

return {
deletedKeyIds: workspace.keys.map((k) => k.id),
totalDeleted: workspace.keys.length,
};
} catch (err) {
if (err instanceof TRPCError) {
// Re-throw if it's already a TRPC error
throw err;
}

console.error({
message: "Error during key deletion",
userId,
tenantId,
keyIds,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});

throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete keys. Our team has been notified of this issue.",
});
}
});