mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
fix: allow duplicate route IDs across groups and add group_id to send_logs
- Change validateRoutes to per-group ID uniqueness instead of global - Remove post-merge cross-group route ID uniqueness checks - Include groupId in msg:* KV key to prevent collision - Add group_id column to send_logs table (migration 0004) - Record groupId in send_logs for accurate permission filtering - Use groupId from log entries for admin log access checks
This commit is contained in:
parent
ab98c84896
commit
03eef7ae89
5 changed files with 26 additions and 29 deletions
1
migrations/0004_add_group_id.sql
Normal file
1
migrations/0004_add_group_id.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE send_logs ADD COLUMN group_id TEXT;
|
||||||
|
|
@ -6,6 +6,7 @@ function createMockDB(): D1Database {
|
||||||
const insertCols = [
|
const insertCols = [
|
||||||
"ts",
|
"ts",
|
||||||
"route_id",
|
"route_id",
|
||||||
|
"group_id",
|
||||||
"event",
|
"event",
|
||||||
"repo",
|
"repo",
|
||||||
"target",
|
"target",
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
const base: {
|
const base: {
|
||||||
ts: number;
|
ts: number;
|
||||||
routeId: string;
|
routeId: string;
|
||||||
|
groupId: string | undefined;
|
||||||
event: string;
|
event: string;
|
||||||
repo: string | undefined;
|
repo: string | undefined;
|
||||||
target: string;
|
target: string;
|
||||||
|
|
@ -78,6 +79,7 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
} = {
|
} = {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
routeId: route.id,
|
routeId: route.id,
|
||||||
|
groupId: route.groupId,
|
||||||
event: event.event,
|
event: event.event,
|
||||||
repo: (event.payload.repository as { full_name?: string } | undefined)?.full_name,
|
repo: (event.payload.repository as { full_name?: string } | undefined)?.full_name,
|
||||||
target: targetStr,
|
target: targetStr,
|
||||||
|
|
@ -91,7 +93,8 @@ export async function dispatchEvent(config: Config, event: WebhookEvent, env: En
|
||||||
const driver = getDriver(target);
|
const driver = getDriver(target);
|
||||||
let result: SendResult;
|
let result: SendResult;
|
||||||
if (message.updateKey) {
|
if (message.updateKey) {
|
||||||
const kvKey = `msg:${route.id}:${message.updateKey}:${targetStr}`;
|
const groupPrefix = route.groupId ? `${route.groupId}:` : "";
|
||||||
|
const kvKey = `msg:${groupPrefix}${route.id}:${message.updateKey}:${targetStr}`;
|
||||||
const existingId = await env.KV.get(kvKey);
|
const existingId = await env.KV.get(kvKey);
|
||||||
if (existingId) {
|
if (existingId) {
|
||||||
result = await driver.edit(message, target, env, existingId);
|
result = await driver.edit(message, target, env, existingId);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export interface SendRecord {
|
||||||
id?: number;
|
id?: number;
|
||||||
ts: number;
|
ts: number;
|
||||||
routeId: string;
|
routeId: string;
|
||||||
|
groupId?: string;
|
||||||
event: string;
|
event: string;
|
||||||
repo?: string;
|
repo?: string;
|
||||||
target: string;
|
target: string;
|
||||||
|
|
@ -22,12 +23,13 @@ export interface SendRecord {
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS =
|
const COLUMNS =
|
||||||
"id, ts, route_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail";
|
"id, ts, route_id, group_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail";
|
||||||
|
|
||||||
interface LogRow {
|
interface LogRow {
|
||||||
id: number;
|
id: number;
|
||||||
ts: number;
|
ts: number;
|
||||||
route_id: string;
|
route_id: string;
|
||||||
|
group_id: string | null;
|
||||||
event: string;
|
event: string;
|
||||||
repo: string | null;
|
repo: string | null;
|
||||||
target: string;
|
target: string;
|
||||||
|
|
@ -50,6 +52,7 @@ function toRecord(r: LogRow): SendRecord {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
ts: r.ts,
|
ts: r.ts,
|
||||||
routeId: r.route_id,
|
routeId: r.route_id,
|
||||||
|
groupId: r.group_id ?? undefined,
|
||||||
event: r.event,
|
event: r.event,
|
||||||
repo: r.repo ?? undefined,
|
repo: r.repo ?? undefined,
|
||||||
target: r.target,
|
target: r.target,
|
||||||
|
|
@ -72,12 +75,13 @@ export async function recordSend(db: D1Database, record: SendRecord): Promise<vo
|
||||||
try {
|
try {
|
||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO send_logs (ts, route_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail)
|
`INSERT INTO send_logs (ts, route_id, group_id, event, repo, target, ok, error, status, message_id, delivery_id, platform, actor, action, duration_ms, error_code, attempts, detail)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
record.ts,
|
record.ts,
|
||||||
record.routeId,
|
record.routeId,
|
||||||
|
record.groupId ?? null,
|
||||||
record.event,
|
record.event,
|
||||||
record.repo ?? null,
|
record.repo ?? null,
|
||||||
record.target,
|
record.target,
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,23 @@ function validateRoutes(
|
||||||
if (!Array.isArray(routes)) return { ok: false, error: "routes must be an array" };
|
if (!Array.isArray(routes)) return { ok: false, error: "routes must be an array" };
|
||||||
if (routes.length > 200) return { ok: false, error: "too many routes" };
|
if (routes.length > 200) return { ok: false, error: "too many routes" };
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seenByGroup = new Map<string, Set<string>>();
|
||||||
for (let i = 0; i < routes.length; i++) {
|
for (let i = 0; i < routes.length; i++) {
|
||||||
const r = routes[i] as Record<string, unknown>;
|
const r = routes[i] as Record<string, unknown>;
|
||||||
if (!r || typeof r !== "object") return { ok: false, error: `route[${i}] is not an object` };
|
if (!r || typeof r !== "object") return { ok: false, error: `route[${i}] is not an object` };
|
||||||
if (typeof r.id !== "string" || !ID_RE.test(r.id)) {
|
if (typeof r.id !== "string" || !ID_RE.test(r.id)) {
|
||||||
return { ok: false, error: `route[${i}].id is invalid` };
|
return { ok: false, error: `route[${i}].id is invalid` };
|
||||||
}
|
}
|
||||||
if (seen.has(r.id)) return { ok: false, error: `duplicate route id "${r.id}"` };
|
const gid = (r.groupId as string) ?? "__nogroup__";
|
||||||
seen.add(r.id);
|
let groupSeen = seenByGroup.get(gid);
|
||||||
|
if (!groupSeen) {
|
||||||
|
groupSeen = new Set();
|
||||||
|
seenByGroup.set(gid, groupSeen);
|
||||||
|
}
|
||||||
|
if (groupSeen.has(r.id)) {
|
||||||
|
return { ok: false, error: `duplicate route id "${r.id}" in group "${gid}"` };
|
||||||
|
}
|
||||||
|
groupSeen.add(r.id);
|
||||||
// Skip full validation for routes that are unchanged from what is stored.
|
// Skip full validation for routes that are unchanged from what is stored.
|
||||||
const prev = unchanged?.get(r.id);
|
const prev = unchanged?.get(r.id);
|
||||||
if (prev && deepEqual(r, prev)) continue;
|
if (prev && deepEqual(r, prev)) continue;
|
||||||
|
|
@ -282,12 +290,8 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
if (s.scope.isSuper) {
|
if (s.scope.isSuper) {
|
||||||
return c.json({ logs: await getSendLog(c.env.DB, limit) });
|
return c.json({ logs: await getSendLog(c.env.DB, limit) });
|
||||||
}
|
}
|
||||||
const all = await loadRoutes(c.env.KV);
|
|
||||||
const allowed = new Set(
|
|
||||||
all.filter((r) => r.groupId != null && s.scope.groupIds.has(r.groupId)).map((r) => r.id),
|
|
||||||
);
|
|
||||||
const logs = (await getSendLog(c.env.DB, 200))
|
const logs = (await getSendLog(c.env.DB, 200))
|
||||||
.filter((l) => allowed.has(l.routeId))
|
.filter((l) => l.groupId != null && s.scope.groupIds.has(l.groupId))
|
||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
return c.json({ logs });
|
return c.json({ logs });
|
||||||
});
|
});
|
||||||
|
|
@ -300,9 +304,7 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
const entry = await getSendLogById(c.env.DB, id);
|
const entry = await getSendLogById(c.env.DB, id);
|
||||||
if (!entry) return c.json({ error: "Log entry not found" }, 404);
|
if (!entry) return c.json({ error: "Log entry not found" }, 404);
|
||||||
if (!s.scope.isSuper) {
|
if (!s.scope.isSuper) {
|
||||||
const all = await loadRoutes(c.env.KV);
|
if (!entry.groupId || !s.scope.groupIds.has(entry.groupId)) {
|
||||||
const route = all.find((r) => r.id === entry.routeId);
|
|
||||||
if (!route?.groupId || !s.scope.groupIds.has(route.groupId)) {
|
|
||||||
return c.json({ error: "Forbidden" }, 403);
|
return c.json({ error: "Forbidden" }, 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -344,13 +346,6 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard against duplicate ids across the merged set.
|
|
||||||
const ids = new Set<string>();
|
|
||||||
for (const r of nextAll) {
|
|
||||||
if (ids.has(r.id)) return c.json({ error: `duplicate route id "${r.id}"` }, 400);
|
|
||||||
ids.add(r.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await saveRoutes(c.env.KV, nextAll);
|
await saveRoutes(c.env.KV, nextAll);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -422,13 +417,6 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
|
||||||
const others = existing.filter((r) => r.groupId !== groupId);
|
const others = existing.filter((r) => r.groupId !== groupId);
|
||||||
const nextAll = [...others, ...result.routes];
|
const nextAll = [...others, ...result.routes];
|
||||||
|
|
||||||
// Guard against ids colliding with routes in other groups.
|
|
||||||
const ids = new Set<string>();
|
|
||||||
for (const r of nextAll) {
|
|
||||||
if (ids.has(r.id)) return c.json({ error: `duplicate route id "${r.id}"` }, 400);
|
|
||||||
ids.add(r.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await saveRoutes(c.env.KV, nextAll);
|
await saveRoutes(c.env.KV, nextAll);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue