diff --git a/package.json b/package.json index 319d300..6a69f4e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "dependencies": { "discord-verify": "1.2.0", "fastify": "5.2.1", - "nodemailer": "6.10.0" + "fastify-raw-body": "^5.0.0", + "nodemailer": "6.10.0", + "stripe": "18.3.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79d9290..2e9e95c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,15 @@ importers: fastify: specifier: 5.2.1 version: 5.2.1 + fastify-raw-body: + specifier: ^5.0.0 + version: 5.0.0 nodemailer: specifier: 6.10.0 version: 6.10.0 + stripe: + specifier: 18.3.0 + version: 18.3.0(@types/node@22.13.1) devDependencies: '@nhcarrigan/eslint-config': specifier: 5.1.0 @@ -744,6 +750,10 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -856,6 +866,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1087,6 +1101,13 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + + fastify-raw-body@5.0.0: + resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==} + engines: {node: '>= 10'} + fastify@5.2.1: resolution: {integrity: sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==} @@ -1221,6 +1242,14 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1237,6 +1266,9 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1640,12 +1672,20 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1740,6 +1780,12 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@3.0.2: resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==} @@ -1771,6 +1817,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1834,6 +1883,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} @@ -1868,6 +1921,15 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stripe@18.3.0: + resolution: {integrity: sha512-FkxrTUUcWB4CVN2yzgsfF/YHD6WgYHduaa7VmokCy5TLCgl5UNJkwortxcedrxSavQ8Qfa4Ir4JxcbIYiBsyLg==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1909,6 +1971,10 @@ packages: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -1971,6 +2037,10 @@ packages: undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -2798,6 +2868,8 @@ snapshots: builtin-modules@3.3.0: {} + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.1: @@ -2906,6 +2978,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + depd@2.0.0: {} + dequal@2.0.3: {} dir-glob@3.0.1: @@ -3307,6 +3381,14 @@ snapshots: fast-uri@3.0.6: {} + fastify-plugin@5.0.1: {} + + fastify-raw-body@5.0.0: + dependencies: + fastify-plugin: 5.0.1 + raw-body: 3.0.0 + secure-json-parse: 2.7.0 + fastify@5.2.1: dependencies: '@fastify/ajv-compiler': 4.0.2 @@ -3465,6 +3547,18 @@ snapshots: hosted-git-info@2.8.9: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} import-fresh@3.3.1: @@ -3476,6 +3570,8 @@ snapshots: indent-string@4.0.0: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -3880,10 +3976,21 @@ snapshots: punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + react-is@16.13.1: {} react@19.0.0: {} @@ -4007,6 +4114,10 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + secure-json-parse@2.7.0: {} + secure-json-parse@3.0.2: {} semver@5.7.2: {} @@ -4039,6 +4150,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4108,6 +4221,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.8.0: {} string.prototype.matchall@4.0.12: @@ -4162,6 +4277,12 @@ snapshots: strip-json-comments@3.1.1: {} + stripe@18.3.0(@types/node@22.13.1): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 22.13.1 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4193,6 +4314,8 @@ snapshots: toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + ts-api-utils@1.4.3(typescript@5.7.3): dependencies: typescript: 5.7.3 @@ -4264,6 +4387,8 @@ snapshots: undici-types@6.20.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 diff --git a/prod.env b/prod.env index 8287cbe..0ea110f 100644 --- a/prod.env +++ b/prod.env @@ -2,4 +2,6 @@ MATRIX_ACCESS_TOKEN="op://Environment Variables - Naomi/Alert Server/matrix_acce MATRIX_ROOM_ID="op://Environment Variables - Naomi/Alert Server/matrix_room_id" API_AUTH="op://Environment Variables - Naomi/Alert Server/api_auth" EMAIL_PASSWORD="op://Environment Variables - Naomi/Alert Server/email_pass" -DISCORD_WEBHOOK_URL="op://Environment Variables - Naomi/Alert Server/discord_hook" \ No newline at end of file +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" \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 869e01a..9717be9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,4 @@ import { instantiateServer } from "./server/serve.js"; -instantiateServer(); +await instantiateServer(); diff --git a/src/server/serve.ts b/src/server/serve.ts index b6660c3..34d92bd 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -5,6 +5,9 @@ */ import fastify from "fastify"; +import rawBody from "fastify-raw-body"; +// eslint-disable-next-line @typescript-eslint/naming-convention -- We are importing a class. +import StripeApp from "stripe"; import { applicationData } from "../config/applicationData.js"; import { auth } from "../modules/auth.js"; import { sendDiscord } from "../modules/discord.js"; @@ -13,11 +16,14 @@ import { validateWebhook } from "../modules/validateWebhook.js"; import { errorSchema } from "../schemas/errorSchema.js"; import { logSchema } from "../schemas/logSchema.js"; import { uptimeSchema } from "../schemas/uptimeSchema.js"; +import { errorHandler } from "../utils/errorHandler.js"; import type { Entitlement } from "../interfaces/entitlement.js"; import type { Error } from "../interfaces/error.js"; import type { Log } from "../interfaces/log.js"; import type { Uptime } from "../interfaces/uptime.js"; +const stripe = new StripeApp(process.env.STRIPE_SECRET_KEY ?? ""); + const html = ` @@ -60,7 +66,7 @@ const html = ` * Starts up the server to receive events. */ // eslint-disable-next-line max-lines-per-function -- This function is long because it is setting up a server. -export const instantiateServer = (): void => { +export const instantiateServer = async(): Promise => { try { const server = fastify({ logger: false, @@ -73,14 +79,19 @@ export const instantiateServer = (): void => { // eslint-disable-next-line @typescript-eslint/naming-convention -- Body must be capitalised for Fastify. server.post<{ Body: Log }>("/log", logSchema, async(request, response) => { - if (!auth(request)) { - await response.status(401).send({ success: false }); - return; + try { + if (!auth(request)) { + await response.status(401).send({ success: false }); + return; + } + const { application, level, message } = request.body; + await sendMail(`[${level}]: ${application}`, message); + await sendDiscord(`[${level}]: ${application}`, message); + await response.status(200).send({ success: true }); + } catch (error) { + await errorHandler(error, "Log Webhook"); + await response.status(500).send({ success: false }); } - const { application, level, message } = request.body; - await sendMail(`[${level}]: ${application}`, message); - await sendDiscord(`[${level}]: ${application}`, message); - await response.status(200).send({ success: true }); }); // eslint-disable-next-line @typescript-eslint/naming-convention -- Body must be capitalised for Fastify. @@ -88,20 +99,25 @@ export const instantiateServer = (): void => { "/error", errorSchema, async(request, response) => { - if (!auth(request)) { - await response.status(401).send({ success: false }); - return; + try { + if (!auth(request)) { + await response.status(401).send({ success: false }); + return; + } + const { application, context, stack, message } = request.body; + await sendMail( + `[ERROR]: ${context} - ${application}`, + `${message}\n\n${stack}`, + ); + await sendDiscord( + `[ERROR]: ${context} - ${application}`, + `${message}\n\n\`\`\`\n${stack}\n\`\`\``, + ); + await response.status(200).send({ success: true }); + } catch (error) { + await errorHandler(error, "Error Webhook"); + await response.status(500).send({ success: false }); } - const { application, context, stack, message } = request.body; - await sendMail( - `[ERROR]: ${context} - ${application}`, - `${message}\n\n${stack}`, - ); - await sendDiscord( - `[ERROR]: ${context} - ${application}`, - `${message}\n\n\`\`\`\n${stack}\n\`\`\``, - ); - await response.status(200).send({ success: true }); }, ); @@ -110,14 +126,19 @@ export const instantiateServer = (): void => { "/uptime", uptimeSchema, async(request, response) => { - if (!auth(request)) { - await response.status(401).send({ success: false }); - return; + try { + if (!auth(request)) { + await response.status(401).send({ success: false }); + return; + } + const { application, message } = request.body; + await sendMail(`[UPTIME]: ${application}`, message); + await sendDiscord(`[UPTIME]: ${application}`, message); + await response.status(200).send({ success: true }); + } catch (error) { + await errorHandler(error, "Uptime Webhook"); + await response.status(500).send({ success: false }); } - const { application, message } = request.body; - await sendMail(`[UPTIME]: ${application}`, message); - await sendDiscord(`[UPTIME]: ${application}`, message); - await response.status(200).send({ success: true }); }, ); @@ -131,49 +152,120 @@ export const instantiateServer = (): void => { "/entitlement", // eslint-disable-next-line complexity -- Fuck off. async(request, response) => { - const { type, application_id: applicationId, event } = request.body; - const appInfo = applicationData[applicationId]; - const isValid = await validateWebhook(request); - if (!isValid) { - await response.status(401).send({ success: false }); - void sendDiscord( + try { + const { type, application_id: applicationId, event } = request.body; + const appInfo = applicationData[applicationId]; + const isValid = await validateWebhook(request); + if (!isValid) { + await response.status(401).send({ success: false }); + void sendDiscord( + `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + "An invalid webhook signature was received.", + ); + void sendMail( + `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + "An invalid webhook signature was received.", + ); + return; + } + await response.status(204).send(); + if (type === 0) { + void sendDiscord( + `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + "Received a ping from Discord.", + ); + void sendMail( + `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, + "Received a ping from Discord.", + ); + return; + } + const { + user_id: userId, + guild_id: guildId, + ends_at: endsAt, + } = event.data; + await sendDiscord( `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - "An invalid webhook signature was received.", + `Entitlement purchased!\n- **User ID**: ${userId}\n- **Guild ID**: ${guildId}\n- **Ends At**: ${endsAt}`, ); - void sendMail( + await sendMail( `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - "An invalid webhook signature was received.", + `Entitlement purchased!\n- **User ID**: ${userId}\n- **Guild ID**: ${guildId}\n- **Ends At**: ${endsAt}`, ); - return; + } catch (error) { + await errorHandler(error, "Entitlement Webhook"); + await response.status(500).send({ success: false }); } - await response.status(204).send(); - if (type === 0) { - void sendDiscord( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - "Received a ping from Discord.", - ); - void sendMail( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - "Received a ping from Discord.", - ); - return; - } - const { - user_id: userId, - guild_id: guildId, - ends_at: endsAt, - } = event.data; - await sendDiscord( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - `Entitlement purchased!\n- **User ID**: ${userId}\n- **Guild ID**: ${guildId}\n- **Ends At**: ${endsAt}`, - ); - await sendMail( - `[ENTITLEMENT]: ${appInfo?.name ?? applicationId}`, - `Entitlement purchased!\n- **User ID**: ${userId}\n- **Guild ID**: ${guildId}\n- **Ends At**: ${endsAt}`, - ); }, ); + await server.register(rawBody, { + encoding: false, + field: "rawBody", + global: true, + runFirst: true, + }); + + // eslint-disable-next-line max-lines-per-function, complexity, max-statements -- Lot of logic here. + server.post("/stripe", async(request, response) => { + 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; + }); + if (event === null) { + await response.status(400).send({ + error: "Invalid Stripe webhook signature.", + }); + void sendDiscord( + `[STRIPE]: Invalid Webhook Signature`, + `Received an invalid webhook signature from Stripe.\n- **Headers**: ${JSON.stringify(request.headers)}\n- **Body**: ${JSON.stringify(raw, null, 2)}`, + ); + 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)}`); + 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)}`); + } + 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)}`); + } + 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}`); + } + } catch (error) { + await errorHandler(error, "Stripe Webhook"); + await response.status(500).send({ error: "Internal Server Error" }); + } + }); + server.listen({ port: 5003 }, (error) => { const application = "Alert Server"; if (error) { diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 0000000..78eae26 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,32 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { sendDiscord } from "../modules/discord.js"; +import { sendMail } from "../modules/sendMail.js"; + +/** + * Forwards an error to the Discord webhook and email. + * @param error - The error to forward. + * @param context - The context in which the error occurred, for logging purposes. + */ +export const errorHandler = async( + error: unknown, + context: string, +): Promise => { + if (error instanceof Error) { + await sendDiscord( + `[ERROR] ${context}: ${error.message}`, + JSON.stringify(error, null, 2), + ); + await sendMail( + `[ERROR] ${context}: ${error.message}`, + JSON.stringify(error, null, 2), + ); + return; + } + await sendDiscord(`[ERROR] ${context}`, JSON.stringify(error, null, 2)); + await sendMail(`[ERROR] ${context}`, JSON.stringify(error, null, 2)); +};