diff --git a/src/api/context.ts b/src/api/context.ts index 0f398d2..0df44c1 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -1,5 +1,6 @@ import { ORPCError } from "@orpc/server"; import { os } from "@orpc/server"; +import { WebClient } from "@slack/web-api"; import type { Store } from "../store/store"; import type { ApiKeyIdentity } from "../lib/api-keys"; @@ -18,6 +19,8 @@ export interface ORPCContext { session: SessionIdentity | null; apiKey: ApiKeyIdentity | null; databaseUrl: string; + slackToken: string; + lockdownUsers: string[]; } const base = os.$context(); @@ -58,3 +61,18 @@ export const authOrApiKeyProcedure = base.use(async ({ context, next }) => { } return next(); }); + +export async function isChannelManager(channelId: string, userId: string, slackToken: string): Promise { + if (!slackToken) return false; + const slack = new WebClient(slackToken); + try { + const conv = await slack.conversations.info({ channel: channelId }); + if ((conv.channel as any)?.creator === userId) return true; + } catch {} + try { + const user = await slack.users.info({ user: userId }); + const u = user.user as any; + if (u?.is_admin || u?.is_owner || u?.is_primary_owner) return true; + } catch {} + return false; +} diff --git a/src/api/routers/subscriptions.ts b/src/api/routers/subscriptions.ts index c532168..c3e86a9 100644 --- a/src/api/routers/subscriptions.ts +++ b/src/api/routers/subscriptions.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { ORPCError } from "@orpc/server"; -import { publicProcedure, authOrApiKeyProcedure, authRequiredProcedure } from "../context"; +import { publicProcedure, authRequiredProcedure, isChannelManager } from "../context"; export const listSubscriptions = publicProcedure .route({ method: "GET", path: "/subscriptions" }) @@ -29,6 +29,13 @@ export const addSubscription = authRequiredProcedure if (!source) throw new ORPCError("NOT_FOUND", { message: `Source channel ${input.sourceChannelId} not found` }); const sub = await context.store.getChannel(input.subscriberChannelId); if (!sub) throw new ORPCError("NOT_FOUND", { message: `Subscriber channel ${input.subscriberChannelId} not found` }); + + const slackId = context.session?.user?.slackId; + if (!slackId) throw new ORPCError("FORBIDDEN", { message: "No Slack ID on session" }); + const isLockdown = context.lockdownUsers.includes(slackId); + const isManager = isLockdown || await isChannelManager(input.subscriberChannelId, slackId, context.slackToken); + if (!isManager) throw new ORPCError("FORBIDDEN", { message: "You must be a channel manager or lockdown user to add subscriptions" }); + await context.store.addSubscription(input.subscriberChannelId, input.sourceChannelId); }); @@ -41,6 +48,12 @@ export const removeSubscription = authRequiredProcedure }), ) .handler(async ({ input, context }) => { + const slackId = context.session?.user?.slackId; + if (!slackId) throw new ORPCError("FORBIDDEN", { message: "No Slack ID on session" }); + const isLockdown = context.lockdownUsers.includes(slackId); + const isManager = isLockdown || await isChannelManager(input.subscriberChannelId, slackId, context.slackToken); + if (!isManager) throw new ORPCError("FORBIDDEN", { message: "You must be a channel manager or lockdown user to remove subscriptions" }); + await context.store.removeSubscription(input.subscriberChannelId, input.sourceChannelId); }); diff --git a/src/app.ts b/src/app.ts index 562ef55..6683dc4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -474,7 +474,14 @@ app.use("/api/*", async (c, next) => { } catch {} } - const ctx: ORPCContext = { store, session, apiKey, databaseUrl: dbUrl }; + const ctx: ORPCContext = { + store, + session, + apiKey, + databaseUrl: dbUrl, + slackToken: c.env.SLACK_BOT_TOKEN || "", + lockdownUsers: (c.env.LOCKDOWN_USERS || "").split(",").map((s: string) => s.trim()).filter(Boolean), + }; const { matched: openMatched, response: openResponse } = await openAPIHandler.handle(c.req.raw, { prefix: "/api",