feat: log entitlement purchases (#3)
Code Analysis / SonarQube (push) Failing after 16s
Node.js CI / Lint and Test (push) Successful in 34s

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [x] I have pinned the dependencies to a specific patch version.

### Style

- [x] I have run the linter and resolved any errors.
- [x] My pull request uses an appropriate title, matching the conventional commit standards.
- [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #3
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #3.
This commit is contained in:
2025-07-06 13:48:17 -07:00
committed by Naomi Carrigan
parent 33e416af83
commit 06b77e93f5
7 changed files with 339 additions and 5 deletions
+76 -5
View File
@@ -5,12 +5,15 @@
*/
import fastify from "fastify";
import { applicationData } from "../config/applicationData.js";
import { auth } from "../modules/auth.js";
import { sendDiscord } from "../modules/discord.js";
import { sendMail } from "../modules/sendMail.js";
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 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";
@@ -90,7 +93,10 @@ export const instantiateServer = (): void => {
return;
}
const { application, context, stack, message } = request.body;
await sendMail(`[ERROR]: ${context} - ${application}`, `${message}\n\n${stack}`);
await sendMail(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n${stack}`,
);
await sendDiscord(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n\`\`\`\n${stack}\n\`\`\``,
@@ -115,13 +121,72 @@ export const instantiateServer = (): void => {
},
);
// 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;
// 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) => {
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}`,
`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}`,
);
},
);
server.listen({ port: 5003 }, (error) => {
const application = "Alert Server";
if (error) {
const { message, stack } = error;
const context = "Server Startup";
void sendMail(`[ERROR]: ${context} - ${application}`, `${message}\n\n${String(stack)}`);
void sendDiscord(`[ERROR]: ${context} - ${application}`, `${message}\n\n\`\`\`\n${String(stack)}\n\`\`\``);
void sendMail(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n${String(stack)}`,
);
void sendDiscord(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n\`\`\`\n${String(stack)}\n\`\`\``,
);
return;
}
const level = "debug";
@@ -134,7 +199,13 @@ export const instantiateServer = (): void => {
const context = "Server Startup";
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Totally being lazy.
const { message, stack } = error as Error;
void sendMail(`[ERROR]: ${context} - ${application}`, `${message}\n\n${stack}`);
void sendDiscord(`[ERROR]: ${context} - ${application}`, `${message}\n\n\`\`\`\n${stack}\n\`\`\``);
void sendMail(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n${stack}`,
);
void sendDiscord(
`[ERROR]: ${context} - ${application}`,
`${message}\n\n\`\`\`\n${stack}\n\`\`\``,
);
}
};