Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136import { ORPCError } from "@orpc/server";import { z } from "zod";import { authRequiredProcedure, isChannelManager } from "../context";import { generateApiKey } from "../../lib/api-keys";import { apiKeys, apiKeyChannels, authUser } from "../../db/schema";import { eq } from "drizzle-orm";import { aliasedTable } from "drizzle-orm/alias";import { getChannel as dbGetChannel } from "../../db/queries";import type { Channel } from "../../db/queries";
export function canCreateApiKeyForChannel( channel: Pick<Channel, "accessPermUsers">, slackId: string, options: { isLockdown: boolean; isManager: boolean },): boolean { return ( options.isLockdown || options.isManager || channel.accessPermUsers.includes("*") || channel.accessPermUsers.includes(slackId) );}
export const listApiKeys = authRequiredProcedure .route({ method: "GET", path: "/api-keys" }) .handler(async ({ context }) => { const db = context.db; const userId = context.session?.user?.id; if (!userId) return [];
const revokedByUser = aliasedTable(authUser, "revokedByUser"); const keys = await db .select({ id: apiKeys.id, name: apiKeys.name, keyPrefix: apiKeys.keyPrefix, createdAt: apiKeys.createdAt, lastUsedAt: apiKeys.lastUsedAt, revokedBy: revokedByUser.slackId, }) .from(apiKeys) .leftJoin(revokedByUser, eq(apiKeys.revokedBy, revokedByUser.id)) .where(eq(apiKeys.owner, userId));
const keysWithChannels = await Promise.all( keys.map(async (key) => { const channels = await db .select({ channelId: apiKeyChannels.channelId }) .from(apiKeyChannels) .where(eq(apiKeyChannels.keyId, key.id)); return { ...key, channels: channels.map((c) => c.channelId) }; }), );
return keysWithChannels; });
export const createApiKey = authRequiredProcedure .route({ method: "POST", path: "/api-keys" }) .input( z.object({ name: z.string().min(1), channelIds: z.array(z.string()).optional(), }), ) .handler(async ({ input, context }) => { const slackId = context.session?.user?.slackId; if (!slackId) throw new ORPCError("FORBIDDEN", { message: "No Slack ID on session" });
const db = context.db;
// Validate user has access to each specified channel if (input.channelIds && input.channelIds.length > 0) { const isLockdown = context.lockdownUsers.includes(slackId); for (const channelId of input.channelIds) { const ch = await dbGetChannel(db, channelId); if (!ch) throw new ORPCError("NOT_FOUND", { message: `Channel ${channelId} not found`, }); const isManager = isLockdown ? false : await isChannelManager(channelId, slackId, context.slackToken); if (!canCreateApiKeyForChannel(ch, slackId, { isLockdown, isManager })) { throw new ORPCError("FORBIDDEN", { message: `You don't have access to channel ${ch.name || channelId}`, }); } } }
const { fullKey, keyPrefix, secretHash } = generateApiKey(); const hash = await secretHash;
const [keyRow] = await db .insert(apiKeys) .values({ name: input.name, owner: context.session?.user?.id || null, keyPrefix, secretHash: hash, }) .returning({ id: apiKeys.id });
if (input.channelIds && keyRow) { for (const channelId of input.channelIds) { await db.insert(apiKeyChannels).values({ keyId: keyRow.id, channelId, }); } }
return { fullKey, keyPrefix, name: input.name, id: keyRow?.id }; });
export const revokeApiKey = authRequiredProcedure .route({ method: "DELETE", path: "/api-keys/{id}" }) .input(z.object({ id: z.coerce.number().int() })) .handler(async ({ input, context }) => { const db = context.db; await db.delete(apiKeyChannels).where(eq(apiKeyChannels.keyId, input.id)); await db .update(apiKeys) .set({ revokedBy: context.session?.user?.id || null }) .where(eq(apiKeys.id, input.id)); return { ok: true }; });
export const apiKeyRouter = { list: listApiKeys, create: createApiKey, revoke: revokeApiKey,};