kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 313 lines 9.1 kB view raw
1import { and, eq } from "drizzle-orm"; 2import { Hono } from "hono"; 3import { HTTPException } from "hono/http-exception"; 4import { describeRoute, resolver, validator } from "hono-openapi"; 5import * as v from "valibot"; 6import db from "../database"; 7import { integrationTable } from "../database/schema"; 8import { 9 defaultGenericWebhookEvents, 10 type GenericWebhookConfig, 11 normalizeGenericWebhookConfig, 12 validateGenericWebhookConfig, 13} from "../plugins/generic-webhook/config"; 14import { genericWebhookIntegrationSchema } from "../schemas"; 15import { workspaceAccess } from "../utils/workspace-access-middleware"; 16 17const genericWebhookIntegration = new Hono<{ 18 Variables: { 19 userId: string; 20 workspaceId: string; 21 apiKey?: { 22 id: string; 23 userId: string; 24 enabled: boolean; 25 }; 26 }; 27}>(); 28 29function maskValue(value: string | undefined): string | null { 30 if (!value) return null; 31 return value.length > 8 ? `${value.slice(0, 4)}${value.slice(-4)}` : "••••"; 32} 33 34function toResponse(integration: { 35 id: string; 36 projectId: string; 37 config: string; 38 isActive: boolean | null; 39 createdAt: Date; 40 updatedAt: Date; 41}) { 42 const config = normalizeGenericWebhookConfig( 43 JSON.parse(integration.config) as GenericWebhookConfig, 44 ); 45 46 return { 47 id: integration.id, 48 projectId: integration.projectId, 49 webhookConfigured: Boolean(config.webhookUrl), 50 maskedWebhookUrl: maskValue(config.webhookUrl), 51 secretConfigured: Boolean(config.secret), 52 maskedSecret: maskValue(config.secret), 53 events: { 54 ...defaultGenericWebhookEvents, 55 ...(config.events ?? {}), 56 }, 57 isActive: integration.isActive, 58 createdAt: integration.createdAt, 59 updatedAt: integration.updatedAt, 60 }; 61} 62 63async function getGenericWebhookIntegration(projectId: string) { 64 const integration = await db.query.integrationTable.findFirst({ 65 where: and( 66 eq(integrationTable.projectId, projectId), 67 eq(integrationTable.type, "generic-webhook"), 68 ), 69 }); 70 71 if (!integration) { 72 return null; 73 } 74 75 return toResponse(integration); 76} 77 78const genericWebhookEventsSchema = v.object({ 79 taskCreated: v.optional(v.boolean()), 80 taskStatusChanged: v.optional(v.boolean()), 81 taskPriorityChanged: v.optional(v.boolean()), 82 taskTitleChanged: v.optional(v.boolean()), 83 taskDescriptionChanged: v.optional(v.boolean()), 84 taskCommentCreated: v.optional(v.boolean()), 85}); 86 87const nullableGenericWebhookIntegrationSchema = v.nullable( 88 genericWebhookIntegrationSchema, 89); 90 91genericWebhookIntegration 92 .get( 93 "/project/:projectId", 94 describeRoute({ 95 operationId: "getGenericWebhookIntegration", 96 tags: ["Generic Webhook"], 97 description: "Get generic outgoing webhook integration for a project", 98 responses: { 99 200: { 100 description: "Generic webhook integration details", 101 content: { 102 "application/json": { 103 schema: resolver(nullableGenericWebhookIntegrationSchema), 104 }, 105 }, 106 }, 107 }, 108 }), 109 validator("param", v.object({ projectId: v.string() })), 110 workspaceAccess.fromProject("projectId"), 111 async (c) => { 112 const { projectId } = c.req.valid("param"); 113 return c.json(await getGenericWebhookIntegration(projectId)); 114 }, 115 ) 116 .post( 117 "/project/:projectId", 118 describeRoute({ 119 operationId: "createGenericWebhookIntegration", 120 tags: ["Generic Webhook"], 121 description: "Create or replace a generic outgoing webhook integration", 122 responses: { 123 200: { 124 description: "Generic webhook integration created successfully", 125 content: { 126 "application/json": { 127 schema: resolver(genericWebhookIntegrationSchema), 128 }, 129 }, 130 }, 131 }, 132 }), 133 validator("param", v.object({ projectId: v.string() })), 134 validator( 135 "json", 136 v.object({ 137 webhookUrl: v.pipe(v.string(), v.minLength(1)), 138 secret: v.optional(v.string()), 139 events: v.optional(genericWebhookEventsSchema), 140 }), 141 ), 142 workspaceAccess.fromProject("projectId"), 143 async (c) => { 144 const { projectId } = c.req.valid("param"); 145 const body = c.req.valid("json"); 146 147 const config = normalizeGenericWebhookConfig({ 148 webhookUrl: body.webhookUrl, 149 secret: body.secret, 150 events: body.events, 151 }); 152 153 const validation = await validateGenericWebhookConfig(config); 154 if (!validation.valid) { 155 throw new HTTPException(400, { 156 message: validation.errors?.join(", ") ?? "Invalid config", 157 }); 158 } 159 160 const existing = await db.query.integrationTable.findFirst({ 161 where: and( 162 eq(integrationTable.projectId, projectId), 163 eq(integrationTable.type, "generic-webhook"), 164 ), 165 }); 166 167 if (existing) { 168 await db 169 .update(integrationTable) 170 .set({ 171 config: JSON.stringify(config), 172 isActive: true, 173 updatedAt: new Date(), 174 }) 175 .where(eq(integrationTable.id, existing.id)); 176 } else { 177 await db.insert(integrationTable).values({ 178 projectId, 179 type: "generic-webhook", 180 config: JSON.stringify(config), 181 isActive: true, 182 }); 183 } 184 185 return c.json(await getGenericWebhookIntegration(projectId)); 186 }, 187 ) 188 .patch( 189 "/project/:projectId", 190 describeRoute({ 191 operationId: "updateGenericWebhookIntegration", 192 tags: ["Generic Webhook"], 193 description: "Update generic outgoing webhook settings", 194 responses: { 195 200: { 196 description: "Generic webhook integration updated successfully", 197 content: { 198 "application/json": { 199 schema: resolver(genericWebhookIntegrationSchema), 200 }, 201 }, 202 }, 203 }, 204 }), 205 validator("param", v.object({ projectId: v.string() })), 206 validator( 207 "json", 208 v.object({ 209 webhookUrl: v.optional(v.string()), 210 secret: v.optional(v.nullable(v.string())), 211 isActive: v.optional(v.boolean()), 212 events: v.optional(genericWebhookEventsSchema), 213 }), 214 ), 215 workspaceAccess.fromProject("projectId"), 216 async (c) => { 217 const { projectId } = c.req.valid("param"); 218 const body = c.req.valid("json"); 219 220 const existing = await db.query.integrationTable.findFirst({ 221 where: and( 222 eq(integrationTable.projectId, projectId), 223 eq(integrationTable.type, "generic-webhook"), 224 ), 225 }); 226 227 if (!existing) { 228 throw new HTTPException(404, { 229 message: "Generic webhook integration not found", 230 }); 231 } 232 233 const currentConfig = normalizeGenericWebhookConfig( 234 JSON.parse(existing.config) as GenericWebhookConfig, 235 ); 236 const nextConfig = normalizeGenericWebhookConfig({ 237 webhookUrl: body.webhookUrl?.trim() || currentConfig.webhookUrl, 238 secret: 239 body.secret === undefined 240 ? currentConfig.secret 241 : (body.secret ?? undefined), 242 events: { 243 ...(currentConfig.events ?? {}), 244 ...(body.events ?? {}), 245 }, 246 }); 247 248 const validation = await validateGenericWebhookConfig(nextConfig); 249 if (!validation.valid) { 250 throw new HTTPException(400, { 251 message: validation.errors?.join(", ") ?? "Invalid config", 252 }); 253 } 254 255 await db 256 .update(integrationTable) 257 .set({ 258 config: JSON.stringify(nextConfig), 259 isActive: 260 body.isActive !== undefined 261 ? body.isActive 262 : (existing.isActive ?? true), 263 updatedAt: new Date(), 264 }) 265 .where(eq(integrationTable.id, existing.id)); 266 267 return c.json(await getGenericWebhookIntegration(projectId)); 268 }, 269 ) 270 .delete( 271 "/project/:projectId", 272 describeRoute({ 273 operationId: "deleteGenericWebhookIntegration", 274 tags: ["Generic Webhook"], 275 description: "Delete generic outgoing webhook integration for a project", 276 responses: { 277 200: { 278 description: "Generic webhook integration deleted successfully", 279 content: { 280 "application/json": { 281 schema: resolver(v.object({ success: v.boolean() })), 282 }, 283 }, 284 }, 285 }, 286 }), 287 validator("param", v.object({ projectId: v.string() })), 288 workspaceAccess.fromProject("projectId"), 289 async (c) => { 290 const { projectId } = c.req.valid("param"); 291 292 const existing = await db.query.integrationTable.findFirst({ 293 where: and( 294 eq(integrationTable.projectId, projectId), 295 eq(integrationTable.type, "generic-webhook"), 296 ), 297 }); 298 299 if (!existing) { 300 throw new HTTPException(404, { 301 message: "Generic webhook integration not found", 302 }); 303 } 304 305 await db 306 .delete(integrationTable) 307 .where(eq(integrationTable.id, existing.id)); 308 309 return c.json({ success: true }); 310 }, 311 ); 312 313export default genericWebhookIntegration;