/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { blockedIps } from "../cache/blockedIps.js"; import { database } from "../db/database.js"; import { getIpFromRequest } from "../modules/getIpFromRequest.js"; import { getSanctionComponents } from "../modules/getSanctionComponents.js"; import { isValidString } from "../utils/typeguards.js"; import type { FastifyPluginAsync } from "fastify"; const oneDay = 24 * 60 * 60 * 1000; /** * Mounts the entry routes for the application. These routes * should not require CORS, as they are used by external services * such as our uptime monitor. * @param server - The Fastify server instance. */ export const sanctionRoutes: FastifyPluginAsync = async(server) => { server.get("/sanctions", async(_request, reply) => { const sanctions = await database.getInstance().sanctions.findMany({ orderBy: { createdAt: "desc", }, take: 100, }); return await reply.status(200).type("application/json"). send(sanctions.map((sanction) => { return { number: sanction.number, platform: sanction.platform, reason: sanction.reason, type: sanction.type, username: sanction.username, uuid: sanction.uuid, }; })); }); // eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body. server.post<{ Body: { platform: string; uuid: string; username: string; type: string; reason: string; }; }>( "/sanction", async(request, reply) => { const token = request.headers.authorization; if (token === undefined || token !== process.env.ANNOUNCEMENT_TOKEN) { blockedIps.push({ ip: getIpFromRequest(request), ttl: new Date(Date.now() + oneDay), }); return await reply.status(401).send({ error: // eslint-disable-next-line stylistic/max-len -- Big boi string. "This endpoint requires a special auth token. If you believe you should have access, please contact Naomi. To protect our services, your IP has been blocked from all routes for 24 hours.", }); } const { platform, uuid, username, type, reason } = request.body; if ( [ platform, uuid, username, type, reason ].some((value) => { return !isValidString(value); }) ) { return await reply.status(400).send({ error: "Missing required fields.", }); } if (![ "warning", "kick", "mute", "ban", ].includes(type)) { return await reply.status(400).send({ error: "Invalid type. Choose from warning, kick, mute, ban.", }); } const count = await database.getInstance().sanctions.count(); const number = count + 1; await database.getInstance().sanctions.create({ data: { number, platform, reason, type, username, uuid, }, }); const components = getSanctionComponents( { number, platform, reason, type, username, uuid, }, ); await fetch( `${process.env.SANCTION_WEBHOOK ?? ""}?with_components=true`, { body: JSON.stringify({ components: components, flags: 32_768, }), headers: { // eslint-disable-next-line @typescript-eslint/naming-convention -- headers. "content-type": "application/json", }, method: "POST", }, ); return await reply.status(201).send({ message: `Sanction ${number.toString()} has been logged!`, }); }, ); };