mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
- 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
38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
/**
|
|
* Shared "session expired" flag: any 401 from the admin API flips it and the
|
|
* console swaps to the login card. One source of truth for every composable.
|
|
*/
|
|
export function useAuthState() {
|
|
const needLogin = useState<boolean>("wh-need-login", () => false);
|
|
return { needLogin };
|
|
}
|
|
|
|
/**
|
|
* JSON fetch for same-origin admin API calls: sends credentials, treats 401
|
|
* as "not logged in" (flips the shared flag and throws), and surfaces the
|
|
* server's `{ error }` message on failure.
|
|
*/
|
|
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
credentials: "same-origin",
|
|
...init,
|
|
// 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;
|
|
throw new Error("unauthorized");
|
|
}
|
|
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;
|
|
}
|