You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
5.6 KiB
152 lines
5.6 KiB
import {SavedWebhook} from "./SavedWebhook.js";
|
|
import {FastifyReply, FastifyRequest} from "fastify";
|
|
import {AccessToken, AuthorizationCode, ModuleOptions} from "simple-oauth2";
|
|
import pug from "pug";
|
|
import {renderError} from "./PugRenderer.js";
|
|
import {APIWebhook} from "discord-api-types";
|
|
import {getBaseUrl, getFirstValue} from "./FastifyHelpers.js";
|
|
import {join, relative} from "path";
|
|
import {RouteGenericInterface} from "fastify/types/route";
|
|
import {checkAndClearXSRFCookie, generateXSRFCookie} from "./CookieHelpers.js";
|
|
|
|
interface DiscordWebhookToken extends AccessToken {
|
|
token: { webhook?: APIWebhook }
|
|
}
|
|
|
|
const AuthConfig: ModuleOptions["auth"] = {
|
|
tokenHost: "https://discord.com",
|
|
authorizePath: "/api/oauth2/authorize",
|
|
tokenPath: "/api/oauth2/token",
|
|
revokePath: "/api/oauth2/token/revoke",
|
|
}
|
|
|
|
export interface OAuthRoute extends RouteGenericInterface {
|
|
Querystring: {
|
|
code: string | string[] | undefined,
|
|
state: string | string[] | undefined,
|
|
error: string | string[] | undefined,
|
|
error_description: string | string[] | undefined,
|
|
error_uri: string | string[] | undefined,
|
|
}
|
|
}
|
|
|
|
export class DiscordWebhookHandler {
|
|
readonly webhook: SavedWebhook
|
|
readonly templateFilename: string
|
|
readonly config: ModuleOptions
|
|
readonly destinationFunc: () => string
|
|
|
|
constructor({
|
|
webhook,
|
|
templateFilename,
|
|
appId,
|
|
secret,
|
|
destinationFunc
|
|
}: { webhook: SavedWebhook, templateFilename: string, appId: string, secret: string, destinationFunc: () => string }) {
|
|
this.templateFilename = templateFilename
|
|
this.webhook = webhook
|
|
this.config = {
|
|
client: {
|
|
id: appId,
|
|
secret,
|
|
},
|
|
auth: AuthConfig,
|
|
}
|
|
this.destinationFunc = destinationFunc
|
|
}
|
|
|
|
async handleRequest(req: FastifyRequest<OAuthRoute>, res: FastifyReply): Promise<void> {
|
|
const baseUrl = getBaseUrl(req)
|
|
const withoutParams = new URL(req.url)
|
|
withoutParams.search = ""
|
|
const code = getFirstValue(req.query.code) ?? null
|
|
const errorCode = getFirstValue(req.query.error) ?? null
|
|
const client = new AuthorizationCode(this.config)
|
|
if (code === null && errorCode === null) {
|
|
const state = generateXSRFCookie(res)
|
|
const authUrl = client.authorizeURL({
|
|
scope: ["applications.commands", "webhook.incoming"],
|
|
redirect_uri: withoutParams.toString(),
|
|
state,
|
|
})
|
|
res.code(200)
|
|
res.type("text/html")
|
|
res.send(pug.renderFile(join("static/pages", this.templateFilename), {
|
|
authUrl,
|
|
baseUrl,
|
|
isReset: this.webhook.isPresent
|
|
}))
|
|
return
|
|
}
|
|
const validXSRF = checkAndClearXSRFCookie(req, res)
|
|
if (errorCode !== null || code === null) {
|
|
const errorDescription = getFirstValue(req.query.error_description) ?? null
|
|
const errorUrl = getFirstValue(req.query.error_uri)
|
|
return renderError({
|
|
baseUrl,
|
|
res,
|
|
code: 400,
|
|
error: errorDescription ?? errorCode ?? "There was no code or error present.",
|
|
context: "authorizing the application",
|
|
errorUrl,
|
|
buttonText: "Try Again",
|
|
buttonUrl: relative("/", new URL(req.url).pathname),
|
|
})
|
|
}
|
|
if (!validXSRF) {
|
|
return renderError({
|
|
baseUrl,
|
|
res,
|
|
code: 409,
|
|
error: "The state was incorrectly set - try restarting the authentication process.",
|
|
context: "processing the new access code",
|
|
buttonText: "Try Again",
|
|
buttonUrl: relative("/", new URL(req.url).pathname),
|
|
})
|
|
}
|
|
let token: DiscordWebhookToken
|
|
try {
|
|
token = await client.getToken({
|
|
code,
|
|
scope: ["applications.commands", "webhook.incoming"],
|
|
redirect_uri: getBaseUrl(req) + "setup/gameChannel"
|
|
}) as DiscordWebhookToken
|
|
} catch (e) {
|
|
return renderError({
|
|
baseUrl,
|
|
res: res,
|
|
error: e,
|
|
code: 400,
|
|
context: "exchanging the code you gave me for a token",
|
|
buttonText: "Try Again",
|
|
buttonUrl: relative("/", new URL(req.url).pathname),
|
|
})
|
|
}
|
|
if (!token.token.webhook) {
|
|
return renderError({
|
|
baseUrl,
|
|
res: res,
|
|
code: 400,
|
|
error: "The token did not contain a webhook.",
|
|
context: "processing the token I received",
|
|
buttonText: "Try Again",
|
|
buttonUrl: relative("/", new URL(req.url).pathname),
|
|
})
|
|
}
|
|
const wasSet = this.webhook.isPresent
|
|
try {
|
|
await this.webhook.replaceHook(token.token.webhook)
|
|
} catch (e) {
|
|
return renderError({
|
|
baseUrl,
|
|
res: res,
|
|
code: 500,
|
|
error: e,
|
|
context: `saving the new webhook${wasSet ? " and deleting the old one" : ""}`,
|
|
buttonText: "Try Again",
|
|
buttonUrl: relative("/", new URL(req.url).pathname),
|
|
})
|
|
}
|
|
res.redirect(this.destinationFunc())
|
|
}
|
|
} |