mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat(groups): host-based forge sources with optional display name
- forgeSources entries are now { host, type, name? }: the repository URL's
hostname is matched case-insensitively against host (github.com for GitHub,
distinct hosts for multiple Gitea instances); the footer label is the
optional name, falling back to the host
- GroupEditor renders one row per source: host input + type select + optional
display name (grid layout); hostname validation mirrors the server
- fix: apiFetch sends Content-Type: application/json — h3's readBody only
parses JSON bodies with that header, so every PUT/POST from the refactored
console arrived as a raw string and failed with 'groups must be an array'
- hardening: readJsonBody (admin + actions) JSON-parses string bodies so curl
and older clients without the content-type header still work
- regression test: groups PUT without content-type + forgeSources round-trip
- docs: groups.md/message-format.md (en/zh), AGENTS.md, config.example.yaml
This commit is contained in:
parent
17d10db845
commit
3f6f7f17b5
19 changed files with 469 additions and 108 deletions
|
|
@ -478,6 +478,32 @@
|
|||
@apply mt-1 w-full justify-center;
|
||||
}
|
||||
|
||||
.forge-source-row {
|
||||
@apply mb-2 grid items-center gap-2 rounded-sm border border-border bg-surface-2 p-2.5;
|
||||
grid-template-areas:
|
||||
"host type del"
|
||||
"name name name";
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.forge-source-row .f-host {
|
||||
grid-area: host;
|
||||
}
|
||||
|
||||
.forge-source-row .select.forge-type {
|
||||
grid-area: type;
|
||||
@apply w-[110px];
|
||||
}
|
||||
|
||||
.forge-source-row .f-name {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
.forge-source-row > .icon-btn {
|
||||
grid-area: del;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
/* ---- Toasts ---- */
|
||||
.toasts {
|
||||
@apply pointer-events-none fixed bottom-7 left-1/2 z-[60] flex -translate-x-1/2 flex-col items-center gap-2;
|
||||
|
|
|
|||
|
|
@ -46,27 +46,47 @@
|
|||
/>
|
||||
<div class="hint">{{ t("groupEditor.langHint") }}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.emoji") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.emojiNote") }}</span></label
|
||||
>
|
||||
<label class="inline">
|
||||
<input v-model="form.emoji" type="checkbox" />
|
||||
<span>{{ t("groupEditor.emojiLabel") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.forgeLabel") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.forgeLabelNote") }}</span></label
|
||||
>
|
||||
<label class="inline">
|
||||
<input v-model="form.forgeLabel" type="checkbox" />
|
||||
<span>{{ t("groupEditor.forgeLabelLabel") }}</span>
|
||||
</label>
|
||||
<div class="hint">{{ t("groupEditor.forgeLabelHint") }}</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.emoji") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.emojiNote") }}</span></label
|
||||
>
|
||||
<label class="inline">
|
||||
<input v-model="form.emoji" type="checkbox" />
|
||||
<span>{{ t("groupEditor.emojiLabel") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
>{{ t("groupEditor.forgeSources") }}
|
||||
<span class="lbl-note">{{ t("groupEditor.forgeSourcesNote") }}</span></label
|
||||
>
|
||||
<div v-for="(s, i) in form.forgeSources" :key="i" class="forge-source-row">
|
||||
<input
|
||||
v-model="s.host"
|
||||
type="text"
|
||||
class="input f-host"
|
||||
:placeholder="t('groupEditor.forgeSourcesHost')"
|
||||
/>
|
||||
<select v-model="s.type" class="select forge-type">
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gitea">Gitea</option>
|
||||
</select>
|
||||
<button type="button" class="icon-btn danger" @click="form.forgeSources.splice(i, 1)">
|
||||
✕
|
||||
</button>
|
||||
<input
|
||||
v-model="s.name"
|
||||
type="text"
|
||||
class="input f-name"
|
||||
:placeholder="t('groupEditor.forgeSourcesName')"
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost add-filter" @click="addForgeSource">
|
||||
{{ t("groupEditor.forgeSourcesAdd") }}
|
||||
</button>
|
||||
<div class="hint">{{ t("groupEditor.forgeSourcesHint") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
|
|
@ -193,7 +213,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from "vue";
|
||||
import type { Group } from "~/types";
|
||||
import type { ForgeSource, Group } from "~/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
@ -219,7 +239,7 @@ const form = reactive({
|
|||
providers: [] as ("github" | "gitea")[],
|
||||
installationId: "",
|
||||
emoji: true,
|
||||
forgeLabel: false,
|
||||
forgeSources: [] as ForgeSource[],
|
||||
lang: "",
|
||||
logPlatform: "" as "" | "discord" | "telegram",
|
||||
logChannelId: "",
|
||||
|
|
@ -228,6 +248,9 @@ const form = reactive({
|
|||
logTopicId: "",
|
||||
});
|
||||
|
||||
function addForgeSource(): void {
|
||||
form.forgeSources.push({ host: "", type: "github" });
|
||||
}
|
||||
function toggleProvider(p: "github" | "gitea", e: Event): void {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
form.providers = checked
|
||||
|
|
@ -249,7 +272,7 @@ watch(
|
|||
);
|
||||
form.installationId = g?.installationId != null ? String(g.installationId) : "";
|
||||
form.emoji = g?.emoji ?? true;
|
||||
form.forgeLabel = g?.forgeLabel ?? false;
|
||||
form.forgeSources = (g?.forgeSources ?? []).map((s) => ({ ...s }));
|
||||
form.lang = g?.lang ?? "";
|
||||
form.logPlatform = lt?.platform ?? "";
|
||||
form.logChannelId = lt?.channelId ?? "";
|
||||
|
|
@ -308,6 +331,12 @@ function save(): void {
|
|||
return;
|
||||
}
|
||||
}
|
||||
for (const s of form.forgeSources) {
|
||||
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s.host.trim())) {
|
||||
formError.value = t("groupEditor.errForgeSourceHost");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const owners = splitList(form.owners);
|
||||
const members = props.group?.members
|
||||
? props.group.members.map((m) => ({ ...m }))
|
||||
|
|
@ -323,7 +352,13 @@ function save(): void {
|
|||
providers: form.providers.length ? form.providers : undefined,
|
||||
installationId,
|
||||
emoji: form.emoji,
|
||||
forgeLabel: form.forgeLabel,
|
||||
forgeSources: form.forgeSources.length
|
||||
? form.forgeSources.map((s) => ({
|
||||
host: s.host.trim(),
|
||||
type: s.type,
|
||||
...(s.name?.trim() ? { name: s.name.trim() } : {}),
|
||||
}))
|
||||
: undefined,
|
||||
lang: form.lang.trim() || undefined,
|
||||
logTarget,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T>
|
|||
const res = await fetch(path, {
|
||||
credentials: "same-origin",
|
||||
...init,
|
||||
headers: { accept: "application/json", ...(init?.headers ?? {}) },
|
||||
// JSON in/out: h3's readBody only parses JSON bodies when the request
|
||||
// declares application/json, and the browser defaults string bodies to
|
||||
// text/plain — without this header every PUT/POST would arrive as a
|
||||
// raw string and fail validation.
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (res.status === 401) {
|
||||
useAuthState().needLogin.value = true;
|
||||
|
|
|
|||
|
|
@ -198,11 +198,14 @@ const en: Dict = {
|
|||
"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.forgeLabel": "Show forge source",
|
||||
"groupEditor.forgeLabelNote": "(optional)",
|
||||
"groupEditor.forgeLabelLabel": "Show which forge (GitHub / Gitea / custom) produced each event",
|
||||
"groupEditor.forgeLabelHint":
|
||||
"Adds the forge icon and site name to the message footer so events from different forges can be told apart.",
|
||||
"groupEditor.forgeSources": "Forge hosts",
|
||||
"groupEditor.forgeSourcesNote": "(optional — label events by their host)",
|
||||
"groupEditor.forgeSourcesHost": "git.example.com",
|
||||
"groupEditor.forgeSourcesName": "Optional display name",
|
||||
"groupEditor.forgeSourcesAdd": "+ Add host",
|
||||
"groupEditor.forgeSourcesHint":
|
||||
"Events whose repository host matches an entry are labeled in the message footer with its display name (or the host when none is set) — e.g. git1.example.com and git2.example.com for two Gitea instances. GitHub events match github.com.",
|
||||
"groupEditor.errForgeSourceHost": "Every forge host must be a valid hostname",
|
||||
"groupEditor.logTarget": "Webhook log channel",
|
||||
"groupEditor.logTargetNote": "(optional)",
|
||||
"groupEditor.logDisabled": "— Disabled —",
|
||||
|
|
@ -453,11 +456,14 @@ const zh: Dict = {
|
|||
"将本分组绑定到一个 GitHub App 安装(安装了 App 的组织/用户)。只有来自该安装的事件才会进入本分组的路由;留空表示接受任意安装(仍受来源限定过滤)。",
|
||||
"groupEditor.errInstallationId": "安装 ID 必须为正整数",
|
||||
"groupEditor.emoji": "消息中显示表情符号",
|
||||
"groupEditor.forgeLabel": "显示来源平台",
|
||||
"groupEditor.forgeLabelNote": "(可选)",
|
||||
"groupEditor.forgeLabelLabel": "在消息中标识事件来源(GitHub / Gitea / 自定义)",
|
||||
"groupEditor.forgeLabelHint":
|
||||
"在消息底部显示来源平台的图标与站点名称,便于区分不同 forge 的事件。",
|
||||
"groupEditor.forgeSources": "来源主机",
|
||||
"groupEditor.forgeSourcesNote": "(可选 —— 按主机标识事件)",
|
||||
"groupEditor.forgeSourcesHost": "git.example.com",
|
||||
"groupEditor.forgeSourcesName": "可选显示名称",
|
||||
"groupEditor.forgeSourcesAdd": "+ 添加主机",
|
||||
"groupEditor.forgeSourcesHint":
|
||||
"仓库主机与条目匹配的事件,会在消息底部用该条目的显示名称标注(未填名称则用主机名)——例如两个 Gitea 实例可分别填 git1.example.com 和 git2.example.com。GitHub 事件匹配 github.com。",
|
||||
"groupEditor.errForgeSourceHost": "每个来源主机必须是有效的主机名",
|
||||
"groupEditor.logTarget": "Webhook 日志频道",
|
||||
"groupEditor.logTargetNote": "(可选)",
|
||||
"groupEditor.logDisabled": "— 未启用 —",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ export interface Route {
|
|||
|
||||
export type GroupRole = "owner" | "admin" | "viewer";
|
||||
|
||||
export interface ForgeSource {
|
||||
host: string;
|
||||
type: "github" | "gitea";
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
login: string;
|
||||
role: GroupRole;
|
||||
|
|
@ -40,7 +46,7 @@ export interface Group {
|
|||
providers?: ("github" | "gitea" | "gitlab")[];
|
||||
installationId?: number;
|
||||
emoji?: boolean;
|
||||
forgeLabel?: boolean;
|
||||
forgeSources?: ForgeSource[];
|
||||
lang?: string;
|
||||
logTarget?: RouteTarget;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue