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 317 lines 8.8 kB view raw
1import { eq } from "drizzle-orm"; 2import type { Context, Next } from "hono"; 3import { HTTPException } from "hono/http-exception"; 4import db, { schema } from "../database"; 5import { validateWorkspaceAccess } from "./validate-workspace-access"; 6 7type WorkspaceIdSource = 8 | { type: "query"; key: string } 9 | { type: "body"; key: string } 10 | { type: "param"; key: string } 11 | { 12 type: "lookup"; 13 resource: 14 | "project" 15 | "task" 16 | "label" 17 | "timeEntry" 18 | "activity" 19 | "comment" 20 | "column" 21 | "workflowRule"; 22 idKey: string; 23 }; 24 25type WorkspaceAccessMiddlewareConfig = { 26 sources: WorkspaceIdSource[]; 27}; 28 29async function readJsonObjectBody( 30 c: Context, 31): Promise<Record<string, unknown>> { 32 const raw = (await c.req.json().catch(() => ({}))) || {}; 33 if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { 34 return {}; 35 } 36 return raw as Record<string, unknown>; 37} 38 39export function workspaceAccessMiddleware( 40 config: WorkspaceAccessMiddlewareConfig, 41) { 42 return async (c: Context, next: Next) => { 43 const userId = c.get("userId"); 44 45 if (!userId) { 46 throw new HTTPException(401, { message: "Unauthorized" }); 47 } 48 49 let workspaceId: string | null = null; 50 51 for (const source of config.sources) { 52 if (source.type === "query") { 53 workspaceId = c.req.query(source.key) || null; 54 } else if (source.type === "body") { 55 const body = await readJsonObjectBody(c); 56 workspaceId = 57 typeof body[source.key] === "string" ? body[source.key] : null; 58 } else if (source.type === "param") { 59 workspaceId = c.req.param(source.key) || null; 60 } else if (source.type === "lookup") { 61 const body = await readJsonObjectBody(c); 62 const idFromBody = 63 typeof body[source.idKey] === "string" ? body[source.idKey] : null; 64 const id = 65 c.req.param(source.idKey) || c.req.query(source.idKey) || idFromBody; 66 if (id) { 67 workspaceId = await lookupWorkspaceId(source.resource, id); 68 } 69 } 70 71 if (workspaceId) { 72 break; 73 } 74 } 75 76 if (!workspaceId) { 77 throw new HTTPException(400, { 78 message: "Workspace ID could not be determined", 79 }); 80 } 81 82 const apiKey = c.get("apiKey"); 83 const apiKeyId = apiKey?.id; 84 85 await validateWorkspaceAccess(userId, workspaceId, apiKeyId); 86 87 c.set("workspaceId", workspaceId); 88 89 return next(); 90 }; 91} 92 93async function lookupWorkspaceId( 94 resource: 95 | "project" 96 | "task" 97 | "label" 98 | "timeEntry" 99 | "activity" 100 | "comment" 101 | "column" 102 | "workflowRule", 103 id: string, 104): Promise<string | null> { 105 try { 106 switch (resource) { 107 case "project": { 108 const [project] = await db 109 .select({ workspaceId: schema.projectTable.workspaceId }) 110 .from(schema.projectTable) 111 .where(eq(schema.projectTable.id, id)) 112 .limit(1); 113 return project?.workspaceId || null; 114 } 115 116 case "task": { 117 const [task] = await db 118 .select({ 119 workspaceId: schema.projectTable.workspaceId, 120 }) 121 .from(schema.taskTable) 122 .innerJoin( 123 schema.projectTable, 124 eq(schema.taskTable.projectId, schema.projectTable.id), 125 ) 126 .where(eq(schema.taskTable.id, id)) 127 .limit(1); 128 return task?.workspaceId || null; 129 } 130 131 case "label": { 132 const [label] = await db 133 .select({ workspaceId: schema.labelTable.workspaceId }) 134 .from(schema.labelTable) 135 .where(eq(schema.labelTable.id, id)) 136 .limit(1); 137 return label?.workspaceId || null; 138 } 139 140 case "timeEntry": { 141 const [timeEntry] = await db 142 .select({ 143 workspaceId: schema.projectTable.workspaceId, 144 }) 145 .from(schema.timeEntryTable) 146 .innerJoin( 147 schema.taskTable, 148 eq(schema.timeEntryTable.taskId, schema.taskTable.id), 149 ) 150 .innerJoin( 151 schema.projectTable, 152 eq(schema.taskTable.projectId, schema.projectTable.id), 153 ) 154 .where(eq(schema.timeEntryTable.id, id)) 155 .limit(1); 156 return timeEntry?.workspaceId || null; 157 } 158 159 case "activity": { 160 const [activity] = await db 161 .select({ 162 workspaceId: schema.projectTable.workspaceId, 163 }) 164 .from(schema.activityTable) 165 .innerJoin( 166 schema.taskTable, 167 eq(schema.activityTable.taskId, schema.taskTable.id), 168 ) 169 .innerJoin( 170 schema.projectTable, 171 eq(schema.taskTable.projectId, schema.projectTable.id), 172 ) 173 .where(eq(schema.activityTable.id, id)) 174 .limit(1); 175 return activity?.workspaceId || null; 176 } 177 178 case "comment": { 179 const [comment] = await db 180 .select({ 181 workspaceId: schema.projectTable.workspaceId, 182 }) 183 .from(schema.commentTable) 184 .innerJoin( 185 schema.taskTable, 186 eq(schema.commentTable.taskId, schema.taskTable.id), 187 ) 188 .innerJoin( 189 schema.projectTable, 190 eq(schema.taskTable.projectId, schema.projectTable.id), 191 ) 192 .where(eq(schema.commentTable.id, id)) 193 .limit(1); 194 return comment?.workspaceId || null; 195 } 196 197 case "column": { 198 const [column] = await db 199 .select({ 200 workspaceId: schema.projectTable.workspaceId, 201 }) 202 .from(schema.columnTable) 203 .innerJoin( 204 schema.projectTable, 205 eq(schema.columnTable.projectId, schema.projectTable.id), 206 ) 207 .where(eq(schema.columnTable.id, id)) 208 .limit(1); 209 return column?.workspaceId || null; 210 } 211 212 case "workflowRule": { 213 const [workflowRule] = await db 214 .select({ 215 workspaceId: schema.projectTable.workspaceId, 216 }) 217 .from(schema.workflowRuleTable) 218 .innerJoin( 219 schema.projectTable, 220 eq(schema.workflowRuleTable.projectId, schema.projectTable.id), 221 ) 222 .where(eq(schema.workflowRuleTable.id, id)) 223 .limit(1); 224 return workflowRule?.workspaceId || null; 225 } 226 227 default: 228 return null; 229 } 230 } catch (error) { 231 console.error(`Error looking up workspaceId for ${resource}:`, error); 232 return null; 233 } 234} 235 236export const workspaceAccess = { 237 fromQuery: (key = "workspaceId") => 238 workspaceAccessMiddleware({ sources: [{ type: "query", key }] }), 239 240 fromBody: (key = "workspaceId") => 241 workspaceAccessMiddleware({ sources: [{ type: "body", key }] }), 242 243 fromParam: (key = "workspaceId") => 244 workspaceAccessMiddleware({ sources: [{ type: "param", key }] }), 245 246 fromProject: (idKey = "id") => 247 workspaceAccessMiddleware({ 248 sources: [ 249 { type: "query", key: "workspaceId" }, 250 { type: "lookup", resource: "project", idKey }, 251 ], 252 }), 253 254 fromTask: (idKey = "id") => 255 workspaceAccessMiddleware({ 256 sources: [ 257 { type: "lookup", resource: "task", idKey }, 258 { type: "query", key: "workspaceId" }, 259 ], 260 }), 261 262 fromTaskId: (idKey = "taskId") => 263 workspaceAccessMiddleware({ 264 sources: [ 265 { type: "lookup", resource: "task", idKey }, 266 { type: "query", key: "workspaceId" }, 267 ], 268 }), 269 270 fromLabel: (idKey = "id") => 271 workspaceAccessMiddleware({ 272 sources: [ 273 { type: "lookup", resource: "label", idKey }, 274 { type: "query", key: "workspaceId" }, 275 ], 276 }), 277 278 fromTimeEntry: (idKey = "id") => 279 workspaceAccessMiddleware({ 280 sources: [ 281 { type: "lookup", resource: "timeEntry", idKey }, 282 { type: "query", key: "workspaceId" }, 283 ], 284 }), 285 286 fromActivity: (idKey = "id") => 287 workspaceAccessMiddleware({ 288 sources: [ 289 { type: "lookup", resource: "activity", idKey }, 290 { type: "query", key: "workspaceId" }, 291 ], 292 }), 293 294 fromComment: (idKey = "id") => 295 workspaceAccessMiddleware({ 296 sources: [ 297 { type: "lookup", resource: "comment", idKey }, 298 { type: "query", key: "workspaceId" }, 299 ], 300 }), 301 302 fromColumn: (idKey = "id") => 303 workspaceAccessMiddleware({ 304 sources: [ 305 { type: "lookup", resource: "column", idKey }, 306 { type: "query", key: "workspaceId" }, 307 ], 308 }), 309 310 fromWorkflowRule: (idKey = "id") => 311 workspaceAccessMiddleware({ 312 sources: [ 313 { type: "lookup", resource: "workflowRule", idKey }, 314 { type: "query", key: "workspaceId" }, 315 ], 316 }), 317};