mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-23 00:21:28 +00:00
merge: resolve docs conflict with origin/main (auto-fix)
Merged the remote lint/formatting fix; kept the group-level lang row in the zh group schema table.
This commit is contained in:
commit
13ba1e019e
12 changed files with 151 additions and 88 deletions
|
|
@ -57,22 +57,42 @@ describe("invites", () => {
|
|||
});
|
||||
|
||||
it("lists pending invites of a group only", async () => {
|
||||
await createInvite(kv, { groupId: "team", role: "viewer", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
await createInvite(kv, { groupId: "other", role: "viewer", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
await createInvite(kv, {
|
||||
groupId: "team",
|
||||
role: "viewer",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
await createInvite(kv, {
|
||||
groupId: "other",
|
||||
role: "viewer",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
const invites = await listInvites(kv, "team");
|
||||
expect(invites).toHaveLength(1);
|
||||
expect(invites[0]!.groupId).toBe("team");
|
||||
});
|
||||
|
||||
it("revokes an invite", async () => {
|
||||
const token = await createInvite(kv, { groupId: "team", role: "viewer", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
const token = await createInvite(kv, {
|
||||
groupId: "team",
|
||||
role: "viewer",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
await revokeInvite(kv, token);
|
||||
expect(await getInvite(kv, token)).toBeNull();
|
||||
});
|
||||
|
||||
it("accept adds the user as a member and consumes the token", async () => {
|
||||
await saveGroups(kv, [group]);
|
||||
const token = await createInvite(kv, { groupId: "team", role: "admin", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
const token = await createInvite(kv, {
|
||||
groupId: "team",
|
||||
role: "admin",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
const result = await acceptInvite(kv, token, "777", "newbie");
|
||||
expect(result).toEqual({ ok: true, groupId: "team", role: "admin" });
|
||||
expect(await getInvite(kv, token)).toBeNull();
|
||||
|
|
@ -84,9 +104,20 @@ describe("invites", () => {
|
|||
|
||||
it("upgrades an existing viewer to admin", async () => {
|
||||
await saveGroups(kv, [
|
||||
{ ...group, members: [{ login: "boss", role: "owner" }, { login: "newbie", role: "viewer" }] },
|
||||
{
|
||||
...group,
|
||||
members: [
|
||||
{ login: "boss", role: "owner" },
|
||||
{ login: "newbie", role: "viewer" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const token = await createInvite(kv, { groupId: "team", role: "admin", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
const token = await createInvite(kv, {
|
||||
groupId: "team",
|
||||
role: "admin",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
await acceptInvite(kv, token, "777", "newbie");
|
||||
const groups = await loadGroups(kv);
|
||||
expect(groups[0]!.members).toContainEqual({ login: "newbie", role: "admin" });
|
||||
|
|
@ -94,14 +125,24 @@ describe("invites", () => {
|
|||
|
||||
it("rejects expired or unknown invites", async () => {
|
||||
await saveGroups(kv, [group]);
|
||||
const token = await createInvite(kv, { groupId: "team", role: "viewer", expiresAt: Date.now() - 1000, createdBy: "boss" });
|
||||
const token = await createInvite(kv, {
|
||||
groupId: "team",
|
||||
role: "viewer",
|
||||
expiresAt: Date.now() - 1000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
expect(await acceptInvite(kv, token, "1", "x")).toEqual({ ok: false, reason: "invalid" });
|
||||
expect(await acceptInvite(kv, "deadbeef", "1", "x")).toEqual({ ok: false, reason: "invalid" });
|
||||
});
|
||||
|
||||
it("rejects invites for missing groups", async () => {
|
||||
await saveGroups(kv, []);
|
||||
const token = await createInvite(kv, { groupId: "ghost", role: "viewer", expiresAt: Date.now() + 86400_000, createdBy: "boss" });
|
||||
const token = await createInvite(kv, {
|
||||
groupId: "ghost",
|
||||
role: "viewer",
|
||||
expiresAt: Date.now() + 86400_000,
|
||||
createdBy: "boss",
|
||||
});
|
||||
expect(await acceptInvite(kv, token, "1", "x")).toEqual({ ok: false, reason: "group-missing" });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ export interface AuditEntry {
|
|||
ip?: string;
|
||||
}
|
||||
|
||||
const COLUMNS = "id, ts, actor_id, actor_login, action, target_type, target_id, group_id, detail, ip";
|
||||
const COLUMNS =
|
||||
"id, ts, actor_id, actor_login, action, target_type, target_id, group_id, detail, ip";
|
||||
|
||||
interface AuditRow {
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -148,14 +148,7 @@ export function createActionRoutes(): Hono<AuthEnv> {
|
|||
repo: body.repo,
|
||||
issue_number: body.issueNumber,
|
||||
content: body.reaction as
|
||||
| "+1"
|
||||
| "-1"
|
||||
| "laugh"
|
||||
| "confused"
|
||||
| "heart"
|
||||
| "hooray"
|
||||
| "rocket"
|
||||
| "eyes",
|
||||
"+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
|
||||
});
|
||||
} catch (err) {
|
||||
log.error({ err }, "Failed to create reaction");
|
||||
|
|
|
|||
|
|
@ -292,10 +292,7 @@ function ownerCount(members: GroupMember[]): number {
|
|||
}
|
||||
|
||||
/** Route params are always present for matched paths; keeps Hono's loose typing honest. */
|
||||
function param(
|
||||
c: { req: { param: (name: string) => string | undefined } },
|
||||
name: string,
|
||||
): string {
|
||||
function param(c: { req: { param: (name: string) => string | undefined } }, name: string): string {
|
||||
return c.req.param(name) ?? "";
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +326,9 @@ export function createAdminRoutes(): Hono<AuthEnv> {
|
|||
if (!token) return c.redirect("/admin");
|
||||
const auth = c.get("auth");
|
||||
if (!auth) {
|
||||
return c.redirect(`/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`);
|
||||
return c.redirect(
|
||||
`/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`,
|
||||
);
|
||||
}
|
||||
const result = await acceptInvite(c.env.KV, token, auth.session.userId, auth.session.login);
|
||||
if (result.ok) {
|
||||
|
|
@ -411,10 +410,7 @@ export function createAdminRoutes(): Hono<AuthEnv> {
|
|||
);
|
||||
const otherOwner = ownerCount(members) > 1;
|
||||
if (!stillMine && !otherOwner) {
|
||||
return c.json(
|
||||
{ error: `group "${g.id}" would be left without an owner by you` },
|
||||
403,
|
||||
);
|
||||
return c.json({ error: `group "${g.id}" would be left without an owner by you` }, 403);
|
||||
}
|
||||
}
|
||||
nextAll = [
|
||||
|
|
@ -453,7 +449,8 @@ export function createAdminRoutes(): Hono<AuthEnv> {
|
|||
if (prev.emoji !== g.emoji) fields.push("emoji");
|
||||
if (!deepEqual(prev.providers ?? [], g.providers ?? [])) fields.push("providers");
|
||||
if (!deepEqual(prev.owners ?? [], g.owners ?? [])) fields.push("owners");
|
||||
if (!deepEqual(prev.members ?? normalizeGroupMembers(prev), g.members)) fields.push("members");
|
||||
if (!deepEqual(prev.members ?? normalizeGroupMembers(prev), g.members))
|
||||
fields.push("members");
|
||||
if (fields.length > 0) {
|
||||
await recordAudit(c.env.DB, {
|
||||
ts: Date.now(),
|
||||
|
|
|
|||
|
|
@ -62,9 +62,7 @@ export function currentAuth(c: Context<AuthEnv>): AuthContext {
|
|||
return c.get(AUTH_KEY);
|
||||
}
|
||||
|
||||
export type GroupAccess =
|
||||
| { ok: true; group: Group }
|
||||
| { ok: false; status: 403 | 404 };
|
||||
export type GroupAccess = { ok: true; group: Group } | { ok: false; status: 403 | 404 };
|
||||
|
||||
/** Resolves the group and checks the user can at least view it. */
|
||||
export function requireGroup(c: Context<AuthEnv>, groupId: string): GroupAccess {
|
||||
|
|
@ -95,7 +93,9 @@ export function requireGroupRole(
|
|||
export { canEditGroup, canEditRoutes, roleAt };
|
||||
|
||||
/** Best-effort client IP for audit entries (Cloudflare header first). */
|
||||
export function clientIp(c: { req: { header: (n: string) => string | undefined } }): string | undefined {
|
||||
export function clientIp(c: {
|
||||
req: { header: (n: string) => string | undefined };
|
||||
}): string | undefined {
|
||||
return c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue