From 10e3a58d6b77d05bbd7c8fcfe9466a4463198a14 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 25 Sep 2025 10:51:51 -0700 Subject: [PATCH] feat: migrate to dedicated log platform --- prod.env | 3 +- src/modules/auth.ts | 3 + src/modules/discord.ts | 51 ---------- src/modules/pipeLog.ts | 41 ++++++++ src/modules/validateWebhook.ts | 15 ++- src/server/serve.ts | 165 ++++++++++++++++++++++----------- 6 files changed, 168 insertions(+), 110 deletions(-) delete mode 100644 src/modules/discord.ts create mode 100644 src/modules/pipeLog.ts diff --git a/prod.env b/prod.env index 252b16a..38e299c 100644 --- a/prod.env +++ b/prod.env @@ -5,4 +5,5 @@ EMAIL_PASSWORD="op://Environment Variables - Naomi/Alert Server/email_pass" DISCORD_WEBHOOK_URL="op://Environment Variables - Naomi/Alert Server/discord_hook" STRIPE_SECRET_KEY="op://Environment Variables - Naomi/Alert Server/stripe" STRIPE_WEBHOOK_SECRET="op://Environment Variables - Naomi/Alert Server/stripe_webhook" -DISCORD_TOKEN="op://Environment Variables - Naomi/Alert Server/discord_token" \ No newline at end of file +DISCORD_TOKEN="op://Environment Variables - Naomi/Alert Server/discord_token" +LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/log_token" \ No newline at end of file diff --git a/src/modules/auth.ts b/src/modules/auth.ts index b86c06e..9a436e5 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -17,6 +17,9 @@ export const auth = (request: FastifyRequest): boolean => { return false; } const token = request.headers.authorization; + if (token === "" || process.env.API_AUTH === undefined) { + return false; + } if (token !== process.env.API_AUTH) { return false; } diff --git a/src/modules/discord.ts b/src/modules/discord.ts deleted file mode 100644 index 91ec22a..0000000 --- a/src/modules/discord.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ - -/** - * Sends a Discord webhook notification. - * @param subject - The subject of the email. - * @param body - The text content of the email. - */ -export const sendDiscord = async( - subject: string, - body: string, -): Promise => { - await fetch(`https://discord.com/api/v10/channels/1355232348840394785/messages`, { - body: JSON.stringify({ - components: [ - { - // eslint-disable-next-line @typescript-eslint/naming-convention -- Needs to match Discord's structure. - accent_color: 15_418_782, - components: [ - { - content: `# ${subject}`, - type: 10, - }, - { - divider: true, - spacing: 1, - type: 14, - }, - { - content: body, - type: 10, - }, - ], - spoiler: false, - type: 17, - }, - ], - flags: 32_768, - }), - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention -- Needs to match Discord's structure. - "Authorization": `Bot ${process.env.DISCORD_TOKEN ?? ""}`, - // eslint-disable-next-line @typescript-eslint/naming-convention -- Needs to match Discord's structure. - "Content-Type": "application/json", - }, - method: "POST", - }); -}; diff --git a/src/modules/pipeLog.ts b/src/modules/pipeLog.ts new file mode 100644 index 0000000..61dd023 --- /dev/null +++ b/src/modules/pipeLog.ts @@ -0,0 +1,41 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +const priority: Record = { + debug: 0, + error: 3, + info: 1, + warn: 2, +}; + +/** + * Pipes a log message to the Gotify server. + * @param appName - The name of the application. + * @param message - The message to log. + * @param level - The level of the log, used for priority. + */ +export const pipeLog = async( + appName: string, + message: string, + level: string, +): Promise => { + const logToken = process.env.LOG_TOKEN; + if (logToken === undefined) { + return; + } + await fetch(`https://graylog.nhcarrigan.com/message?token=${logToken}`, { + body: JSON.stringify({ + message: message, + priority: priority[level] ?? priority.debug, + title: appName, + }), + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header. + "Content-Type": "application/json", + }, + method: "POST", + }); +}; diff --git a/src/modules/validateWebhook.ts b/src/modules/validateWebhook.ts index 030b640..65c897a 100644 --- a/src/modules/validateWebhook.ts +++ b/src/modules/validateWebhook.ts @@ -6,7 +6,7 @@ import { webcrypto } from "node:crypto"; import { verify } from "discord-verify/node"; import { applicationData } from "../config/applicationData.js"; -import { sendDiscord } from "./discord.js"; +import { pipeLog } from "./pipeLog.js"; import type { Entitlement } from "../interfaces/entitlement.js"; import type { FastifyRequest } from "fastify"; @@ -27,7 +27,11 @@ export const validateWebhook = async( const { application_id: applicationId } = request.body; const appData = applicationData[applicationId]; if (appData === undefined) { - void sendDiscord(`[NOTIFICATION]: Invalid Application ID`, `Received an entitlement event for an invalid application ID: ${applicationId}`); + await pipeLog( + "Alert Server", + `Received an entitlement event for an invalid application ID: ${applicationId}`, + "warn", + ); return false; } const signature = request.headers["x-signature-ed25519"]; @@ -41,9 +45,10 @@ export const validateWebhook = async( webcrypto.subtle, ); if (!isValid) { - void sendDiscord( - `[NOTIFICATION]: Invalid Webhook Signature`, - `Received an entitlement event with an invalid signature.\nApplication ID: ${applicationId}\nSignature: ${signature}\nTimestamp: ${timestamp}\nRaw Body: ${rawBody}`, + await pipeLog( + "Alert Server", + `Received an entitlement event with an invalid signature for application ${appData.name} (${applicationId}).`, + "warn", ); } return isValid; diff --git a/src/server/serve.ts b/src/server/serve.ts index b20f6d3..33265d9 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -10,7 +10,7 @@ import rawBody from "fastify-raw-body"; import StripeApp from "stripe"; import { applicationData } from "../config/applicationData.js"; import { auth } from "../modules/auth.js"; -import { sendDiscord } from "../modules/discord.js"; +import { pipeLog } from "../modules/pipeLog.js"; import { sendMail } from "../modules/sendMail.js"; import { validateWebhook } from "../modules/validateWebhook.js"; import { errorSchema } from "../schemas/errorSchema.js"; @@ -85,7 +85,7 @@ export const instantiateServer = async(): Promise => { return; } const { application, level, message } = request.body; - await sendDiscord(`[${level}]: ${application}`, message); + await pipeLog(application, message, level); await response.status(200).send({ success: true }); } catch (error) { await errorHandler(error, "Log Webhook"); @@ -108,9 +108,10 @@ export const instantiateServer = async(): Promise => { `[ERROR]: ${context} - ${application}`, `${message}\n\n${stack}`, ); - await sendDiscord( - `[ERROR]: ${context} - ${application}`, - `${message}\n\n\`\`\`\n${stack}\n\`\`\``, + await pipeLog( + application, + `${context} - ${message}\n${stack}`, + "error", ); await response.status(200).send({ success: true }); } catch (error) { @@ -132,7 +133,7 @@ export const instantiateServer = async(): Promise => { } const { application, message } = request.body; await sendMail(`[UPTIME]: ${application}`, message); - await sendDiscord(`[UPTIME]: ${application}`, message); + await pipeLog(application, message, "info"); await response.status(200).send({ success: true }); } catch (error) { await errorHandler(error, "Uptime Webhook"); @@ -141,13 +142,17 @@ export const instantiateServer = async(): Promise => { }, ); - // eslint-disable-next-line @typescript-eslint/naming-convention -- Body must be capitalised for Fastify. - server.post<{ Body: Entitlement; Headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention -- Header must be formatted for Fastify. - "x-signature-ed25519": string; + server.post<{ // eslint-disable-next-line @typescript-eslint/naming-convention -- Header must be formatted for Fastify. - "x-signature-timestamp": string; - }; }>( + Body: Entitlement; + // eslint-disable-next-line @typescript-eslint/naming-convention -- Header must be formatted for Fastify. + Headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Header must be formatted for Fastify. + "x-signature-ed25519": string; + // eslint-disable-next-line @typescript-eslint/naming-convention -- Header must be formatted for Fastify. + "x-signature-timestamp": string; + }; + }>( "/entitlement", // eslint-disable-next-line complexity -- Fuck off. async(request, response) => { @@ -157,9 +162,10 @@ export const instantiateServer = async(): Promise => { const isValid = await validateWebhook(request); if (!isValid) { await response.status(401).send({ success: false }); - void sendDiscord( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + await pipeLog( + appInfo?.name ?? applicationId, "An invalid webhook signature was received.", + "error", ); void sendMail( `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, @@ -169,9 +175,10 @@ export const instantiateServer = async(): Promise => { } await response.status(204).send(); if (type === 0) { - void sendDiscord( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + await pipeLog( + appInfo?.name ?? applicationId, "Received a ping from Discord.", + "info", ); void sendMail( `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, @@ -184,9 +191,10 @@ export const instantiateServer = async(): Promise => { guild_id: guildId, ends_at: endsAt, } = event.data; - await sendDiscord( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + await pipeLog( + appInfo?.name ?? applicationId, `Entitlement purchased!\n- **User ID**: ${userId}\n- **Guild ID**: ${guildId}\n- **Ends At**: ${endsAt}`, + "info", ); await sendMail( `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, @@ -211,53 +219,109 @@ export const instantiateServer = async(): Promise => { try { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Nah fam. const raw = request.rawBody as Buffer; - const event = await stripe.webhooks.constructEventAsync( - raw, - request.headers["stripe-signature"] ?? "", - process.env.STRIPE_WEBHOOK_SECRET ?? "", - ).catch(() => { - return null; - }); + const event = await stripe.webhooks. + constructEventAsync( + raw, + request.headers["stripe-signature"] ?? "", + process.env.STRIPE_WEBHOOK_SECRET ?? "", + ). + catch(() => { + return null; + }); if (event === null) { await response.status(400).send({ error: "Invalid Stripe webhook signature.", }); - void sendDiscord( + await pipeLog( `[STRIPE]: Invalid Webhook Signature`, - `Received an invalid webhook signature from Stripe.\n- **Headers**: ${JSON.stringify(request.headers)}\n- **Body**: ${JSON.stringify(raw, null, 2)}`, + `Received an invalid webhook signature from Stripe.\n- **Headers**: ${JSON.stringify( + request.headers, + )}\n- **Body**: ${JSON.stringify(raw, null, 2)}`, + "error", ); return; } await response.status(200).send({ received: true }); if (event.type === "checkout.session.completed") { const checkoutSessionCompleted = event.data.object; - await sendDiscord(`[STRIPE]: Checkout Session Completed`, `A checkout session has been completed.\n - **ITEMS**: ${checkoutSessionCompleted.line_items?.data.map((datum) => { - return `${datum.description ?? "unknown"} (${String(datum.quantity)})`; - }).join(", ") ?? "unknown"}\n- **TOTAL**: ${String(checkoutSessionCompleted.amount_total)}`); - await sendMail(`[STRIPE]: Checkout Session Completed`, `A checkout session has been completed.\n - **ITEMS**: ${checkoutSessionCompleted.line_items?.data.map((datum) => { - return `${datum.description ?? "unknown"} (${String(datum.quantity)})`; - }).join(", ") ?? "unknown"}\n- **TOTAL**: ${String(checkoutSessionCompleted.amount_total)}`); + await pipeLog( + "Stripe", + `A checkout session has been completed.\n - **ITEMS**: ${ + checkoutSessionCompleted.line_items?.data. + map((datum) => { + return `${datum.description ?? "unknown"} (${String( + datum.quantity, + )})`; + }). + join(", ") ?? "unknown" + }\n- **TOTAL**: ${String(checkoutSessionCompleted.amount_total)}`, + "info", + ); + await sendMail( + `[STRIPE]: Checkout Session Completed`, + `A checkout session has been completed.\n - **ITEMS**: ${ + checkoutSessionCompleted.line_items?.data. + map((datum) => { + return `${datum.description ?? "unknown"} (${String( + datum.quantity, + )})`; + }). + join(", ") ?? "unknown" + }\n- **TOTAL**: ${String(checkoutSessionCompleted.amount_total)}`, + ); return; } if (event.type === "invoice.paid") { const invoicePaid = event.data.object; - await sendDiscord(`[STRIPE]: Invoice Paid`, `An invoice has been paid.\n - **ITEMS**: ${invoicePaid.lines.data.map((datum) => { - return `${datum.description ?? "unknown"} (${String(datum.quantity)})`; - }).join(", ")}\n- **TOTAL**: ${String(invoicePaid.amount_paid)}`); - await sendMail(`[STRIPE]: Invoice Paid`, `An invoice has been paid.\n - **ITEMS**: ${invoicePaid.lines.data.map((datum) => { - return `${datum.description ?? "unknown"} (${String(datum.quantity)})`; - }).join(", ")}\n- **TOTAL**: ${String(invoicePaid.amount_paid)}`); + await pipeLog( + "Stripe", + `An invoice has been paid.\n - **ITEMS**: ${invoicePaid.lines.data. + map((datum) => { + return `${datum.description ?? "unknown"} (${String( + datum.quantity, + )})`; + }). + join(", ")}\n- **TOTAL**: ${String(invoicePaid.amount_paid)}`, + "info", + ); + await sendMail( + `[STRIPE]: Invoice Paid`, + `An invoice has been paid.\n - **ITEMS**: ${invoicePaid.lines.data. + map((datum) => { + return `${datum.description ?? "unknown"} (${String( + datum.quantity, + )})`; + }). + join(", ")}\n- **TOTAL**: ${String(invoicePaid.amount_paid)}`, + ); } if (event.type === "payment_intent.succeeded") { const paymentIntentSucceeded = event.data.object; - await sendDiscord(`[STRIPE]: Payment Intent Succeeded`, `A payment intent has succeeded.\n- **AMOUNT**: ${String(paymentIntentSucceeded.amount)}`); - await sendMail(`[STRIPE]: Payment Intent Succeeded`, `A payment intent has succeeded.\n- **AMOUNT**: ${String(paymentIntentSucceeded.amount)}`); + await pipeLog( + "Stripe", + `A payment intent has succeeded.\n- **AMOUNT**: ${String( + paymentIntentSucceeded.amount, + )}`, + "info", + ); + await sendMail( + `[STRIPE]: Payment Intent Succeeded`, + `A payment intent has succeeded.\n- **AMOUNT**: ${String( + paymentIntentSucceeded.amount, + )}`, + ); } if (event.type === "subscription_schedule.completed") { const subscriptionScheduleCompleted = event.data.object; - // Then define and call a function to handle the event subscription_schedule.completed - await sendDiscord(`[STRIPE]: Subscription Completed`, `A subscription has been completed.\n- **ID**: ${subscriptionScheduleCompleted.id}`); - await sendMail(`[STRIPE]: Subscription Completed`, `A subscription has been completed.\n- **ID**: ${subscriptionScheduleCompleted.id}`); + await pipeLog( + "Stripe", + `A subscription has been completed.\n- **ID**: ${subscriptionScheduleCompleted.id}`, + "info", + ); + await sendMail( + `[STRIPE]: Subscription Completed`, + `A subscription has been completed.\n- **ID**: ${subscriptionScheduleCompleted.id}`, + ); } } catch (error) { await errorHandler(error, "Stripe Webhook"); @@ -266,6 +330,7 @@ export const instantiateServer = async(): Promise => { }); server.listen({ port: 5003 }, (error) => { + // eslint-disable-next-line max-lines -- This block is long because of logging. const application = "Alert Server"; if (error) { const { message, stack } = error; @@ -274,16 +339,13 @@ export const instantiateServer = async(): Promise => { `[ERROR]: ${context} - ${application}`, `${message}\n\n${String(stack)}`, ); - void sendDiscord( - `[ERROR]: ${context} - ${application}`, - `${message}\n\n\`\`\`\n${String(stack)}\n\`\`\``, - ); + void pipeLog(application, `${message}\n\n${String(stack)}`, "error"); return; } const level = "debug"; const message = `Server listening on port 5003.`; void sendMail(`[${level}]: ${application}`, message); - void sendDiscord(`[${level}]: ${application}`, message); + void pipeLog(application, message, level); }); } catch (error) { const application = "Alert Server"; @@ -294,9 +356,6 @@ export const instantiateServer = async(): Promise => { `[ERROR]: ${context} - ${application}`, `${message}\n\n${stack}`, ); - void sendDiscord( - `[ERROR]: ${context} - ${application}`, - `${message}\n\n\`\`\`\n${stack}\n\`\`\``, - ); + void pipeLog(application, `${message}\n\n${stack}`, "error"); } };