mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-19 23:06:15 +08:00
parent
e655aa741a
commit
b06b9f73e5
2
.github/workflows/monkey-ci.yml
vendored
2
.github/workflows/monkey-ci.yml
vendored
|
@ -134,7 +134,7 @@ jobs:
|
|||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- name: Install prettier
|
||||
run: pnpm add -g prettier@2.5.1
|
||||
run: pnpm add -g prettier@2.8.8
|
||||
|
||||
- name: Get changed files
|
||||
if: github.event_name == 'pull_request'
|
||||
|
|
29
backend/__tests__/__testData__/rate-limit.ts
Normal file
29
backend/__tests__/__testData__/rate-limit.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { REQUEST_MULTIPLIER } from "../../src/middlewares/rate-limit";
|
||||
import { MatcherResult, ExpectedRateLimit } from "../vitest";
|
||||
import { Test as SuperTest } from "supertest";
|
||||
|
||||
export function enableRateLimitExpects(): void {
|
||||
expect.extend({
|
||||
toBeRateLimited: async (
|
||||
received: SuperTest,
|
||||
expected: ExpectedRateLimit
|
||||
): Promise<MatcherResult> => {
|
||||
const now = Date.now();
|
||||
const { headers } = await received.expect(200);
|
||||
|
||||
const max =
|
||||
parseInt(headers["x-ratelimit-limit"] as string) / REQUEST_MULTIPLIER;
|
||||
const windowMs =
|
||||
parseInt(headers["x-ratelimit-reset"] as string) * 1000 - now;
|
||||
|
||||
return {
|
||||
pass:
|
||||
max === expected.max && Math.abs(expected.windowMs - windowMs) < 2500,
|
||||
message: () =>
|
||||
"Rate limit max not matching or windowMs is off by more then 2500ms",
|
||||
actual: { max, windowMs },
|
||||
expected: expected,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
|
@ -8,12 +8,14 @@ import * as ReportDal from "../../../src/dal/report";
|
|||
import GeorgeQueue from "../../../src/queues/george-queue";
|
||||
import * as AuthUtil from "../../../src/utils/auth";
|
||||
import _ from "lodash";
|
||||
import { enableRateLimitExpects } from "../../__testData__/rate-limit";
|
||||
|
||||
const mockApp = request(app);
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
const uid = new ObjectId().toHexString();
|
||||
enableRateLimitExpects();
|
||||
|
||||
describe("ApeKeyController", () => {
|
||||
describe("AdminController", () => {
|
||||
const isAdminMock = vi.spyOn(AdminUuidDal, "isAdmin");
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -50,6 +52,11 @@ describe("ApeKeyController", () => {
|
|||
mockApp.get("/admin").set("authorization", `Uid ${uid}`)
|
||||
);
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
await expect(
|
||||
mockApp.get("/admin").set("authorization", `Uid ${uid}`)
|
||||
).toBeRateLimited({ max: 1, windowMs: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggle ban", () => {
|
||||
|
@ -167,6 +174,22 @@ describe("ApeKeyController", () => {
|
|||
.set("authorization", `Uid ${uid}`)
|
||||
);
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
//GIVEN
|
||||
const victimUid = new ObjectId().toHexString();
|
||||
getUserMock.mockResolvedValue({
|
||||
banned: false,
|
||||
discordId: "discordId",
|
||||
} as any);
|
||||
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp
|
||||
.post("/admin/toggleBan")
|
||||
.send({ uid: victimUid })
|
||||
.set("authorization", `Uid ${uid}`)
|
||||
).toBeRateLimited({ max: 1, windowMs: 5000 });
|
||||
});
|
||||
});
|
||||
describe("accept reports", () => {
|
||||
const getReportsMock = vi.spyOn(ReportDal, "getReports");
|
||||
|
@ -269,6 +292,18 @@ describe("ApeKeyController", () => {
|
|||
.set("authorization", `Uid ${uid}`)
|
||||
);
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
//GIVEN
|
||||
getReportsMock.mockResolvedValue([{ id: "1", reason: "one" } as any]);
|
||||
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp
|
||||
.post("/admin/report/accept")
|
||||
.send({ reports: [{ reportId: "1" }] })
|
||||
.set("authorization", `Uid ${uid}`)
|
||||
).toBeRateLimited({ max: 1, windowMs: 5000 });
|
||||
});
|
||||
});
|
||||
describe("reject reports", () => {
|
||||
const getReportsMock = vi.spyOn(ReportDal, "getReports");
|
||||
|
@ -374,6 +409,18 @@ describe("ApeKeyController", () => {
|
|||
.set("authorization", `Uid ${uid}`)
|
||||
);
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
//GIVEN
|
||||
getReportsMock.mockResolvedValue([{ id: "1", reason: "one" } as any]);
|
||||
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp
|
||||
.post("/admin/report/reject")
|
||||
.send({ reports: [{ reportId: "1" }] })
|
||||
.set("authorization", `Uid ${uid}`)
|
||||
).toBeRateLimited({ max: 1, windowMs: 5000 });
|
||||
});
|
||||
});
|
||||
describe("send forgot password email", () => {
|
||||
const sendForgotPasswordEmailMock = vi.spyOn(
|
||||
|
@ -405,6 +452,15 @@ describe("ApeKeyController", () => {
|
|||
"meowdec@example.com"
|
||||
);
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp
|
||||
.post("/admin/sendForgotPasswordEmail")
|
||||
.send({ email: "meowdec@example.com" })
|
||||
.set("authorization", `Uid ${uid}`)
|
||||
).toBeRateLimited({ max: 1, windowMs: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
async function expectFailForNonAdmin(call: SuperTest): Promise<void> {
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as AuthUtils from "../../../src/utils/auth";
|
|||
import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { mockAuthenticateWithApeKey } from "../../__testData__/auth";
|
||||
import { enableRateLimitExpects } from "../../__testData__/rate-limit";
|
||||
const uid = "123456";
|
||||
|
||||
const mockDecodedToken: DecodedIdToken = {
|
||||
|
@ -20,6 +21,7 @@ const mockDecodedToken: DecodedIdToken = {
|
|||
const mockApp = request(app);
|
||||
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
enableRateLimitExpects();
|
||||
|
||||
describe("result controller test", () => {
|
||||
const verifyIdTokenMock = vi.spyOn(AuthUtils, "verifyIdToken");
|
||||
|
@ -322,6 +324,21 @@ describe("result controller test", () => {
|
|||
expect(body.data[1]).not.toHaveProperty("correctChars");
|
||||
expect(body.data[1]).not.toHaveProperty("incorrectChars");
|
||||
});
|
||||
it("should be rate limited", async () => {
|
||||
await expect(
|
||||
mockApp.get("/results").set("Authorization", `Bearer ${uid}`)
|
||||
).toBeRateLimited({ max: 60, windowMs: 60 * 60 * 1000 });
|
||||
});
|
||||
it("should be rate limited for ape keys", async () => {
|
||||
//GIVEN
|
||||
await acceptApeKeys(true);
|
||||
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
|
||||
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp.get("/results").set("Authorization", `ApeKey ${apeKey}`)
|
||||
).toBeRateLimited({ max: 30, windowMs: 24 * 60 * 60 * 1000 });
|
||||
});
|
||||
});
|
||||
describe("getLastResult", () => {
|
||||
const getLastResultMock = vi.spyOn(ResultDal, "getLastResult");
|
||||
|
@ -385,6 +402,22 @@ describe("result controller test", () => {
|
|||
expect(body.data).not.toHaveProperty("correctChars");
|
||||
expect(body.data).not.toHaveProperty("incorrectChars");
|
||||
});
|
||||
it("should rate limit get last result with ape key", async () => {
|
||||
//GIVEN
|
||||
const result = givenDbResult(uid, {
|
||||
charStats: undefined,
|
||||
incorrectChars: 5,
|
||||
correctChars: 12,
|
||||
});
|
||||
getLastResultMock.mockResolvedValue(result);
|
||||
await acceptApeKeys(true);
|
||||
const apeKey = await mockAuthenticateWithApeKey(uid, await configuration);
|
||||
|
||||
//WHEN
|
||||
await expect(
|
||||
mockApp.get("/results/last").set("Authorization", `ApeKey ${apeKey}`)
|
||||
).toBeRateLimited({ max: 30, windowMs: 60 * 1000 }); //should use defaultApeRateLimit
|
||||
});
|
||||
});
|
||||
describe("deleteAll", () => {
|
||||
const deleteAllMock = vi.spyOn(ResultDal, "deleteAll");
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Collection, Db, MongoClient, WithId } from "mongodb";
|
|||
import { afterAll, beforeAll, afterEach } from "vitest";
|
||||
import * as MongoDbMock from "vitest-mongodb";
|
||||
import { MongoDbMockConfig } from "./global-setup";
|
||||
import { enableRateLimitExpects } from "./__testData__/rate-limit";
|
||||
|
||||
process.env["MODE"] = "dev";
|
||||
//process.env["MONGOMS_DISTRO"] = "ubuntu-22.04";
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
"ts-node": {
|
||||
"files": true
|
||||
},
|
||||
"files": ["../src/types/types.d.ts"],
|
||||
"files": ["../src/types/types.d.ts", "vitest.d.ts"],
|
||||
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
|
||||
}
|
||||
|
|
24
backend/__tests__/vitest.d.ts
vendored
Normal file
24
backend/__tests__/vitest.d.ts
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { Assertion, AsymmetricMatchersContaining } from "vitest";
|
||||
import type { Test as SuperTest } from "supertest";
|
||||
|
||||
type ExpectedRateLimit = {
|
||||
/** max calls */
|
||||
max: number;
|
||||
/** window in milliseconds. Needs to be within 2500ms */
|
||||
windowMs: number;
|
||||
};
|
||||
interface RestRequestMatcher<R = Supertest> {
|
||||
toBeRateLimited: (expected: ExpectedRateLimit) => RestRequestMatcher<R>;
|
||||
}
|
||||
|
||||
declare module "vitest" {
|
||||
interface Assertion<T = any> extends RestRequestMatcher<T> {}
|
||||
interface AsymmetricMatchersContaining extends RestRequestMatcher {}
|
||||
}
|
||||
|
||||
interface MatcherResult {
|
||||
pass: boolean;
|
||||
message: () => string;
|
||||
actual?: unknown;
|
||||
expected?: unknown;
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
"date-fns": "3.6.0",
|
||||
"dotenv": "16.4.5",
|
||||
"express": "4.19.2",
|
||||
"express-rate-limit": "6.2.1",
|
||||
"express-rate-limit": "7.4.0",
|
||||
"firebase-admin": "12.0.0",
|
||||
"helmet": "4.6.0",
|
||||
"ioredis": "4.28.5",
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
import { generateOpenApi } from "@ts-rest/open-api";
|
||||
import { contract } from "@monkeytype/contracts/index";
|
||||
import { writeFileSync, mkdirSync } from "fs";
|
||||
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
|
||||
import {
|
||||
ApeKeyRateLimit,
|
||||
EndpointMetadata,
|
||||
} from "@monkeytype/contracts/schemas/api";
|
||||
import type { OpenAPIObject } from "openapi3-ts";
|
||||
import {
|
||||
getLimits,
|
||||
limits,
|
||||
RateLimit,
|
||||
Window,
|
||||
} from "@monkeytype/contracts/rate-limit/index";
|
||||
import { formatDuration } from "date-fns";
|
||||
|
||||
type SecurityRequirementObject = {
|
||||
[name: string]: string[];
|
||||
|
@ -130,11 +140,19 @@ export function getOpenApi(): OpenAPIObject {
|
|||
{
|
||||
jsonQuery: true,
|
||||
setOperationId: "concatenated-path",
|
||||
operationMapper: (operation, route) => ({
|
||||
...operation,
|
||||
...addAuth(route.metadata as EndpointMetadata),
|
||||
...addTags(route.metadata as EndpointMetadata),
|
||||
}),
|
||||
operationMapper: (operation, route) => {
|
||||
const metadata = route.metadata as EndpointMetadata;
|
||||
|
||||
addRateLimit(operation, metadata);
|
||||
|
||||
const result = {
|
||||
...operation,
|
||||
...addAuth(metadata),
|
||||
...addTags(metadata),
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
);
|
||||
return openApiDocument;
|
||||
|
@ -167,6 +185,62 @@ function addTags(metadata: EndpointMetadata | undefined): object {
|
|||
};
|
||||
}
|
||||
|
||||
function addRateLimit(operation, metadata: EndpointMetadata | undefined): void {
|
||||
if (metadata === undefined || metadata.rateLimit === undefined) return;
|
||||
const okResponse = operation.responses["200"];
|
||||
if (okResponse === undefined) return;
|
||||
|
||||
if (!operation.description.trim().endsWith(".")) operation.description += ".";
|
||||
|
||||
operation.description += getRateLimitDescription(metadata.rateLimit);
|
||||
|
||||
okResponse["headers"] = {
|
||||
...okResponse["headers"],
|
||||
"x-ratelimit-limit": {
|
||||
schema: { type: "integer" },
|
||||
description: "The number of allowed requests in the current period",
|
||||
},
|
||||
"x-ratelimit-remaining": {
|
||||
schema: { type: "integer" },
|
||||
description: "The number of remaining requests in the current period",
|
||||
},
|
||||
"x-ratelimit-reset": {
|
||||
schema: { type: "integer" },
|
||||
description: "The timestamp of the start of the next period",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getRateLimitDescription(limit: RateLimit | ApeKeyRateLimit): string {
|
||||
const limits = getLimits(limit);
|
||||
|
||||
let result = ` This operation can be called up to ${
|
||||
limits.limiter.max
|
||||
} times ${formatWindow(limits.limiter.window)} for regular users`;
|
||||
|
||||
if (limits.apeKeyLimiter !== undefined) {
|
||||
result += ` and up to ${limits.apeKeyLimiter.max} times ${formatWindow(
|
||||
limits.apeKeyLimiter.window
|
||||
)} with ApeKeys`;
|
||||
}
|
||||
|
||||
return result + ".";
|
||||
}
|
||||
|
||||
function formatWindow(window: Window): string {
|
||||
if (typeof window === "number") {
|
||||
const seconds = Math.floor(window / 1000);
|
||||
const duration = formatDuration({
|
||||
hours: Math.floor(seconds / 3600),
|
||||
minutes: Math.floor(seconds / 60) % 60,
|
||||
seconds: seconds % 60,
|
||||
});
|
||||
|
||||
return `every ${duration}`;
|
||||
}
|
||||
return "per " + window;
|
||||
}
|
||||
|
||||
//detect if we run this as a main
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// import joi from "joi";
|
||||
import { adminLimit } from "../../middlewares/rate-limit";
|
||||
|
||||
import * as AdminController from "../controllers/admin";
|
||||
|
||||
import { adminContract } from "@monkeytype/contracts/admin";
|
||||
|
@ -9,8 +9,6 @@ import { checkIfUserIsAdmin } from "../../middlewares/permission";
|
|||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
const commonMiddleware = [
|
||||
adminLimit,
|
||||
|
||||
validate({
|
||||
criteria: (configuration) => {
|
||||
return configuration.admin.endpointsEnabled;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { apeKeysContract } from "@monkeytype/contracts/ape-keys";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ApeKeyController from "../controllers/ape-key";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
import { checkUserPermissions } from "../../middlewares/permission";
|
||||
|
@ -24,19 +23,19 @@ const commonMiddleware = [
|
|||
const s = initServer();
|
||||
export default s.router(apeKeysContract, {
|
||||
get: {
|
||||
middleware: [...commonMiddleware, RateLimit.apeKeysGet],
|
||||
middleware: commonMiddleware,
|
||||
handler: async (r) => callController(ApeKeyController.getApeKeys)(r),
|
||||
},
|
||||
add: {
|
||||
middleware: [...commonMiddleware, RateLimit.apeKeysGenerate],
|
||||
middleware: commonMiddleware,
|
||||
handler: async (r) => callController(ApeKeyController.generateApeKey)(r),
|
||||
},
|
||||
save: {
|
||||
middleware: [...commonMiddleware, RateLimit.apeKeysUpdate],
|
||||
middleware: commonMiddleware,
|
||||
handler: async (r) => callController(ApeKeyController.editApeKey)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [...commonMiddleware, RateLimit.apeKeysDelete],
|
||||
middleware: commonMiddleware,
|
||||
handler: async (r) => callController(ApeKeyController.deleteApeKey)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { configsContract } from "@monkeytype/contracts/configs";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ConfigController from "../controllers/config";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
|
@ -8,16 +7,12 @@ const s = initServer();
|
|||
|
||||
export default s.router(configsContract, {
|
||||
get: {
|
||||
middleware: [RateLimit.configGet],
|
||||
handler: async (r) => callController(ConfigController.getConfig)(r),
|
||||
},
|
||||
|
||||
save: {
|
||||
middleware: [RateLimit.configUpdate],
|
||||
handler: async (r) => callController(ConfigController.saveConfig)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [RateLimit.configDelete],
|
||||
handler: async (r) => callController(ConfigController.deleteConfig)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { configurationContract } from "@monkeytype/contracts/configuration";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { checkIfUserIsAdmin } from "../../middlewares/permission";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ConfigurationController from "../controllers/configuration";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
|
@ -14,12 +13,12 @@ export default s.router(configurationContract, {
|
|||
},
|
||||
|
||||
update: {
|
||||
middleware: [checkIfUserIsAdmin(), RateLimit.adminLimit],
|
||||
middleware: [checkIfUserIsAdmin()],
|
||||
handler: async (r) =>
|
||||
callController(ConfigurationController.updateConfiguration)(r),
|
||||
},
|
||||
getSchema: {
|
||||
middleware: [checkIfUserIsAdmin(), RateLimit.adminLimit],
|
||||
middleware: [checkIfUserIsAdmin()],
|
||||
handler: async (r) => callController(ConfigurationController.getSchema)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -34,6 +34,7 @@ import { createExpressEndpoints, initServer } from "@ts-rest/express";
|
|||
import { ZodIssue } from "zod";
|
||||
import { MonkeyValidationError } from "@monkeytype/contracts/schemas/api";
|
||||
import { authenticateTsRestRequest } from "../../middlewares/auth";
|
||||
import { rateLimitRequest } from "../../middlewares/rate-limit";
|
||||
|
||||
const pathOverride = process.env["API_PATH_OVERRIDE"];
|
||||
const BASE_ROUTE = pathOverride !== undefined ? `/${pathOverride}` : "";
|
||||
|
@ -111,7 +112,7 @@ function applyTsRestApiRoutes(app: IRouter): void {
|
|||
.status(422)
|
||||
.json({ message, validationErrors } as MonkeyValidationError);
|
||||
},
|
||||
globalMiddleware: [authenticateTsRestRequest()],
|
||||
globalMiddleware: [authenticateTsRestRequest(), rateLimitRequest()],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { initServer } from "@ts-rest/express";
|
||||
import { withApeRateLimiter } from "../../middlewares/ape-rate-limit";
|
||||
import { validate } from "../../middlewares/configuration";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as LeaderboardController from "../controllers/leaderboard";
|
||||
|
||||
import { leaderboardsContract } from "@monkeytype/contracts/leaderboards";
|
||||
|
@ -24,32 +22,30 @@ const requireWeeklyXpLeaderboardEnabled = validate({
|
|||
const s = initServer();
|
||||
export default s.router(leaderboardsContract, {
|
||||
get: {
|
||||
middleware: [RateLimit.leaderboardsGet],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getLeaderboard)(r),
|
||||
},
|
||||
getRank: {
|
||||
middleware: [withApeRateLimiter(RateLimit.leaderboardsGet)],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getRankFromLeaderboard)(r),
|
||||
},
|
||||
getDaily: {
|
||||
middleware: [requireDailyLeaderboardsEnabled, RateLimit.leaderboardsGet],
|
||||
middleware: [requireDailyLeaderboardsEnabled],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getDailyLeaderboard)(r),
|
||||
},
|
||||
getDailyRank: {
|
||||
middleware: [requireDailyLeaderboardsEnabled, RateLimit.leaderboardsGet],
|
||||
middleware: [requireDailyLeaderboardsEnabled],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getDailyLeaderboardRank)(r),
|
||||
},
|
||||
getWeeklyXp: {
|
||||
middleware: [requireWeeklyXpLeaderboardEnabled, RateLimit.leaderboardsGet],
|
||||
middleware: [requireWeeklyXpLeaderboardEnabled],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getWeeklyXpLeaderboardResults)(r),
|
||||
},
|
||||
getWeeklyXpRank: {
|
||||
middleware: [requireWeeklyXpLeaderboardEnabled, RateLimit.leaderboardsGet],
|
||||
middleware: [requireWeeklyXpLeaderboardEnabled],
|
||||
handler: async (r) =>
|
||||
callController(LeaderboardController.getWeeklyXpLeaderboardRank)(r),
|
||||
},
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
import { presetsContract } from "@monkeytype/contracts/presets";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as PresetController from "../controllers/preset";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
const s = initServer();
|
||||
export default s.router(presetsContract, {
|
||||
get: {
|
||||
middleware: [RateLimit.presetsGet],
|
||||
handler: async (r) => callController(PresetController.getPresets)(r),
|
||||
},
|
||||
add: {
|
||||
middleware: [RateLimit.presetsAdd],
|
||||
handler: async (r) => callController(PresetController.addPreset)(r),
|
||||
},
|
||||
save: {
|
||||
middleware: [RateLimit.presetsEdit],
|
||||
handler: async (r) => callController(PresetController.editPreset)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [RateLimit.presetsRemove],
|
||||
handler: async (r) => callController(PresetController.removePreset)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { psasContract } from "@monkeytype/contracts/psas";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as PsaController from "../controllers/psa";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
import { recordClientVersion } from "../../middlewares/utility";
|
||||
|
@ -8,7 +7,7 @@ import { recordClientVersion } from "../../middlewares/utility";
|
|||
const s = initServer();
|
||||
export default s.router(psasContract, {
|
||||
get: {
|
||||
middleware: [recordClientVersion(), RateLimit.psaGet],
|
||||
middleware: [recordClientVersion()],
|
||||
handler: async (r) => callController(PsaController.getPsas)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import { publicContract } from "@monkeytype/contracts/public";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as PublicController from "../controllers/public";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
const s = initServer();
|
||||
export default s.router(publicContract, {
|
||||
getSpeedHistogram: {
|
||||
middleware: [RateLimit.publicStatsGet],
|
||||
handler: async (r) => callController(PublicController.getSpeedHistogram)(r),
|
||||
},
|
||||
getTypingStats: {
|
||||
middleware: [RateLimit.publicStatsGet],
|
||||
handler: async (r) => callController(PublicController.getTypingStats)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { quotesContract } from "@monkeytype/contracts/quotes";
|
|||
import { initServer } from "@ts-rest/express";
|
||||
import { validate } from "../../middlewares/configuration";
|
||||
import { checkUserPermissions } from "../../middlewares/permission";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as QuoteController from "../controllers/quote";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
|
@ -18,11 +17,10 @@ const checkIfUserIsQuoteMod = checkUserPermissions(["quoteMod"], {
|
|||
const s = initServer();
|
||||
export default s.router(quotesContract, {
|
||||
get: {
|
||||
middleware: [checkIfUserIsQuoteMod, RateLimit.newQuotesGet],
|
||||
middleware: [checkIfUserIsQuoteMod],
|
||||
handler: async (r) => callController(QuoteController.getQuotes)(r),
|
||||
},
|
||||
isSubmissionEnabled: {
|
||||
middleware: [RateLimit.newQuotesIsSubmissionEnabled],
|
||||
handler: async (r) =>
|
||||
callController(QuoteController.isSubmissionEnabled)(r),
|
||||
},
|
||||
|
@ -35,24 +33,21 @@ export default s.router(quotesContract, {
|
|||
invalidMessage:
|
||||
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
|
||||
}),
|
||||
RateLimit.newQuotesAdd,
|
||||
],
|
||||
handler: async (r) => callController(QuoteController.addQuote)(r),
|
||||
},
|
||||
approveSubmission: {
|
||||
middleware: [checkIfUserIsQuoteMod, RateLimit.newQuotesAction],
|
||||
middleware: [checkIfUserIsQuoteMod],
|
||||
handler: async (r) => callController(QuoteController.approveQuote)(r),
|
||||
},
|
||||
rejectSubmission: {
|
||||
middleware: [checkIfUserIsQuoteMod, RateLimit.newQuotesAction],
|
||||
middleware: [checkIfUserIsQuoteMod],
|
||||
handler: async (r) => callController(QuoteController.refuseQuote)(r),
|
||||
},
|
||||
getRating: {
|
||||
middleware: [RateLimit.quoteRatingsGet],
|
||||
handler: async (r) => callController(QuoteController.getRating)(r),
|
||||
},
|
||||
addRating: {
|
||||
middleware: [RateLimit.quoteRatingsSubmit],
|
||||
handler: async (r) => callController(QuoteController.submitRating)(r),
|
||||
},
|
||||
report: {
|
||||
|
@ -63,7 +58,6 @@ export default s.router(quotesContract, {
|
|||
},
|
||||
invalidMessage: "Quote reporting is unavailable.",
|
||||
}),
|
||||
RateLimit.quoteReportSubmit,
|
||||
checkUserPermissions(["canReport"], {
|
||||
criteria: (user) => {
|
||||
return user.canReport !== false;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { resultsContract } from "@monkeytype/contracts/results";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { withApeRateLimiter2 as withApeRateLimiter } from "../../middlewares/ape-rate-limit";
|
||||
import { validate } from "../../middlewares/configuration";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ResultController from "../controllers/result";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
|
@ -16,25 +14,19 @@ const validateResultSavingEnabled = validate({
|
|||
const s = initServer();
|
||||
export default s.router(resultsContract, {
|
||||
get: {
|
||||
middleware: [
|
||||
withApeRateLimiter(RateLimit.resultsGet, RateLimit.resultsGetApe),
|
||||
],
|
||||
handler: async (r) => callController(ResultController.getResults)(r),
|
||||
},
|
||||
add: {
|
||||
middleware: [validateResultSavingEnabled, RateLimit.resultsAdd],
|
||||
middleware: [validateResultSavingEnabled],
|
||||
handler: async (r) => callController(ResultController.addResult)(r),
|
||||
},
|
||||
updateTags: {
|
||||
middleware: [RateLimit.resultsTagsUpdate],
|
||||
handler: async (r) => callController(ResultController.updateTags)(r),
|
||||
},
|
||||
deleteAll: {
|
||||
middleware: [RateLimit.resultsDeleteAll],
|
||||
handler: async (r) => callController(ResultController.deleteAll)(r),
|
||||
},
|
||||
getLast: {
|
||||
middleware: [withApeRateLimiter(RateLimit.resultsGet)],
|
||||
handler: async (r) => callController(ResultController.getLastResult)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { usersContract } from "@monkeytype/contracts/users";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { withApeRateLimiter2 as withApeRateLimiter } from "../../middlewares/ape-rate-limit";
|
||||
import { validate } from "../../middlewares/configuration";
|
||||
import { checkUserPermissions } from "../../middlewares/permission";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as UserController from "../controllers/user";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
|
@ -38,7 +36,6 @@ const requireInboxEnabled = validate({
|
|||
const s = initServer();
|
||||
export default s.router(usersContract, {
|
||||
get: {
|
||||
middleware: [RateLimit.userGet],
|
||||
handler: async (r) => callController(UserController.getUser)(r),
|
||||
},
|
||||
create: {
|
||||
|
@ -49,149 +46,117 @@ export default s.router(usersContract, {
|
|||
},
|
||||
invalidMessage: "Sign up is temporarily disabled",
|
||||
}),
|
||||
RateLimit.userSignup,
|
||||
],
|
||||
handler: async (r) => callController(UserController.createNewUser)(r),
|
||||
},
|
||||
getNameAvailability: {
|
||||
middleware: [RateLimit.userCheckName],
|
||||
handler: async (r) => callController(UserController.checkName)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [RateLimit.userDelete],
|
||||
handler: async (r) => callController(UserController.deleteUser)(r),
|
||||
},
|
||||
reset: {
|
||||
middleware: [RateLimit.userReset],
|
||||
handler: async (r) => callController(UserController.resetUser)(r),
|
||||
},
|
||||
updateName: {
|
||||
middleware: [RateLimit.userUpdateName],
|
||||
handler: async (r) => callController(UserController.updateName)(r),
|
||||
},
|
||||
updateLeaderboardMemory: {
|
||||
middleware: [RateLimit.userUpdateLBMemory],
|
||||
handler: async (r) => callController(UserController.updateLbMemory)(r),
|
||||
},
|
||||
updateEmail: {
|
||||
middleware: [RateLimit.userUpdateEmail],
|
||||
handler: async (r) => callController(UserController.updateEmail)(r),
|
||||
},
|
||||
updatePassword: {
|
||||
middleware: [RateLimit.userUpdateEmail],
|
||||
handler: async (r) => callController(UserController.updatePassword)(r),
|
||||
},
|
||||
getPersonalBests: {
|
||||
middleware: [withApeRateLimiter(RateLimit.userGet)],
|
||||
handler: async (r) => callController(UserController.getPersonalBests)(r),
|
||||
},
|
||||
deletePersonalBests: {
|
||||
middleware: [RateLimit.userClearPB],
|
||||
handler: async (r) => callController(UserController.clearPb)(r),
|
||||
},
|
||||
optOutOfLeaderboards: {
|
||||
middleware: [RateLimit.userOptOutOfLeaderboards],
|
||||
handler: async (r) =>
|
||||
callController(UserController.optOutOfLeaderboards)(r),
|
||||
},
|
||||
addResultFilterPreset: {
|
||||
middleware: [requireFilterPresetsEnabled, RateLimit.userCustomFilterAdd],
|
||||
middleware: [requireFilterPresetsEnabled],
|
||||
handler: async (r) =>
|
||||
callController(UserController.addResultFilterPreset)(r),
|
||||
},
|
||||
removeResultFilterPreset: {
|
||||
middleware: [requireFilterPresetsEnabled, RateLimit.userCustomFilterRemove],
|
||||
middleware: [requireFilterPresetsEnabled],
|
||||
handler: async (r) =>
|
||||
callController(UserController.removeResultFilterPreset)(r),
|
||||
},
|
||||
getTags: {
|
||||
middleware: [withApeRateLimiter(RateLimit.userTagsGet)],
|
||||
handler: async (r) => callController(UserController.getTags)(r),
|
||||
},
|
||||
createTag: {
|
||||
middleware: [RateLimit.userTagsAdd],
|
||||
handler: async (r) => callController(UserController.addTag)(r),
|
||||
},
|
||||
editTag: {
|
||||
middleware: [RateLimit.userTagsEdit],
|
||||
handler: async (r) => callController(UserController.editTag)(r),
|
||||
},
|
||||
deleteTag: {
|
||||
middleware: [RateLimit.userTagsRemove],
|
||||
handler: async (r) => callController(UserController.removeTag)(r),
|
||||
},
|
||||
deleteTagPersonalBest: {
|
||||
middleware: [RateLimit.userTagsClearPB],
|
||||
handler: async (r) => callController(UserController.clearTagPb)(r),
|
||||
},
|
||||
getCustomThemes: {
|
||||
middleware: [RateLimit.userCustomThemeGet],
|
||||
handler: async (r) => callController(UserController.getCustomThemes)(r),
|
||||
},
|
||||
addCustomTheme: {
|
||||
middleware: [RateLimit.userCustomThemeAdd],
|
||||
handler: async (r) => callController(UserController.addCustomTheme)(r),
|
||||
},
|
||||
deleteCustomTheme: {
|
||||
middleware: [RateLimit.userCustomThemeRemove],
|
||||
handler: async (r) => callController(UserController.removeCustomTheme)(r),
|
||||
},
|
||||
editCustomTheme: {
|
||||
middleware: [RateLimit.userCustomThemeEdit],
|
||||
handler: async (r) => callController(UserController.editCustomTheme)(r),
|
||||
},
|
||||
getDiscordOAuth: {
|
||||
middleware: [requireDiscordIntegrationEnabled, RateLimit.userDiscordLink],
|
||||
middleware: [requireDiscordIntegrationEnabled],
|
||||
handler: async (r) => callController(UserController.getOauthLink)(r),
|
||||
},
|
||||
linkDiscord: {
|
||||
middleware: [requireDiscordIntegrationEnabled, RateLimit.userDiscordLink],
|
||||
middleware: [requireDiscordIntegrationEnabled],
|
||||
handler: async (r) => callController(UserController.linkDiscord)(r),
|
||||
},
|
||||
unlinkDiscord: {
|
||||
middleware: [RateLimit.userDiscordUnlink],
|
||||
handler: async (r) => callController(UserController.unlinkDiscord)(r),
|
||||
},
|
||||
getStats: {
|
||||
middleware: [withApeRateLimiter(RateLimit.userGet)],
|
||||
handler: async (r) => callController(UserController.getStats)(r),
|
||||
},
|
||||
setStreakHourOffset: {
|
||||
middleware: [RateLimit.setStreakHourOffset],
|
||||
handler: async (r) => callController(UserController.setStreakHourOffset)(r),
|
||||
},
|
||||
getFavoriteQuotes: {
|
||||
middleware: [RateLimit.quoteFavoriteGet],
|
||||
handler: async (r) => callController(UserController.getFavoriteQuotes)(r),
|
||||
},
|
||||
addQuoteToFavorites: {
|
||||
middleware: [RateLimit.quoteFavoritePost],
|
||||
handler: async (r) => callController(UserController.addFavoriteQuote)(r),
|
||||
},
|
||||
removeQuoteFromFavorites: {
|
||||
middleware: [RateLimit.quoteFavoriteDelete],
|
||||
handler: async (r) => callController(UserController.removeFavoriteQuote)(r),
|
||||
},
|
||||
getProfile: {
|
||||
middleware: [
|
||||
requireProfilesEnabled,
|
||||
withApeRateLimiter(RateLimit.userProfileGet),
|
||||
],
|
||||
middleware: [requireProfilesEnabled],
|
||||
handler: async (r) => callController(UserController.getProfile)(r),
|
||||
},
|
||||
updateProfile: {
|
||||
middleware: [
|
||||
requireProfilesEnabled,
|
||||
withApeRateLimiter(RateLimit.userProfileUpdate),
|
||||
],
|
||||
middleware: [requireProfilesEnabled],
|
||||
handler: async (r) => callController(UserController.updateProfile)(r),
|
||||
},
|
||||
getInbox: {
|
||||
middleware: [requireInboxEnabled, RateLimit.userMailGet],
|
||||
middleware: [requireInboxEnabled],
|
||||
handler: async (r) => callController(UserController.getInbox)(r),
|
||||
},
|
||||
updateInbox: {
|
||||
middleware: [requireInboxEnabled, RateLimit.userMailUpdate],
|
||||
middleware: [requireInboxEnabled],
|
||||
handler: async (r) => callController(UserController.updateInbox)(r),
|
||||
},
|
||||
report: {
|
||||
|
@ -207,35 +172,28 @@ export default s.router(usersContract, {
|
|||
return user.canReport !== false;
|
||||
},
|
||||
}),
|
||||
RateLimit.quoteReportSubmit,
|
||||
],
|
||||
handler: async (r) => callController(UserController.reportUser)(r),
|
||||
},
|
||||
verificationEmail: {
|
||||
middleware: [RateLimit.userRequestVerificationEmail],
|
||||
handler: async (r) =>
|
||||
callController(UserController.sendVerificationEmail)(r),
|
||||
},
|
||||
forgotPasswordEmail: {
|
||||
middleware: [RateLimit.userForgotPasswordEmail],
|
||||
handler: async (r) =>
|
||||
callController(UserController.sendForgotPasswordEmail)(r),
|
||||
},
|
||||
revokeAllTokens: {
|
||||
middleware: [RateLimit.userRevokeAllTokens],
|
||||
handler: async (r) => callController(UserController.revokeAllTokens)(r),
|
||||
},
|
||||
getTestActivity: {
|
||||
middleware: [RateLimit.userTestActivity],
|
||||
handler: async (r) => callController(UserController.getTestActivity)(r),
|
||||
},
|
||||
getCurrentTestActivity: {
|
||||
middleware: [withApeRateLimiter(RateLimit.userCurrentTestActivity)],
|
||||
handler: async (r) =>
|
||||
callController(UserController.getCurrentTestActivity)(r),
|
||||
},
|
||||
getStreak: {
|
||||
middleware: [withApeRateLimiter(RateLimit.userStreak)],
|
||||
handler: async (r) => callController(UserController.getStreak)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import MonkeyError from "../utils/error";
|
||||
import type { Response, NextFunction, RequestHandler } from "express";
|
||||
import statuses from "../constants/monkey-status-codes";
|
||||
import rateLimit, {
|
||||
type RateLimitRequestHandler,
|
||||
type Options,
|
||||
} from "express-rate-limit";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
|
||||
const REQUEST_MULTIPLIER = isDevEnvironment() ? 1 : 1;
|
||||
|
||||
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
|
||||
return req?.ctx?.decodedToken?.uid;
|
||||
};
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
|
||||
const {
|
||||
APE_KEY_RATE_LIMIT_EXCEEDED: { message, code },
|
||||
} = statuses;
|
||||
|
||||
export const customHandler = (
|
||||
_req: MonkeyTypes.Request,
|
||||
_res: Response,
|
||||
_next: NextFunction,
|
||||
_options: Options
|
||||
): void => {
|
||||
throw new MonkeyError(code, message);
|
||||
};
|
||||
|
||||
const apeRateLimiter = rateLimit({
|
||||
windowMs: ONE_MINUTE,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKey,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export function withApeRateLimiter(
|
||||
defaultRateLimiter: RateLimitRequestHandler,
|
||||
apeRateLimiterOverride?: RateLimitRequestHandler
|
||||
): RequestHandler {
|
||||
return (req: MonkeyTypes.Request, res: Response, next: NextFunction) => {
|
||||
if (req.ctx.decodedToken.type === "ApeKey") {
|
||||
const rateLimiter = apeRateLimiterOverride ?? apeRateLimiter;
|
||||
// TODO: bump version?
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return rateLimiter(req, res, next);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return defaultRateLimiter(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
export function withApeRateLimiter2(
|
||||
defaultRateLimiter: RateLimitRequestHandler,
|
||||
apeRateLimiterOverride?: RateLimitRequestHandler
|
||||
): MonkeyTypes.RequestHandler {
|
||||
return (req: TsRestRequestWithCtx, res: Response, next: NextFunction) => {
|
||||
if (req.ctx.decodedToken.type === "ApeKey") {
|
||||
const rateLimiter = apeRateLimiterOverride ?? apeRateLimiter;
|
||||
// TODO: bump version?
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return rateLimiter(req, res, next);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return defaultRateLimiter(req, res, next);
|
||||
};
|
||||
}
|
|
@ -26,7 +26,8 @@ const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
|||
|
||||
export type TsRestRequestWithCtx = {
|
||||
ctx: Readonly<MonkeyTypes.Context>;
|
||||
} & TsRestRequest;
|
||||
} & TsRestRequest &
|
||||
ExpressRequest;
|
||||
|
||||
/**
|
||||
* Authenticate request based on the auth settings of the route.
|
||||
|
|
|
@ -2,10 +2,39 @@ import _ from "lodash";
|
|||
import MonkeyError from "../utils/error";
|
||||
import type { Response, NextFunction } from "express";
|
||||
import { RateLimiterMemory } from "rate-limiter-flexible";
|
||||
import rateLimit, { type Options } from "express-rate-limit";
|
||||
import {
|
||||
rateLimit,
|
||||
RateLimitRequestHandler,
|
||||
type Options,
|
||||
} from "express-rate-limit";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import {
|
||||
limits,
|
||||
RateLimiterId,
|
||||
RateLimitOptions,
|
||||
Window,
|
||||
} from "@monkeytype/contracts/rate-limit/index";
|
||||
import statuses from "../constants/monkey-status-codes";
|
||||
|
||||
const REQUEST_MULTIPLIER = isDevEnvironment() ? 100 : 1;
|
||||
export const REQUEST_MULTIPLIER = isDevEnvironment() ? 100 : 1;
|
||||
|
||||
export const customHandler = (
|
||||
req: MonkeyTypes.ExpressRequestWithContext,
|
||||
_res: Response,
|
||||
_next: NextFunction,
|
||||
_options: Options
|
||||
): void => {
|
||||
if (req.ctx.decodedToken.type === "ApeKey") {
|
||||
throw new MonkeyError(
|
||||
statuses.APE_KEY_RATE_LIMIT_EXCEEDED.code,
|
||||
statuses.APE_KEY_RATE_LIMIT_EXCEEDED.message
|
||||
);
|
||||
}
|
||||
throw new MonkeyError(429, "Request limit reached, please try again later.");
|
||||
};
|
||||
|
||||
const getKey = (req: MonkeyTypes.Request, _res: Response): string => {
|
||||
return (
|
||||
|
@ -23,22 +52,85 @@ const getKeyWithUid = (req: MonkeyTypes.Request, _res: Response): string => {
|
|||
return useUid ? uid : getKey(req, _res);
|
||||
};
|
||||
|
||||
export const customHandler = (
|
||||
_req: MonkeyTypes.Request,
|
||||
_res: Response,
|
||||
_next: NextFunction,
|
||||
_options: Options
|
||||
): void => {
|
||||
throw new MonkeyError(429, "Request limit reached, please try again later.");
|
||||
};
|
||||
function initialiseLimiters(): Record<RateLimiterId, RateLimitRequestHandler> {
|
||||
const keys = Object.keys(limits) as RateLimiterId[];
|
||||
|
||||
const ONE_HOUR_SECONDS = 60 * 60;
|
||||
const ONE_HOUR_MS = 1000 * ONE_HOUR_SECONDS;
|
||||
const ONE_DAY_MS = 24 * ONE_HOUR_MS;
|
||||
const convert = (options: RateLimitOptions): RateLimitRequestHandler => {
|
||||
return rateLimit({
|
||||
windowMs: convertWindowToMs(options.window),
|
||||
max: options.max * REQUEST_MULTIPLIER,
|
||||
handler: customHandler,
|
||||
keyGenerator: getKeyWithUid,
|
||||
});
|
||||
};
|
||||
|
||||
return keys.reduce(
|
||||
(output, key) => ({ ...output, [key]: convert(limits[key]) }),
|
||||
{}
|
||||
) as Record<RateLimiterId, RateLimitRequestHandler>;
|
||||
}
|
||||
|
||||
function convertWindowToMs(window: Window): number {
|
||||
if (typeof window === "number") return window;
|
||||
switch (window) {
|
||||
case "second":
|
||||
return 1000;
|
||||
case "minute":
|
||||
return 60 * 1000;
|
||||
case "hour":
|
||||
return 60 * 60 * 1000;
|
||||
case "day":
|
||||
return 24 * 60 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
export const requestLimiters: Record<RateLimiterId, RateLimitRequestHandler> =
|
||||
initialiseLimiters();
|
||||
|
||||
export function rateLimitRequest<
|
||||
T extends AppRouter | AppRoute
|
||||
>(): TsRestRequestHandler<T> {
|
||||
return async (
|
||||
req: TsRestRequestWithCtx,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const rateLimit = (req.tsRestRoute["metadata"] as EndpointMetadata)
|
||||
?.rateLimit;
|
||||
if (rateLimit === undefined) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasApeKeyLimiterId = typeof rateLimit === "object";
|
||||
let rateLimiterId: RateLimiterId;
|
||||
|
||||
if (req.ctx.decodedToken.type === "ApeKey") {
|
||||
rateLimiterId = hasApeKeyLimiterId
|
||||
? rateLimit.apeKey
|
||||
: "defaultApeRateLimit";
|
||||
} else {
|
||||
rateLimiterId = hasApeKeyLimiterId ? rateLimit.normal : rateLimit;
|
||||
}
|
||||
|
||||
const rateLimiter = requestLimiters[rateLimiterId];
|
||||
if (rateLimiter === undefined) {
|
||||
next(
|
||||
new MonkeyError(
|
||||
500,
|
||||
`Unknown rateLimiterId '${rateLimiterId}', how did you manage to do this?`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
rateLimiter(req, res, next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Root Rate Limit
|
||||
export const rootRateLimiter = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
windowMs: 60 * 1000 * 60,
|
||||
max: 1000 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKey,
|
||||
handler: (_req, _res, _next, _options): void => {
|
||||
|
@ -52,7 +144,7 @@ export const rootRateLimiter = rateLimit({
|
|||
// Bad Authentication Rate Limiter
|
||||
const badAuthRateLimiter = new RateLimiterMemory({
|
||||
points: 30 * REQUEST_MULTIPLIER,
|
||||
duration: ONE_HOUR_SECONDS,
|
||||
duration: 60 * 60, //one hour seconds
|
||||
});
|
||||
|
||||
export async function badAuthRateLimiterHandler(
|
||||
|
@ -103,476 +195,9 @@ export async function incrementBadAuth(
|
|||
} catch (error) {}
|
||||
}
|
||||
|
||||
export const adminLimit = rateLimit({
|
||||
windowMs: 5000,
|
||||
max: 1 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Config Routing
|
||||
export const configUpdate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const configGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 120 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const configDelete = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 120 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Leaderboards Routing
|
||||
export const leaderboardsGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// New Quotes Routing
|
||||
export const newQuotesGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const newQuotesIsSubmissionEnabled = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const newQuotesAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const newQuotesAction = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Quote Ratings Routing
|
||||
export const quoteRatingsGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const quoteRatingsSubmit = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 500 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Quote reporting
|
||||
export const quoteReportSubmit = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30 min
|
||||
max: 50 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Quote favorites
|
||||
export const quoteFavoriteGet = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30 min
|
||||
max: 50 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const quoteFavoritePost = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30 min
|
||||
max: 50 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const quoteFavoriteDelete = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30 min
|
||||
max: 50 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Presets Routing
|
||||
export const presetsGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const presetsAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const presetsRemove = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const presetsEdit = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// PSA (Public Service Announcement) Routing
|
||||
export const psaGet = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// get public speed stats
|
||||
export const publicStatsGet = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Results Routing
|
||||
export const resultsGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Results Routing
|
||||
export const resultsGetApe = rateLimit({
|
||||
windowMs: ONE_DAY_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const resultsAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 300 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const resultsTagsUpdate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 100 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const resultsDeleteAll = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 10 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const resultsLeaderboardGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const resultsLeaderboardQualificationGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// Users Routing
|
||||
export const userGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const setStreakHourOffset = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 5 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userSignup = rateLimit({
|
||||
windowMs: 24 * ONE_HOUR_MS, // 1 day
|
||||
max: 2 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKey,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userDelete = rateLimit({
|
||||
windowMs: 24 * ONE_HOUR_MS, // 1 day
|
||||
max: 3 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userReset = rateLimit({
|
||||
windowMs: 24 * ONE_HOUR_MS, // 1 day
|
||||
max: 3 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCheckName = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userUpdateName = rateLimit({
|
||||
windowMs: 24 * ONE_HOUR_MS, // 1 day
|
||||
max: 3 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userUpdateLBMemory = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userUpdateEmail = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userClearPB = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userOptOutOfLeaderboards = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 10 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomFilterAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomFilterRemove = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTagsGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTagsRemove = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTagsClearPB = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTagsEdit = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTagsAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeRemove = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomThemeEdit = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 30 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userDiscordLink = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 15 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const usersTagsEdit = userDiscordLink;
|
||||
|
||||
export const userDiscordUnlink = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 15 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userRequestVerificationEmail = rateLimit({
|
||||
windowMs: ONE_HOUR_MS / 4,
|
||||
max: 1 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userForgotPasswordEmail = rateLimit({
|
||||
windowMs: ONE_HOUR_MS / 60,
|
||||
max: 1 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userRevokeAllTokens = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 10 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userProfileGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 100 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userProfileUpdate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userMailGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userMailUpdate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userTestActivity = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCurrentTestActivity = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userStreak = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// ApeKeys Routing
|
||||
export const apeKeysGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 120 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const apeKeysGenerate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 15 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const webhookLimit = rateLimit({
|
||||
windowMs: 1000,
|
||||
max: 1 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const apeKeysUpdate = apeKeysGenerate;
|
||||
|
||||
export const apeKeysDelete = apeKeysGenerate;
|
||||
|
|
4
backend/src/types/types.d.ts
vendored
4
backend/src/types/types.d.ts
vendored
|
@ -22,6 +22,10 @@ declare namespace MonkeyTypes {
|
|||
ctx: Readonly<Context>;
|
||||
} & ExpressRequest;
|
||||
|
||||
type ExpressRequestWithContext = {
|
||||
ctx: Readonly<Context>;
|
||||
} & ExpressRequest;
|
||||
|
||||
type Request2<TQuery = undefined, TBody = undefined, TParams = undefined> = {
|
||||
query: Readonly<TQuery>;
|
||||
body: Readonly<TBody>;
|
||||
|
|
|
@ -440,7 +440,7 @@ const notes = {
|
|||
} as const;
|
||||
|
||||
type ValidNotes = keyof typeof notes;
|
||||
type ValidFrequencies = typeof notes[ValidNotes];
|
||||
type ValidFrequencies = (typeof notes)[ValidNotes];
|
||||
|
||||
type GetNoteFrequencyCallback = (octave: number) => number;
|
||||
|
||||
|
|
|
@ -264,12 +264,12 @@ function setFilter<G extends ResultFiltersGroup>(
|
|||
filter: ResultFiltersGroupItem<G>,
|
||||
value: boolean
|
||||
): void {
|
||||
filters[group][filter] = value as typeof filters[G][typeof filter];
|
||||
filters[group][filter] = value as (typeof filters)[G][typeof filter];
|
||||
}
|
||||
|
||||
function setAllFilters(group: ResultFiltersGroup, value: boolean): void {
|
||||
Object.keys(getGroup(group)).forEach((filter) => {
|
||||
filters[group][filter as keyof typeof filters[typeof group]] =
|
||||
filters[group][filter as keyof (typeof filters)[typeof group]] =
|
||||
value as never;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ export async function refresh(
|
|||
const showTopRow =
|
||||
(TestWords.hasNumbers && Config.keymapMode === "next") ||
|
||||
Config.keymapShowTopRow === "always" ||
|
||||
((lts as typeof layouts["qwerty"]).keymapShowTopRow &&
|
||||
((lts as (typeof layouts)["qwerty"]).keymapShowTopRow &&
|
||||
Config.keymapShowTopRow !== "never");
|
||||
|
||||
const isMatrix =
|
||||
|
|
|
@ -40,7 +40,7 @@ function getSearchService<T>(
|
|||
|
||||
const newSearchService = buildSearchService<T>(data, textExtractor);
|
||||
searchServiceCache[language] =
|
||||
newSearchService as unknown as typeof searchServiceCache[typeof language];
|
||||
newSearchService as unknown as (typeof searchServiceCache)[typeof language];
|
||||
|
||||
return newSearchService;
|
||||
}
|
||||
|
|
|
@ -1036,7 +1036,7 @@ function sortAndRefreshHistory(
|
|||
|
||||
if (filteredResults.length < 2) return;
|
||||
|
||||
const key = keyString as keyof typeof filteredResults[0];
|
||||
const key = keyString as keyof (typeof filteredResults)[0];
|
||||
|
||||
// This allows to reverse the sorting order when clicking multiple times on the table header
|
||||
let descending = true;
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"knip": "2.19.2",
|
||||
"lint-staged": "13.2.3",
|
||||
"only-allow": "1.2.1",
|
||||
"prettier": "2.5.1",
|
||||
"prettier": "2.8.8",
|
||||
"turbo": "2.0.12",
|
||||
"vitest": "2.0.5"
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { initContract } from "@ts-rest/core";
|
|||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
|
@ -107,10 +107,11 @@ export const adminContract = c.router(
|
|||
{
|
||||
pathPrefix: "/admin",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "admin",
|
||||
authenticationOptions: { noCache: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "adminLimit",
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
|
@ -50,6 +50,9 @@ export const apeKeysContract = c.router(
|
|||
responses: {
|
||||
200: GetApeKeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGet",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add ape key",
|
||||
|
@ -60,6 +63,9 @@ export const apeKeysContract = c.router(
|
|||
responses: {
|
||||
200: AddApeKeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update ape key",
|
||||
|
@ -71,6 +77,9 @@ export const apeKeysContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete ape key",
|
||||
|
@ -82,14 +91,17 @@ export const apeKeysContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/ape-keys",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "ape-keys",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithNullableData,
|
||||
} from "./schemas/api";
|
||||
|
@ -26,35 +26,44 @@ export const configsContract = c.router(
|
|||
responses: {
|
||||
200: GetConfigResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "configGet",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update config",
|
||||
description:
|
||||
"Update the config of the current user. Only provided values will be updated while the missing values will be unchanged.",
|
||||
method: "PATCH",
|
||||
path: "",
|
||||
body: PartialConfigSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
summary: "update config",
|
||||
description:
|
||||
"Update the config of the current user. Only provided values will be updated while the missing values will be unchanged.",
|
||||
metadata: meta({
|
||||
rateLimit: "configUpdate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete config",
|
||||
description: "Delete/reset the config for the current user.",
|
||||
method: "DELETE",
|
||||
path: "",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
summary: "delete config",
|
||||
description: "Delete/reset the config for the current user.",
|
||||
metadata: meta({
|
||||
rateLimit: "configDelete",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configs",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "configs",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
|
@ -45,11 +45,11 @@ export const configurationContract = c.router(
|
|||
responses: {
|
||||
200: GetConfigurationResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
summary: "update configuration",
|
||||
|
@ -61,12 +61,13 @@ export const configurationContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
noCache: true,
|
||||
isPublicOnDev: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "adminLimit",
|
||||
}),
|
||||
},
|
||||
getSchema: {
|
||||
summary: "get configuration schema",
|
||||
|
@ -76,20 +77,21 @@ export const configurationContract = c.router(
|
|||
responses: {
|
||||
200: ConfigurationSchemaResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
isPublicOnDev: true,
|
||||
noCache: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "adminLimit",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configuration",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "configuration",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
import { CommonResponses, meta, responseWithData } from "./schemas/api";
|
||||
import { IdSchema } from "./schemas/util";
|
||||
|
||||
export const GenerateDataRequestSchema = z.object({
|
||||
|
@ -47,12 +43,12 @@ export const devContract = c.router(
|
|||
{
|
||||
pathPrefix: "/dev",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "development",
|
||||
authenticationOptions: {
|
||||
isPublicOnDev: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
responseWithData,
|
||||
responseWithNullableData,
|
||||
} from "./schemas/api";
|
||||
|
@ -98,9 +98,9 @@ export const leaderboardsContract = c.router(
|
|||
responses: {
|
||||
200: GetLeaderboardResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
},
|
||||
getRank: {
|
||||
summary: "get leaderboard rank",
|
||||
|
@ -112,9 +112,9 @@ export const leaderboardsContract = c.router(
|
|||
responses: {
|
||||
200: GetLeaderboardRankResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
},
|
||||
getDaily: {
|
||||
summary: "get daily leaderboard",
|
||||
|
@ -125,9 +125,9 @@ export const leaderboardsContract = c.router(
|
|||
responses: {
|
||||
200: GetLeaderboardResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
},
|
||||
getDailyRank: {
|
||||
summary: "get daily leaderboard rank",
|
||||
|
@ -148,9 +148,9 @@ export const leaderboardsContract = c.router(
|
|||
responses: {
|
||||
200: GetWeeklyXpLeaderboardResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
},
|
||||
getWeeklyXpRank: {
|
||||
summary: "get weekly xp leaderboard rank",
|
||||
|
@ -166,9 +166,10 @@ export const leaderboardsContract = c.router(
|
|||
{
|
||||
pathPrefix: "/leaderboards",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "leaderboards",
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "leaderboardsGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
|
@ -38,6 +38,9 @@ export const presetsContract = c.router(
|
|||
responses: {
|
||||
200: GetPresetResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsGet",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add preset",
|
||||
|
@ -48,6 +51,9 @@ export const presetsContract = c.router(
|
|||
responses: {
|
||||
200: AddPresetResponseSchemna,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsAdd",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update preset",
|
||||
|
@ -58,8 +64,13 @@ export const presetsContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsEdit",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete preset",
|
||||
description: "Delete preset by id.",
|
||||
method: "DELETE",
|
||||
path: "/:presetId",
|
||||
pathParams: DeletePresetsParamsSchema,
|
||||
|
@ -67,16 +78,17 @@ export const presetsContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
summary: "delete preset",
|
||||
description: "Delete preset by id.",
|
||||
metadata: meta({
|
||||
rateLimit: "presetsRemove",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/presets",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "presets",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -2,11 +2,7 @@ import { initContract } from "@ts-rest/core";
|
|||
import { z } from "zod";
|
||||
import { PSASchema } from "./schemas/psas";
|
||||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
import { CommonResponses, meta, responseWithData } from "./schemas/api";
|
||||
export const GetPsaResponseSchema = responseWithData(z.array(PSASchema));
|
||||
export type GetPsaResponse = z.infer<typeof GetPsaResponseSchema>;
|
||||
|
||||
|
@ -26,12 +22,13 @@ export const psasContract = c.router(
|
|||
{
|
||||
pathPrefix: "/psas",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "psas",
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "psaGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
import { CommonResponses, meta, responseWithData } from "./schemas/api";
|
||||
import { SpeedHistogramSchema, TypingStatsSchema } from "./schemas/public";
|
||||
import { Mode2Schema, ModeSchema } from "./schemas/shared";
|
||||
import { LanguageSchema } from "./schemas/util";
|
||||
|
@ -59,12 +55,13 @@ export const publicContract = c.router(
|
|||
{
|
||||
pathPrefix: "/public",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "public",
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "publicStatsGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { z } from "zod";
|
|||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
responseWithNullableData,
|
||||
|
@ -96,6 +96,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: GetQuotesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesGet",
|
||||
}),
|
||||
},
|
||||
isSubmissionEnabled: {
|
||||
summary: "is submission enabled",
|
||||
|
@ -105,9 +108,10 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: IsSubmissionEnabledResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "newQuotesIsSubmissionEnabled",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "submit quote",
|
||||
|
@ -118,6 +122,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAdd",
|
||||
}),
|
||||
},
|
||||
approveSubmission: {
|
||||
summary: "submit quote",
|
||||
|
@ -128,6 +135,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: ApproveQuoteResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAction",
|
||||
}),
|
||||
},
|
||||
rejectSubmission: {
|
||||
summary: "reject quote",
|
||||
|
@ -138,6 +148,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAction",
|
||||
}),
|
||||
},
|
||||
getRating: {
|
||||
summary: "get rating",
|
||||
|
@ -148,6 +161,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: GetQuoteRatingResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteRatingsGet",
|
||||
}),
|
||||
},
|
||||
addRating: {
|
||||
summary: "add rating",
|
||||
|
@ -158,6 +174,9 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteRatingsSubmit",
|
||||
}),
|
||||
},
|
||||
report: {
|
||||
summary: "report quote",
|
||||
|
@ -168,14 +187,17 @@ export const quotesContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteReportSubmit",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/quotes",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "quotes",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
374
packages/contracts/src/rate-limit/index.ts
Normal file
374
packages/contracts/src/rate-limit/index.ts
Normal file
|
@ -0,0 +1,374 @@
|
|||
export type Window = "second" | "minute" | "hour" | "day" | number;
|
||||
export type RateLimitOptions = {
|
||||
/** Timeframe or time in milliseconds */
|
||||
window: Window;
|
||||
/** Max request within the given window */
|
||||
max: number;
|
||||
};
|
||||
|
||||
export const limits = {
|
||||
defaultApeRateLimit: {
|
||||
window: "minute",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
adminLimit: {
|
||||
window: 5000, //5 seconds
|
||||
max: 1,
|
||||
},
|
||||
|
||||
// Config Routing
|
||||
configUpdate: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
configGet: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
configDelete: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
// Leaderboards Routing
|
||||
leaderboardsGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// New Quotes Routing
|
||||
newQuotesGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
newQuotesIsSubmissionEnabled: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
newQuotesAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
newQuotesAction: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// Quote Ratings Routing
|
||||
quoteRatingsGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
quoteRatingsSubmit: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// Quote reporting
|
||||
quoteReportSubmit: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
// Quote favorites
|
||||
quoteFavoriteGet: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
quoteFavoritePost: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
quoteFavoriteDelete: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
// Presets Routing
|
||||
presetsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsRemove: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsEdit: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// PSA (Public Service Announcement) Routing
|
||||
psaGet: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// get public speed stats
|
||||
publicStatsGet: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Results Routing
|
||||
resultsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Results Routing
|
||||
resultsGetApe: {
|
||||
window: "day",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
resultsAdd: {
|
||||
window: "hour",
|
||||
max: 300,
|
||||
},
|
||||
|
||||
resultsTagsUpdate: {
|
||||
window: "hour",
|
||||
max: 100,
|
||||
},
|
||||
|
||||
resultsDeleteAll: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
resultsLeaderboardGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
resultsLeaderboardQualificationGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Users Routing
|
||||
userGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
setStreakHourOffset: {
|
||||
window: "hour",
|
||||
max: 5,
|
||||
},
|
||||
|
||||
userSignup: {
|
||||
window: "day",
|
||||
max: 2,
|
||||
},
|
||||
|
||||
userDelete: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userReset: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userCheckName: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userUpdateName: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userUpdateLBMemory: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userUpdateEmail: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userClearPB: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userOptOutOfLeaderboards: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
userCustomFilterAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userCustomFilterRemove: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsRemove: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userTagsClearPB: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsEdit: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userTagsAdd: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeGet: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeAdd: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeRemove: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeEdit: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userDiscordLink: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
userDiscordUnlink: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
userRequestVerificationEmail: {
|
||||
window: 15 * 60 * 1000, //15 minutes
|
||||
max: 1,
|
||||
},
|
||||
|
||||
userForgotPasswordEmail: {
|
||||
window: "minute",
|
||||
max: 1,
|
||||
},
|
||||
|
||||
userRevokeAllTokens: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
userProfileGet: {
|
||||
window: "hour",
|
||||
max: 100,
|
||||
},
|
||||
|
||||
userProfileUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userMailGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userMailUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTestActivity: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userCurrentTestActivity: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userStreak: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// ApeKeys Routing
|
||||
apeKeysGet: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
apeKeysGenerate: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
webhookLimit: {
|
||||
window: "second",
|
||||
max: 1,
|
||||
},
|
||||
} satisfies Record<string, RateLimitOptions>;
|
||||
|
||||
export type RateLimiterId = keyof typeof limits;
|
||||
export type RateLimitIds = {
|
||||
/** rate limiter options for non-apeKey requests */
|
||||
normal: RateLimiterId;
|
||||
/** Rate limiter options for apeKey requests */
|
||||
apeKey: RateLimiterId;
|
||||
};
|
||||
|
||||
export function getLimits(limit: RateLimiterId | RateLimitIds): {
|
||||
limiter: RateLimitOptions;
|
||||
apeKeyLimiter?: RateLimitOptions;
|
||||
} {
|
||||
const isApeKeyLimiter = typeof limit === "object";
|
||||
const limiter = isApeKeyLimiter ? limit.normal : limit;
|
||||
const apeLimiter = isApeKeyLimiter ? limit.apeKey : undefined;
|
||||
|
||||
return {
|
||||
limiter: limits[limiter],
|
||||
apeKeyLimiter: apeLimiter !== undefined ? limits[apeLimiter] : undefined,
|
||||
};
|
||||
}
|
|
@ -2,7 +2,7 @@ import { initContract } from "@ts-rest/core";
|
|||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
|
@ -75,19 +75,22 @@ export const resultsContract = c.router(
|
|||
{
|
||||
get: {
|
||||
summary: "get results",
|
||||
description:
|
||||
"Gets up to 1000 results (endpoint limited to 30 requests per day for ape keys)",
|
||||
description: "Gets up to 1000 results",
|
||||
method: "GET",
|
||||
path: "",
|
||||
query: GetResultsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetResultsResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
acceptApeKeys: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: {
|
||||
normal: "resultsGet",
|
||||
apeKey: "resultsGetApe",
|
||||
},
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add result",
|
||||
|
@ -98,6 +101,9 @@ export const resultsContract = c.router(
|
|||
responses: {
|
||||
200: AddResultResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "resultsAdd",
|
||||
}),
|
||||
},
|
||||
updateTags: {
|
||||
summary: "update result tags",
|
||||
|
@ -108,6 +114,9 @@ export const resultsContract = c.router(
|
|||
responses: {
|
||||
200: UpdateResultTagsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "resultsTagsUpdate",
|
||||
}),
|
||||
},
|
||||
deleteAll: {
|
||||
summary: "delete all results",
|
||||
|
@ -118,11 +127,12 @@ export const resultsContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
requireFreshToken: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "resultsDeleteAll",
|
||||
}),
|
||||
},
|
||||
getLast: {
|
||||
summary: "get last result",
|
||||
|
@ -132,19 +142,20 @@ export const resultsContract = c.router(
|
|||
responses: {
|
||||
200: GetLastResultResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
acceptApeKeys: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "resultsGet",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/results",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "results",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { z, ZodSchema } from "zod";
|
||||
import { RateLimitIds, RateLimiterId } from "../rate-limit";
|
||||
|
||||
export type OpenApiTag =
|
||||
| "configs"
|
||||
|
@ -17,9 +18,25 @@ export type OpenApiTag =
|
|||
export type EndpointMetadata = {
|
||||
/** Authentication options, by default a bearer token is required. */
|
||||
authenticationOptions?: RequestAuthenticationOptions;
|
||||
|
||||
openApiTags?: OpenApiTag | OpenApiTag[];
|
||||
|
||||
/** RateLimitId or RateLimitIds.
|
||||
* Only specifying RateLimiterId will use a default limiter with 30 requests/minute for ApeKey requests.
|
||||
*/
|
||||
rateLimit?: RateLimiterId | RateLimitIds;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param meta Ensure the type of metadata is `EndpointMetadata`.
|
||||
* Ts-rest does not allow to specify the type of `metadata`.
|
||||
* @returns
|
||||
*/
|
||||
export function meta(meta: EndpointMetadata): EndpointMetadata {
|
||||
return meta;
|
||||
}
|
||||
|
||||
export type RequestAuthenticationOptions = {
|
||||
/** Endpoint is accessible without any authentication. If `false` bearer authentication is required. */
|
||||
isPublic?: boolean;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { initContract } from "@ts-rest/core";
|
|||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
meta,
|
||||
MonkeyClientError,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
|
@ -334,6 +334,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetUserResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
create: {
|
||||
summary: "create user",
|
||||
|
@ -344,6 +347,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userSignup",
|
||||
}),
|
||||
},
|
||||
getNameAvailability: {
|
||||
summary: "check name",
|
||||
|
@ -355,9 +361,10 @@ export const usersContract = c.router(
|
|||
200: MonkeyResponseSchema.describe("Name is available"),
|
||||
409: MonkeyResponseSchema.describe("Name is not available"),
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userCheckName",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete user",
|
||||
|
@ -368,9 +375,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userDelete",
|
||||
}),
|
||||
},
|
||||
reset: {
|
||||
summary: "reset user",
|
||||
|
@ -381,9 +389,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userReset",
|
||||
}),
|
||||
},
|
||||
updateName: {
|
||||
summary: "update username",
|
||||
|
@ -394,9 +403,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userUpdateName",
|
||||
}),
|
||||
},
|
||||
updateLeaderboardMemory: {
|
||||
summary: "update lbMemory",
|
||||
|
@ -407,6 +417,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userUpdateLBMemory",
|
||||
}),
|
||||
},
|
||||
updateEmail: {
|
||||
summary: "update email",
|
||||
|
@ -417,9 +430,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userUpdateEmail",
|
||||
}),
|
||||
},
|
||||
updatePassword: {
|
||||
summary: "update password",
|
||||
|
@ -430,9 +444,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userUpdateEmail",
|
||||
}),
|
||||
},
|
||||
getPersonalBests: {
|
||||
summary: "get personal bests",
|
||||
|
@ -443,9 +458,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetPersonalBestsResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
deletePersonalBests: {
|
||||
summary: "delete personal bests",
|
||||
|
@ -456,9 +472,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userClearPB",
|
||||
}),
|
||||
},
|
||||
optOutOfLeaderboards: {
|
||||
summary: "leaderboards opt out",
|
||||
|
@ -469,9 +486,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userOptOutOfLeaderboards",
|
||||
}),
|
||||
},
|
||||
addResultFilterPreset: {
|
||||
summary: "add result filter preset",
|
||||
|
@ -482,6 +500,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: AddResultFilterPresetResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomFilterAdd",
|
||||
}),
|
||||
},
|
||||
removeResultFilterPreset: {
|
||||
summary: "remove result filter preset",
|
||||
|
@ -493,6 +514,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomFilterRemove",
|
||||
}),
|
||||
},
|
||||
getTags: {
|
||||
summary: "get tags",
|
||||
|
@ -502,9 +526,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetTagsResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userTagsGet",
|
||||
}),
|
||||
},
|
||||
createTag: {
|
||||
summary: "add tag",
|
||||
|
@ -515,6 +540,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: AddTagResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsAdd",
|
||||
}),
|
||||
},
|
||||
editTag: {
|
||||
summary: "edit tag",
|
||||
|
@ -525,6 +553,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsEdit",
|
||||
}),
|
||||
},
|
||||
deleteTag: {
|
||||
summary: "delete tag",
|
||||
|
@ -536,6 +567,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsRemove",
|
||||
}),
|
||||
},
|
||||
deleteTagPersonalBest: {
|
||||
summary: "delete tag PBs",
|
||||
|
@ -547,6 +581,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsClearPB",
|
||||
}),
|
||||
},
|
||||
getCustomThemes: {
|
||||
summary: "get custom themes",
|
||||
|
@ -556,6 +593,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetCustomThemesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeGet",
|
||||
}),
|
||||
},
|
||||
addCustomTheme: {
|
||||
summary: "add custom themes",
|
||||
|
@ -566,6 +606,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: AddCustomThemeResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeAdd",
|
||||
}),
|
||||
},
|
||||
deleteCustomTheme: {
|
||||
summary: "delete custom themes",
|
||||
|
@ -576,6 +619,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeRemove",
|
||||
}),
|
||||
},
|
||||
editCustomTheme: {
|
||||
summary: "edit custom themes",
|
||||
|
@ -586,6 +632,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeEdit",
|
||||
}),
|
||||
},
|
||||
getDiscordOAuth: {
|
||||
summary: "discord oauth",
|
||||
|
@ -595,6 +644,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetDiscordOauthLinkResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordLink",
|
||||
}),
|
||||
},
|
||||
linkDiscord: {
|
||||
summary: "link with discord",
|
||||
|
@ -605,7 +657,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: LinkDiscordResponseSchema,
|
||||
},
|
||||
metadata: {} as EndpointMetadata,
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordLink",
|
||||
}),
|
||||
},
|
||||
unlinkDiscord: {
|
||||
summary: "unlink discord",
|
||||
|
@ -616,6 +670,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordUnlink",
|
||||
}),
|
||||
},
|
||||
getStats: {
|
||||
summary: "get stats",
|
||||
|
@ -625,9 +682,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetStatsResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
setStreakHourOffset: {
|
||||
summary: "set streak hour offset",
|
||||
|
@ -638,6 +696,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "setStreakHourOffset",
|
||||
}),
|
||||
},
|
||||
getFavoriteQuotes: {
|
||||
summary: "get favorite quotes",
|
||||
|
@ -647,6 +708,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetFavoriteQuotesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoriteGet",
|
||||
}),
|
||||
},
|
||||
addQuoteToFavorites: {
|
||||
summary: "add favorite quotes",
|
||||
|
@ -657,6 +721,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoritePost",
|
||||
}),
|
||||
},
|
||||
removeQuoteFromFavorites: {
|
||||
summary: "remove favorite quotes",
|
||||
|
@ -667,6 +734,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoriteDelete",
|
||||
}),
|
||||
},
|
||||
getProfile: {
|
||||
summary: "get profile",
|
||||
|
@ -679,9 +749,10 @@ export const usersContract = c.router(
|
|||
200: GetProfileResponseSchema,
|
||||
404: MonkeyClientError.describe("User not found"),
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userProfileGet",
|
||||
}),
|
||||
},
|
||||
updateProfile: {
|
||||
summary: "update profile",
|
||||
|
@ -692,6 +763,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: UpdateUserProfileResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userProfileUpdate",
|
||||
}),
|
||||
},
|
||||
getInbox: {
|
||||
summary: "get inbox",
|
||||
|
@ -701,6 +775,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetUserInboxResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userMailGet",
|
||||
}),
|
||||
},
|
||||
updateInbox: {
|
||||
summary: "update inbox",
|
||||
|
@ -711,6 +788,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userMailUpdate",
|
||||
}),
|
||||
},
|
||||
report: {
|
||||
summary: "report user",
|
||||
|
@ -721,6 +801,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteReportSubmit",
|
||||
}),
|
||||
},
|
||||
verificationEmail: {
|
||||
summary: "send verification email",
|
||||
|
@ -730,9 +813,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { noCache: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userRequestVerificationEmail",
|
||||
}),
|
||||
},
|
||||
forgotPasswordEmail: {
|
||||
summary: "send forgot password email",
|
||||
|
@ -743,9 +827,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userForgotPasswordEmail",
|
||||
}),
|
||||
},
|
||||
revokeAllTokens: {
|
||||
summary: "revoke all tokens",
|
||||
|
@ -756,9 +841,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true, noCache: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userRevokeAllTokens",
|
||||
}),
|
||||
},
|
||||
getTestActivity: {
|
||||
summary: "get test activity",
|
||||
|
@ -768,6 +854,9 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetTestActivityResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTestActivity",
|
||||
}),
|
||||
},
|
||||
getCurrentTestActivity: {
|
||||
summary: "get current test activity",
|
||||
|
@ -778,9 +867,10 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetCurrentTestActivityResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userCurrentTestActivity",
|
||||
}),
|
||||
},
|
||||
getStreak: {
|
||||
summary: "get streak",
|
||||
|
@ -790,17 +880,18 @@ export const usersContract = c.router(
|
|||
responses: {
|
||||
200: GetStreakResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
} as EndpointMetadata,
|
||||
rateLimit: "userStreak",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/users",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
metadata: meta({
|
||||
openApiTags: "users",
|
||||
} as EndpointMetadata,
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ importers:
|
|||
specifier: 1.2.1
|
||||
version: 1.2.1
|
||||
prettier:
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8
|
||||
turbo:
|
||||
specifier: 2.0.12
|
||||
version: 2.0.12
|
||||
|
@ -87,8 +87,8 @@ importers:
|
|||
specifier: 4.19.2
|
||||
version: 4.19.2
|
||||
express-rate-limit:
|
||||
specifier: 6.2.1
|
||||
version: 6.2.1(express@4.19.2)
|
||||
specifier: 7.4.0
|
||||
version: 7.4.0(express@4.19.2)
|
||||
firebase-admin:
|
||||
specifier: 12.0.0
|
||||
version: 12.0.0(encoding@0.1.13)
|
||||
|
@ -4557,11 +4557,11 @@ packages:
|
|||
exponential-backoff@3.1.1:
|
||||
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
|
||||
|
||||
express-rate-limit@6.2.1:
|
||||
resolution: {integrity: sha512-22ovnpEiKR5iAMXDOQ7A6aOvb078JLvoHGlyrrWBl3PeJ34coyakaviPelj4Nc8d+yDoVIWYmaUNP5aYT4ICDQ==}
|
||||
engines: {node: '>= 14.5.0'}
|
||||
express-rate-limit@7.4.0:
|
||||
resolution: {integrity: sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: ^4
|
||||
express: 4 || 5 || ^5.0.0-beta.1
|
||||
|
||||
express@4.19.2:
|
||||
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
||||
|
@ -7365,8 +7365,8 @@ packages:
|
|||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@2.5.1:
|
||||
resolution: {integrity: sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==}
|
||||
prettier@2.8.8:
|
||||
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
|
@ -14221,7 +14221,7 @@ snapshots:
|
|||
exponential-backoff@3.1.1:
|
||||
optional: true
|
||||
|
||||
express-rate-limit@6.2.1(express@4.19.2):
|
||||
express-rate-limit@7.4.0(express@4.19.2):
|
||||
dependencies:
|
||||
express: 4.19.2
|
||||
|
||||
|
@ -17662,7 +17662,7 @@ snapshots:
|
|||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@2.5.1: {}
|
||||
prettier@2.8.8: {}
|
||||
|
||||
pretty-bytes@5.6.0: {}
|
||||
|
||||
|
|
Loading…
Reference in a new issue