Files
outline/plugins/github/server/api/github.ts
T
Tom Moor f50bb00b29 Refactor of OAuth account linking flows (#12246)
* Refactor of OAuth account linking flows

* PR feedback
2026-05-02 18:54:38 -04:00

138 lines
4.0 KiB
TypeScript

import Router from "koa-router";
import find from "lodash/find";
import { IntegrationService, IntegrationType } from "@shared/types";
import { createContext } from "@server/context";
import { ValidationError } from "@server/errors";
import apexAuthRedirect from "@server/middlewares/apexAuthRedirect";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
import validateWebhook from "@server/middlewares/validateWebhook";
import { IntegrationAuthentication, Integration } from "@server/models";
import type { APIContext } from "@server/types";
import { verifyOAuthStateNonce } from "@server/utils/oauth";
import { GitHubOAuthNonceCookie, GitHubUtils } from "../../shared/GitHubUtils";
import env from "../env";
import { GitHub } from "../github";
import GitHubWebhookTask from "../tasks/GitHubWebhookTask";
import * as T from "./schema";
const router = new Router();
router.get(
"github.callback",
auth({ optional: true }),
validate(T.GitHubCallbackSchema),
apexAuthRedirect<T.GitHubCallbackReq>({
getTeamId: (ctx) => GitHubUtils.parseState(ctx.input.query.state)?.teamId,
getRedirectPath: (ctx, team) =>
GitHubUtils.callbackUrl({
baseUrl: team.url,
params: ctx.request.querystring,
}),
getErrorPath: () => GitHubUtils.errorUrl("unauthenticated"),
}),
transaction(),
async (ctx: APIContext<T.GitHubCallbackReq>) => {
const {
code,
state,
error,
installation_id: installationId,
setup_action: setupAction,
} = ctx.input.query;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
if (error) {
ctx.redirect(GitHubUtils.errorUrl(error));
return;
}
if (setupAction === T.SetupAction.request) {
ctx.redirect(GitHubUtils.installRequestUrl());
return;
}
const parsedState = GitHubUtils.parseState(state);
if (!parsedState) {
throw ValidationError("Invalid state");
}
verifyOAuthStateNonce(ctx, GitHubOAuthNonceCookie, parsedState.nonce);
const client = await GitHub.authenticateAsUser(code!, state);
const installationsByUser = await client.requestAppInstallations();
const installation = find(
installationsByUser,
(i) => i.id === installationId
);
if (!installation) {
return ctx.redirect(GitHubUtils.errorUrl("unauthenticated"));
}
const scopes = Object.entries(installation.permissions).map(
([name, permission]) => `${name}:${String(permission)}`
);
const authentication = await IntegrationAuthentication.create(
{
service: IntegrationService.GitHub,
userId: user.id,
teamId: user.teamId,
scopes,
},
{ transaction }
);
await Integration.createWithCtx(createContext({ user, transaction }), {
service: IntegrationService.GitHub,
type: IntegrationType.Embed,
userId: user.id,
teamId: user.teamId,
authenticationId: authentication.id,
settings: {
github: {
installation: {
id: installationId!,
account: {
id: installation.account?.id,
// @ts-expect-error Property 'login' does not exist on type
name: installation.account?.login,
avatarUrl: installation.account?.avatar_url,
},
},
},
},
});
ctx.redirect(GitHubUtils.url);
}
);
router.post(
"github.webhooks",
validateWebhook({
secretKey: env.GITHUB_WEBHOOK_SECRET!,
getSignatureFromHeader: (ctx) => {
const { headers } = ctx.request;
const signatureHeader = headers["x-hub-signature-256"];
const signature = Array.isArray(signatureHeader)
? signatureHeader[0]
: signatureHeader;
return signature?.split("=")[1];
},
}),
async (ctx: APIContext) => {
const { headers, body } = ctx.request;
await new GitHubWebhookTask().schedule({
payload: body,
headers,
});
ctx.status = 202;
}
);
export default router;