import type { H3Event } from "h3";
import {
appendResponseHeader,
createError,
getHeader,
getQuery,
readBody,
sendRedirect,
setResponseStatus,
} from "h3";
import {
getOAuthURL,
handleOAuthCallback as handleGithubOAuthCallback,
getInstallationAccount,
} from "../github/oauth";
import { removeToken, saveDiscordLink, saveTelegramLink } from "../github/store";
import { createAdminSession, adminCookie, getAdminSession } from "./session";
import {
loadGroups,
saveGroups,
resolveScope,
hasAnyAccess,
ensureInstallationGroup,
normalizeGroupMembers,
roleAt,
} from "./groups";
import { clientIp } from "./auth";
import { recordAudit } from "../lib/audit";
import { sendMessage } from "../drivers/telegram/rest";
import { cfEnv } from "../cf";
import type { Env, Group } from "../types";
interface PendingState {
redirectTo: string;
expiresAt: number;
discordUserId?: string;
telegramUserId?: string;
telegramChatId?: string;
}
function linkedPage(login: string): string {
return `
绑定成功
✓
GitHub 账号已绑定
已连接为 @${login},现在可以回到 Discord 用 GitHub 评论了。
`;
}
function generateRandomHex(length: number): string {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function safeRedirectPath(value: string | undefined): string {
if (!value) return "/";
if (!value.startsWith("/")) return "/";
if (value.startsWith("//")) return "/";
if (/^\/\\/.test(value)) return "/";
return value;
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function selfSignupEnabled(env: Env): boolean {
const flag = (env.ALLOW_SELF_SIGNUP ?? "").trim().toLowerCase();
return flag === "1" || flag === "true" || flag === "yes" || flag === "on";
}
/**
* Opt-in self service: users without any group access get a personal group
* they own, so they can configure their own routing without a super admin.
* The group id is deterministic (`u-{userId}`), so it is created at most once.
*/
async function ensurePersonalGroup(env: Env, userId: string, login: string): Promise {
if (!selfSignupEnabled(env)) return false;
const groups = await loadGroups(env.KV);
const gid = `u-${userId}`;
if (groups.some((g) => g.id === gid)) return true;
const personal: Group = {
id: gid,
name: `@${login}`,
members: [{ login, role: "owner" }],
adminIds: [login],
};
await saveGroups(env.KV, [...groups, personal]);
await recordAudit(env.DB, {
ts: Date.now(),
actorId: userId,
actorLogin: login,
action: "group.create",
targetType: "group",
targetId: gid,
groupId: gid,
detail: { auto: true },
});
return true;
}
/**
* Post-install choice page: pick which group the installation binds to.
* Options are the groups the signed-in user owns (role `owner`), plus a
* default "create a new group" choice.
*/
function installPage(opts: {
installationId: number;
accountLogin: string;
owned: Group[];
}): string {
const { installationId, accountLogin, owned } = opts;
const accountLine = accountLogin
? `
账号:${escapeHtml(accountLogin)}(安装 ID ${installationId})