mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
feat: per-group webhook ingress, custom webhooks, GitHub App tenant isolation
Add POST /webhook/{groupId} with per-group secrets (KV tenant:{groupId}), a custom provider (X-WebHooker-Signature HMAC, arbitrary JSON -> custom events through the route pipeline), and GitHub App installation isolation (Group.installationId) with automatic provisioning on installation.created (inst-{id} groups or binding matching owners groups). Includes WebhookPanel admin UI, custom route template, docs and 157 passing tests.
This commit is contained in:
parent
0b078d938b
commit
b600f02027
34 changed files with 1711 additions and 183 deletions
|
|
@ -50,6 +50,25 @@ const en: Dict = {
|
|||
"members.errLastOwner": "Cannot remove the last owner.",
|
||||
"members.inviteOk": "Invite accepted — welcome!",
|
||||
"members.inviteBad": "This invite is invalid or expired.",
|
||||
"webhook.title": "Webhook endpoint",
|
||||
"webhook.note": "(per-group URL + secret)",
|
||||
"webhook.empty": "Not configured. Generate a secret to enable the group's own webhook endpoint.",
|
||||
"webhook.url": "URL",
|
||||
"webhook.secret": "Secret",
|
||||
"webhook.noSecret": "— no secret configured —",
|
||||
"webhook.secretHidden":
|
||||
"The secret is only shown once after generating/regenerating. Regenerate to reveal a new one.",
|
||||
"webhook.copy": "Copy",
|
||||
"webhook.copied": "Copied!",
|
||||
"webhook.generate": "Generate secret",
|
||||
"webhook.regenerate": "Regenerate",
|
||||
"webhook.disable": "Disable",
|
||||
"webhook.usageTitle": "How to use",
|
||||
"webhook.usageGitHub":
|
||||
"GitHub: repo/org Settings → Webhooks → Add webhook → payload URL = the URL above, Content type = application/json, Secret = the secret above.",
|
||||
"webhook.usageGitea": "Gitea: repo Settings → Webhooks → Add Webhook → same URL + secret.",
|
||||
"webhook.usageCustom":
|
||||
"Custom: POST any JSON signed with X-WebHooker-Signature (sha256 HMAC). Create a route with event = custom to receive it:",
|
||||
"groups.empty": "No groups yet. Groups scope routes by org/user and delegate access.",
|
||||
"groups.createFirst": "Create your first group",
|
||||
"groups.admins": "ADMINS",
|
||||
|
|
@ -95,6 +114,7 @@ const en: Dict = {
|
|||
"templates.createDelete": "Create / Delete",
|
||||
"templates.member": "Member",
|
||||
"templates.commitComment": "Commit Comment",
|
||||
"templates.customWebhook": "Custom Webhook",
|
||||
"routeEditor.name": "Name",
|
||||
"routeEditor.namePlaceholder": "My Route",
|
||||
"routeEditor.id": "ID",
|
||||
|
|
@ -167,6 +187,12 @@ const en: Dict = {
|
|||
"groupEditor.providersNote": "(leave both unchecked = all)",
|
||||
"groupEditor.providersHint":
|
||||
"Only webhook events from the checked forges (GitHub / Gitea) enter this group's routes.",
|
||||
"groupEditor.installationId": "GitHub App installation ID",
|
||||
"groupEditor.installationIdNote": "(optional)",
|
||||
"groupEditor.installationIdPlaceholder": "e.g. 12345678",
|
||||
"groupEditor.installationIdHint":
|
||||
"Bind this group to one GitHub App installation (the org/user that installed the app). Only events from that installation enter this group's routes; leave empty to allow any installation (still filtered by Owner scope).",
|
||||
"groupEditor.errInstallationId": "Installation ID must be a positive integer",
|
||||
"groupEditor.emoji": "Show emojis in messages",
|
||||
"groupEditor.logTarget": "Webhook log channel",
|
||||
"groupEditor.logTargetNote": "(optional)",
|
||||
|
|
@ -276,6 +302,24 @@ const zh: Dict = {
|
|||
"members.errLastOwner": "不能移除最后一位 owner。",
|
||||
"members.inviteOk": "邀请已接受 —— 欢迎!",
|
||||
"members.inviteBad": "该邀请无效或已过期。",
|
||||
"webhook.title": "Webhook 入口",
|
||||
"webhook.note": "(分组独立的 URL + secret)",
|
||||
"webhook.empty": "未配置。生成 secret 即可启用该分组的独立 webhook 入口。",
|
||||
"webhook.url": "URL",
|
||||
"webhook.secret": "Secret",
|
||||
"webhook.noSecret": "—— 未配置 secret ——",
|
||||
"webhook.secretHidden": "secret 仅在生成/重新生成时显示一次。如需查看请重新生成。",
|
||||
"webhook.copy": "复制",
|
||||
"webhook.copied": "已复制!",
|
||||
"webhook.generate": "生成 secret",
|
||||
"webhook.regenerate": "重新生成",
|
||||
"webhook.disable": "停用",
|
||||
"webhook.usageTitle": "使用方法",
|
||||
"webhook.usageGitHub":
|
||||
"GitHub:仓库/组织 Settings → Webhooks → Add webhook → Payload URL 填上面的 URL,Content type 选 application/json,Secret 填上面的 secret。",
|
||||
"webhook.usageGitea": "Gitea:仓库 Settings → Webhooks → Add Webhook,填写同样的 URL 和 secret。",
|
||||
"webhook.usageCustom":
|
||||
"自定义:用 X-WebHooker-Signature(sha256 HMAC)签名任意 JSON 后 POST。创建一条 event = custom 的路由即可接收:",
|
||||
"groups.admins": "管理员",
|
||||
"groups.owners": "来源",
|
||||
"groups.members": "成员",
|
||||
|
|
@ -319,6 +363,7 @@ const zh: Dict = {
|
|||
"templates.createDelete": "创建 / 删除",
|
||||
"templates.member": "协作者",
|
||||
"templates.commitComment": "提交评论",
|
||||
"templates.customWebhook": "自定义 Webhook",
|
||||
"routeEditor.name": "名称",
|
||||
"routeEditor.namePlaceholder": "我的路由",
|
||||
"routeEditor.id": "ID",
|
||||
|
|
@ -390,6 +435,12 @@ const zh: Dict = {
|
|||
"groupEditor.providersNote": "(都不勾选 = 全部来源)",
|
||||
"groupEditor.providersHint":
|
||||
"只有来自所勾选 forge(GitHub / Gitea)的 webhook 事件才会进入本分组的路由。",
|
||||
"groupEditor.installationId": "GitHub App 安装 ID",
|
||||
"groupEditor.installationIdNote": "(可选)",
|
||||
"groupEditor.installationIdPlaceholder": "如 12345678",
|
||||
"groupEditor.installationIdHint":
|
||||
"将本分组绑定到一个 GitHub App 安装(安装了 App 的组织/用户)。只有来自该安装的事件才会进入本分组的路由;留空表示接受任意安装(仍受来源限定过滤)。",
|
||||
"groupEditor.errInstallationId": "安装 ID 必须为正整数",
|
||||
"groupEditor.emoji": "消息中显示表情符号",
|
||||
"groupEditor.logTarget": "Webhook 日志频道",
|
||||
"groupEditor.logTargetNote": "(可选)",
|
||||
|
|
|
|||
50
admin/composables/useWebhook.ts
Normal file
50
admin/composables/useWebhook.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export interface GroupWebhookInfo {
|
||||
url: string;
|
||||
hasSecret: boolean;
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export function useWebhookApi() {
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await fetch(path, {
|
||||
headers: { accept: "application/json" },
|
||||
credentials: "same-origin",
|
||||
...init,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(data.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : String(err);
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function info(groupId: string): Promise<GroupWebhookInfo> {
|
||||
return request(`/admin/api/groups/${encodeURIComponent(groupId)}/webhook`);
|
||||
}
|
||||
|
||||
function regenerate(groupId: string): Promise<GroupWebhookInfo> {
|
||||
return request(`/admin/api/groups/${encodeURIComponent(groupId)}/webhook/regenerate`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
function disable(groupId: string): Promise<{ ok: boolean }> {
|
||||
return request(`/admin/api/groups/${encodeURIComponent(groupId)}/webhook`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
return { loading, error, info, regenerate, disable };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue