mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-02 03:25:22 +08:00
parent
73f379ae8a
commit
e2d574444a
30 changed files with 654 additions and 234 deletions
|
@ -1,4 +1,4 @@
|
|||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { randomBytes } from "crypto";
|
||||
import { hash } from "bcrypt";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
|
194
backend/__tests__/api/controllers/configuration.spec.ts
Normal file
194
backend/__tests__/api/controllers/configuration.spec.ts
Normal file
|
@ -0,0 +1,194 @@
|
|||
import request from "supertest";
|
||||
import app from "../../../src/app";
|
||||
import {
|
||||
BASE_CONFIGURATION,
|
||||
CONFIGURATION_FORM_SCHEMA,
|
||||
} from "../../../src/constants/base-configuration";
|
||||
import * as Configuration from "../../../src/init/configuration";
|
||||
import type { Configuration as ConfigurationType } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as Misc from "../../../src/utils/misc";
|
||||
import { DecodedIdToken } from "firebase-admin/auth";
|
||||
import * as AuthUtils from "../../../src/utils/auth";
|
||||
import * as AdminUuids from "../../../src/dal/admin-uids";
|
||||
|
||||
const mockApp = request(app);
|
||||
const uid = new ObjectId().toHexString();
|
||||
const mockDecodedToken = {
|
||||
uid,
|
||||
email: "newuser@mail.com",
|
||||
iat: 0,
|
||||
} as DecodedIdToken;
|
||||
|
||||
describe("Configuration Controller", () => {
|
||||
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");
|
||||
const verifyIdTokenMock = vi.spyOn(AuthUtils, "verifyIdToken");
|
||||
const isAdminMock = vi.spyOn(AdminUuids, "isAdmin");
|
||||
|
||||
beforeEach(() => {
|
||||
isAdminMock.mockReset();
|
||||
verifyIdTokenMock.mockReset();
|
||||
isDevEnvironmentMock.mockReset();
|
||||
|
||||
isDevEnvironmentMock.mockReturnValue(true);
|
||||
isAdminMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("getConfiguration", () => {
|
||||
it("should get without authentication", async () => {
|
||||
//GIVEN
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp.get("/configuration").expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Configuration retrieved",
|
||||
data: BASE_CONFIGURATION,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getConfigurationSchema", () => {
|
||||
it("should get without authentication on dev", async () => {
|
||||
//GIVEN
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp.get("/configuration/schema").expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Configuration schema retrieved",
|
||||
data: CONFIGURATION_FORM_SCHEMA,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail without authentication on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp.get("/configuration/schema").expect(401);
|
||||
});
|
||||
it("should get with authentication on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.get("/configuration/schema")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Configuration schema retrieved",
|
||||
data: CONFIGURATION_FORM_SCHEMA,
|
||||
});
|
||||
|
||||
expect(verifyIdTokenMock).toHaveBeenCalled();
|
||||
});
|
||||
it("should fail with non-admin user on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
|
||||
isAdminMock.mockResolvedValue(false);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.get("/configuration/schema")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.expect(403);
|
||||
|
||||
//THEN
|
||||
expect(body.message).toEqual("You don't have permission to do this.");
|
||||
expect(verifyIdTokenMock).toHaveBeenCalled();
|
||||
expect(isAdminMock).toHaveBeenCalledWith(uid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateConfiguration", () => {
|
||||
const patchConfigurationMock = vi.spyOn(
|
||||
Configuration,
|
||||
"patchConfiguration"
|
||||
);
|
||||
beforeEach(() => {
|
||||
patchConfigurationMock.mockReset();
|
||||
patchConfigurationMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("should update without authentication on dev", async () => {
|
||||
//GIVEN
|
||||
const patch = {
|
||||
users: {
|
||||
premium: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as Partial<ConfigurationType>;
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.patch("/configuration")
|
||||
.send({ configuration: patch })
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toEqual({
|
||||
message: "Configuration updated",
|
||||
data: null,
|
||||
});
|
||||
|
||||
expect(patchConfigurationMock).toHaveBeenCalledWith(patch);
|
||||
});
|
||||
|
||||
it("should fail update without authentication on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
|
||||
//WHEN
|
||||
await request(app)
|
||||
.patch("/configuration")
|
||||
.send({ configuration: {} })
|
||||
.expect(401);
|
||||
|
||||
//THEN
|
||||
expect(patchConfigurationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should update with authentication on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.patch("/configuration")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ configuration: {} })
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(patchConfigurationMock).toHaveBeenCalled();
|
||||
expect(verifyIdTokenMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fail for non admin users on prod", async () => {
|
||||
//GIVEN
|
||||
isDevEnvironmentMock.mockReturnValue(false);
|
||||
isAdminMock.mockResolvedValue(false);
|
||||
verifyIdTokenMock.mockResolvedValue(mockDecodedToken);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.patch("/configuration")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ configuration: {} })
|
||||
.expect(403);
|
||||
|
||||
//THEN
|
||||
expect(patchConfigurationMock).not.toHaveBeenCalled();
|
||||
expect(isAdminMock).toHaveBeenCalledWith(uid);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -86,22 +86,15 @@ describe("middlewares/auth", () => {
|
|||
requireFreshToken: true,
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await authenticateRequest(
|
||||
expect(() =>
|
||||
authenticateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
} catch (e) {
|
||||
result = e;
|
||||
}
|
||||
|
||||
expect(result.message).toBe(
|
||||
)
|
||||
).rejects.toThrowError(
|
||||
"Unauthorized\nStack: This endpoint requires a fresh token"
|
||||
);
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should allow the request if token is fresh", async () => {
|
||||
Date.now = vi.fn(() => 10000);
|
||||
|
@ -321,7 +314,7 @@ describe("middlewares/auth", () => {
|
|||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should fail wit apeKey if apeKey is not supported", async () => {
|
||||
it("should fail with apeKey if apeKey is not supported", async () => {
|
||||
//WHEN
|
||||
await expect(() =>
|
||||
authenticate(
|
||||
|
@ -332,6 +325,22 @@ describe("middlewares/auth", () => {
|
|||
|
||||
//THEN
|
||||
});
|
||||
it("should fail with apeKey if apeKeys are disabled", async () => {
|
||||
//GIVEN
|
||||
|
||||
//@ts-expect-error
|
||||
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;
|
||||
|
||||
//WHEN
|
||||
await expect(() =>
|
||||
authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: false }
|
||||
)
|
||||
).rejects.toThrowError("ApeKeys are not being accepted at this time");
|
||||
|
||||
//THEN
|
||||
});
|
||||
it("should allow the request with authentation on public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate({}, { isPublic: true });
|
||||
|
@ -489,6 +498,112 @@ describe("middlewares/auth", () => {
|
|||
expect.anything()
|
||||
);
|
||||
});
|
||||
it("should allow the request with authentation on dev public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate({}, { isPublicOnDev: true });
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("Bearer");
|
||||
expect(decodedToken?.email).toBe(mockDecodedToken.email);
|
||||
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should allow the request without authentication on dev public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate(
|
||||
{ headers: {} },
|
||||
{ isPublicOnDev: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("None");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("None");
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("should allow the request with apeKey on dev public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: true, isPublicOnDev: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("ApeKey");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("should allow with apeKey if apeKeys are disabled on dev public endpoint", async () => {
|
||||
//GIVEN
|
||||
|
||||
//@ts-expect-error
|
||||
mockRequest.ctx.configuration.apeKeys.acceptKeys = false;
|
||||
|
||||
//WHEN
|
||||
const result = await authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: true, isPublicOnDev: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("ApeKey");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("should allow the request with authentation on dev public endpoint in production", async () => {
|
||||
//WHEN
|
||||
isDevModeMock.mockReturnValue(false);
|
||||
const result = await authenticate({}, { isPublicOnDev: true });
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("Bearer");
|
||||
expect(decodedToken?.email).toBe(mockDecodedToken.email);
|
||||
expect(decodedToken?.uid).toBe(mockDecodedToken.uid);
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should fail without authentication on dev public endpoint in production", async () => {
|
||||
//WHEN
|
||||
isDevModeMock.mockReturnValue(false);
|
||||
|
||||
//THEN
|
||||
await expect(() =>
|
||||
authenticate({ headers: {} }, { isPublicOnDev: true })
|
||||
).rejects.toThrowError("Unauthorized");
|
||||
});
|
||||
it("should allow with apeKey on dev public endpoint in production", async () => {
|
||||
//WHEN
|
||||
isDevModeMock.mockReturnValue(false);
|
||||
const result = await authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: true, isPublicOnDev: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("ApeKey");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey");
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -53,8 +53,8 @@ export function getOpenApi(): OpenAPIObject {
|
|||
{
|
||||
name: "configs",
|
||||
description:
|
||||
"User specific configurations like test settings, theme or tags.",
|
||||
"x-displayName": "User configuration",
|
||||
"User specific configs like test settings, theme or tags.",
|
||||
"x-displayName": "User configs",
|
||||
"x-public": "no",
|
||||
},
|
||||
{
|
||||
|
@ -99,6 +99,12 @@ export function getOpenApi(): OpenAPIObject {
|
|||
"x-displayName": "Admin",
|
||||
"x-public": "no",
|
||||
},
|
||||
{
|
||||
name: "configuration",
|
||||
description: "Server configuration",
|
||||
"x-displayName": "Server configuration",
|
||||
"x-public": "yes",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ToggleBanResponse,
|
||||
} from "@monkeytype/contracts/admin";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addImportantLog } from "../../dal/logs";
|
||||
|
||||
export async function test(
|
||||
|
|
|
@ -1,32 +1,38 @@
|
|||
import * as Configuration from "../../init/configuration";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import { MonkeyResponse2 } from "../../utils/monkey-response";
|
||||
import { CONFIGURATION_FORM_SCHEMA } from "../../constants/base-configuration";
|
||||
import {
|
||||
ConfigurationSchemaResponse,
|
||||
GetConfigurationResponse,
|
||||
PatchConfigurationRequest,
|
||||
} from "@monkeytype/contracts/configuration";
|
||||
import MonkeyError from "../../utils/error";
|
||||
|
||||
export async function getConfiguration(
|
||||
_req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
_req: MonkeyTypes.Request2
|
||||
): Promise<GetConfigurationResponse> {
|
||||
const currentConfiguration = await Configuration.getLiveConfiguration();
|
||||
return new MonkeyResponse("Configuration retrieved", currentConfiguration);
|
||||
return new MonkeyResponse2("Configuration retrieved", currentConfiguration);
|
||||
}
|
||||
|
||||
export async function getSchema(
|
||||
_req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
return new MonkeyResponse(
|
||||
_req: MonkeyTypes.Request2
|
||||
): Promise<ConfigurationSchemaResponse> {
|
||||
return new MonkeyResponse2(
|
||||
"Configuration schema retrieved",
|
||||
CONFIGURATION_FORM_SCHEMA
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateConfiguration(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
req: MonkeyTypes.Request2<undefined, PatchConfigurationRequest>
|
||||
): Promise<MonkeyResponse2> {
|
||||
const { configuration } = req.body;
|
||||
const success = await Configuration.patchConfiguration(configuration);
|
||||
|
||||
if (!success) {
|
||||
return new MonkeyResponse("Configuration update failed", {}, 500);
|
||||
throw new MonkeyError(500, "Configuration update failed");
|
||||
}
|
||||
|
||||
return new MonkeyResponse("Configuration updated");
|
||||
return new MonkeyResponse2("Configuration updated", null);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
GetWeeklyXpLeaderboardResponse,
|
||||
LanguageAndModeQuery,
|
||||
} from "@monkeytype/contracts/leaderboards";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
export async function getLeaderboard(
|
||||
req: MonkeyTypes.Request2<GetLeaderboardQuery>
|
||||
|
|
|
@ -36,7 +36,7 @@ import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
|
|||
import { UAParser } from "ua-parser-js";
|
||||
import { canFunboxGetPb } from "../../utils/pb";
|
||||
import { buildDbResult, replaceLegacyValues } from "../../utils/result";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addLog } from "../../dal/logs";
|
||||
import {
|
||||
AddResultRequest,
|
||||
|
|
|
@ -1,43 +1,25 @@
|
|||
import joi from "joi";
|
||||
import { Router } from "express";
|
||||
import * as ConfigurationController from "../controllers/configuration";
|
||||
import { authenticateRequest } from "../../middlewares/auth";
|
||||
import { adminLimit } from "../../middlewares/rate-limit";
|
||||
import { asyncHandler, useInProduction } from "../../middlewares/utility";
|
||||
import { configurationContract } from "@monkeytype/contracts/configuration";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import { checkIfUserIsAdmin } from "../../middlewares/permission";
|
||||
import { validateRequest } from "../../middlewares/validation";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import * as ConfigurationController from "../controllers/configuration";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
|
||||
const router = Router();
|
||||
const s = initServer();
|
||||
|
||||
router.get("/", asyncHandler(ConfigurationController.getConfiguration));
|
||||
export default s.router(configurationContract, {
|
||||
get: {
|
||||
handler: async (r) =>
|
||||
callController(ConfigurationController.getConfiguration)(r),
|
||||
},
|
||||
|
||||
router.patch(
|
||||
"/",
|
||||
adminLimit,
|
||||
useInProduction([
|
||||
authenticateRequest({
|
||||
noCache: true,
|
||||
}),
|
||||
checkIfUserIsAdmin(),
|
||||
]),
|
||||
validateRequest({
|
||||
body: {
|
||||
configuration: joi.object(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(ConfigurationController.updateConfiguration)
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/schema",
|
||||
adminLimit,
|
||||
useInProduction([
|
||||
authenticateRequest({
|
||||
noCache: true,
|
||||
}),
|
||||
checkIfUserIsAdmin(),
|
||||
]),
|
||||
asyncHandler(ConfigurationController.getSchema)
|
||||
);
|
||||
|
||||
export default router;
|
||||
update: {
|
||||
middleware: [checkIfUserIsAdmin(), RateLimit.adminLimit],
|
||||
handler: async (r) =>
|
||||
callController(ConfigurationController.updateConfiguration)(r),
|
||||
},
|
||||
getSchema: {
|
||||
middleware: [checkIfUserIsAdmin(), RateLimit.adminLimit],
|
||||
handler: async (r) => callController(ConfigurationController.getSchema)(r),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -56,6 +56,7 @@ const router = s.router(contract, {
|
|||
public: publicStats,
|
||||
leaderboards,
|
||||
results,
|
||||
configuration,
|
||||
});
|
||||
|
||||
export function addApiRoutes(app: Application): void {
|
||||
|
@ -145,13 +146,16 @@ function applyDevApiRoutes(app: Application): void {
|
|||
}
|
||||
|
||||
function applyApiRoutes(app: Application): void {
|
||||
// Cannot be added to the route map because it needs to be added before the maintenance handler
|
||||
app.use("/configuration", configuration);
|
||||
|
||||
addSwaggerMiddlewares(app);
|
||||
|
||||
//TODO move to globalMiddleware when all endpoints use tsrest
|
||||
app.use(
|
||||
(req: MonkeyTypes.Request, res: Response, next: NextFunction): void => {
|
||||
if (req.path.startsWith("/configuration")) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const inMaintenance =
|
||||
process.env["MAINTENANCE"] === "true" ||
|
||||
req.ctx.configuration.maintenance;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
/**
|
||||
* This is the base schema for the configuration of the API backend.
|
||||
|
|
|
@ -17,7 +17,6 @@ import { UTCDate } from "@date-fns/utc";
|
|||
import {
|
||||
AllRewards,
|
||||
Badge,
|
||||
Configuration,
|
||||
CustomTheme,
|
||||
MonkeyMail,
|
||||
UserInventory,
|
||||
|
@ -33,6 +32,7 @@ import {
|
|||
import { addImportantLog } from "./logs";
|
||||
import { ResultFilters } from "@monkeytype/contracts/schemas/users";
|
||||
import { Result as ResultType } from "@monkeytype/contracts/schemas/results";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
|
|
|
@ -4,14 +4,15 @@ import { ObjectId } from "mongodb";
|
|||
import Logger from "../utils/logger";
|
||||
import { identity } from "../utils/misc";
|
||||
import { BASE_CONFIGURATION } from "../constants/base-configuration";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import { addLog } from "../dal/logs";
|
||||
import { PartialConfiguration } from "@monkeytype/contracts/configuration";
|
||||
|
||||
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
|
||||
|
||||
function mergeConfigurations(
|
||||
baseConfiguration: Configuration,
|
||||
liveConfiguration: Partial<Configuration>
|
||||
liveConfiguration: PartialConfiguration
|
||||
): void {
|
||||
if (
|
||||
!_.isPlainObject(baseConfiguration) ||
|
||||
|
@ -111,7 +112,7 @@ async function pushConfiguration(configuration: Configuration): Promise<void> {
|
|||
}
|
||||
|
||||
export async function patchConfiguration(
|
||||
configurationUpdates: Partial<Configuration>
|
||||
configurationUpdates: PartialConfiguration
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currentConfiguration = _.cloneDeep(configuration);
|
||||
|
|
|
@ -15,12 +15,13 @@ import { performance } from "perf_hooks";
|
|||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import { AppRoute, AppRouter } from "@ts-rest/core";
|
||||
import { RequestAuthenticationOptions } from "@monkeytype/contracts/schemas/api";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
||||
isPublic: false,
|
||||
acceptApeKeys: false,
|
||||
requireFreshToken: false,
|
||||
isPublicOnDev: false,
|
||||
};
|
||||
|
||||
export type TsRestRequestWithCtx = {
|
||||
|
@ -73,6 +74,9 @@ async function _authenticateRequestInternal(
|
|||
let token: MonkeyTypes.DecodedToken;
|
||||
let authType = "None";
|
||||
|
||||
const isPublic =
|
||||
options.isPublic || (options.isPublicOnDev && isDevEnvironment());
|
||||
|
||||
const { authorization: authHeader } = req.headers;
|
||||
|
||||
try {
|
||||
|
@ -82,7 +86,7 @@ async function _authenticateRequestInternal(
|
|||
req.ctx.configuration,
|
||||
options
|
||||
);
|
||||
} else if (options.isPublic === true) {
|
||||
} else if (isPublic === true) {
|
||||
token = {
|
||||
type: "None",
|
||||
uid: "",
|
||||
|
@ -239,12 +243,17 @@ async function authenticateWithApeKey(
|
|||
configuration: Configuration,
|
||||
options: RequestAuthenticationOptions
|
||||
): Promise<MonkeyTypes.DecodedToken> {
|
||||
if (!configuration.apeKeys.acceptKeys) {
|
||||
throw new MonkeyError(503, "ApeKeys are not being accepted at this time");
|
||||
}
|
||||
const isPublic =
|
||||
options.isPublic || (options.isPublicOnDev && isDevEnvironment());
|
||||
|
||||
if (!options.acceptApeKeys && !options.isPublic) {
|
||||
throw new MonkeyError(401, "This endpoint does not accept ApeKeys");
|
||||
if (!isPublic) {
|
||||
if (!configuration.apeKeys.acceptKeys) {
|
||||
throw new MonkeyError(503, "ApeKeys are not being accepted at this time");
|
||||
}
|
||||
|
||||
if (!options.acceptApeKeys) {
|
||||
throw new MonkeyError(401, "This endpoint does not accept ApeKeys");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Response, NextFunction, RequestHandler } from "express";
|
||||
import MonkeyError from "../utils/error";
|
||||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
export type ValidationOptions<T> = {
|
||||
criteria: (data: T) => boolean;
|
||||
|
|
|
@ -4,18 +4,32 @@ import type { Response, NextFunction, RequestHandler } from "express";
|
|||
import { getPartialUser } from "../dal/user";
|
||||
import { isAdmin } from "../dal/admin-uids";
|
||||
import type { ValidationOptions } from "./configuration";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import { TsRestRequestWithCtx } from "./auth";
|
||||
import { RequestAuthenticationOptions } from "@monkeytype/contracts/schemas/api";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
|
||||
/**
|
||||
* Check if the user is an admin before handling request.
|
||||
* Note that this middleware must be used after authentication in the middleware stack.
|
||||
*/
|
||||
export function checkIfUserIsAdmin(): RequestHandler {
|
||||
export function checkIfUserIsAdmin<
|
||||
T extends AppRouter | AppRoute
|
||||
>(): TsRestRequestHandler<T> {
|
||||
return async (
|
||||
req: MonkeyTypes.Request,
|
||||
req: TsRestRequestWithCtx,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const options: RequestAuthenticationOptions =
|
||||
req.tsRestRoute["metadata"]?.["authenticationOptions"] ?? {};
|
||||
|
||||
if (options.isPublicOnDev && isDevEnvironment()) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const admin = await isAdmin(uid);
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from "lodash";
|
||||
import type { Request, Response, NextFunction, RequestHandler } from "express";
|
||||
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus";
|
||||
|
||||
export const emptyMiddleware = (
|
||||
|
@ -36,17 +35,6 @@ export function asyncHandler(handler: AsyncHandler): RequestHandler {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the middlewares only in production. Otherwise, uses an empty middleware.
|
||||
*/
|
||||
export function useInProduction(
|
||||
middlewares: RequestHandler[]
|
||||
): RequestHandler[] {
|
||||
return middlewares.map((middleware) =>
|
||||
isDevEnvironment() ? emptyMiddleware : middleware
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* record the client version from the `x-client-version` or ` client-version` header to prometheus
|
||||
*/
|
||||
|
|
|
@ -2,7 +2,7 @@ import LRUCache from "lru-cache";
|
|||
import Logger from "../utils/logger";
|
||||
import { MonkeyQueue } from "./monkey-queue";
|
||||
import { getCurrentDayTimestamp, getCurrentWeekTimestamp } from "../utils/misc";
|
||||
import { ValidModeRule } from "@monkeytype/shared-types";
|
||||
import { ValidModeRule } from "@monkeytype/contracts/schemas/configuration";
|
||||
|
||||
const QUEUE_NAME = "later";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import * as RedisClient from "../init/redis";
|
||||
import LaterQueue from "../queues/later-queue";
|
||||
import { getCurrentWeekTimestamp } from "../utils/misc";
|
||||
|
|
2
backend/src/types/types.d.ts
vendored
2
backend/src/types/types.d.ts
vendored
|
@ -14,7 +14,7 @@ declare namespace MonkeyTypes {
|
|||
};
|
||||
|
||||
export type Context = {
|
||||
configuration: import("@monkeytype/shared-types").Configuration;
|
||||
configuration: import("@monkeytype/contracts/schemas/configuration").Configuration;
|
||||
decodedToken: DecodedToken;
|
||||
};
|
||||
|
||||
|
|
|
@ -2,12 +2,16 @@ import _, { omit } from "lodash";
|
|||
import * as RedisClient from "../init/redis";
|
||||
import LaterQueue from "../queues/later-queue";
|
||||
import { getCurrentDayTimestamp, matchesAPattern, kogascore } from "./misc";
|
||||
import { Configuration, ValidModeRule } from "@monkeytype/shared-types";
|
||||
import {
|
||||
Configuration,
|
||||
ValidModeRule,
|
||||
} from "@monkeytype/contracts/schemas/configuration";
|
||||
import {
|
||||
DailyLeaderboardRank,
|
||||
LeaderboardEntry,
|
||||
} from "@monkeytype/contracts/schemas/leaderboards";
|
||||
import MonkeyError from "./error";
|
||||
import { Mode, Mode2 } from "@monkeytype/contracts/schemas/shared";
|
||||
|
||||
const dailyLeaderboardNamespace = "monkeytype:dailyleaderboard";
|
||||
const scoresNamespace = `${dailyLeaderboardNamespace}:scores`;
|
||||
|
@ -221,14 +225,14 @@ function isValidModeRule(
|
|||
|
||||
export function getDailyLeaderboard(
|
||||
language: string,
|
||||
mode: string,
|
||||
mode2: string,
|
||||
mode: Mode,
|
||||
mode2: Mode2<Mode>,
|
||||
dailyLeaderboardsConfig: Configuration["dailyLeaderboards"],
|
||||
customTimestamp = -1
|
||||
): DailyLeaderboard | null {
|
||||
const { validModeRules, enabled } = dailyLeaderboardsConfig;
|
||||
|
||||
const modeRule = { language, mode, mode2 };
|
||||
const modeRule: ValidModeRule = { language, mode, mode2 };
|
||||
const isValidMode = isValidModeRule(modeRule, validModeRules);
|
||||
|
||||
if (!enabled || !isValidMode) {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { Configuration } from "@monkeytype/shared-types";
|
||||
|
||||
export default class Root {
|
||||
constructor(private httpClient: Ape.HttpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
async get(): Ape.EndpointResponse<Configuration> {
|
||||
return await this.httpClient.get("/configuration");
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import Quotes from "./quotes";
|
||||
import Users from "./users";
|
||||
import Configuration from "./configuration";
|
||||
import Dev from "./dev";
|
||||
|
||||
export default {
|
||||
Quotes,
|
||||
Users,
|
||||
Configuration,
|
||||
Dev,
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ const Ape = {
|
|||
...tsRestClient,
|
||||
users: new endpoints.Users(httpClient),
|
||||
quotes: new endpoints.Quotes(httpClient),
|
||||
configuration: new endpoints.Configuration(httpClient),
|
||||
dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Configuration } from "@monkeytype/shared-types";
|
||||
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
|
||||
import Ape from ".";
|
||||
|
||||
let config: Configuration | undefined = undefined;
|
||||
|
@ -11,9 +11,9 @@ export async function sync(): Promise<void> {
|
|||
const response = await Ape.configuration.get();
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.error("Could not fetch configuration", response.message);
|
||||
console.error("Could not fetch configuration", response.body.message);
|
||||
return;
|
||||
} else {
|
||||
config = response.data ?? undefined;
|
||||
config = response.body.data ?? undefined;
|
||||
}
|
||||
}
|
||||
|
|
96
packages/contracts/src/configuration.ts
Normal file
96
packages/contracts/src/configuration.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CommonResponses,
|
||||
EndpointMetadata,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./schemas/api";
|
||||
import { ConfigurationSchema } from "./schemas/configuration";
|
||||
|
||||
export const GetConfigurationResponseSchema =
|
||||
responseWithData(ConfigurationSchema);
|
||||
|
||||
export type GetConfigurationResponse = z.infer<
|
||||
typeof GetConfigurationResponseSchema
|
||||
>;
|
||||
|
||||
export const PartialConfigurationSchema = ConfigurationSchema.deepPartial();
|
||||
export type PartialConfiguration = z.infer<typeof PartialConfigurationSchema>;
|
||||
|
||||
export const PatchConfigurationRequestSchema = z
|
||||
.object({
|
||||
configuration: PartialConfigurationSchema.strict(),
|
||||
})
|
||||
.strict();
|
||||
export type PatchConfigurationRequest = z.infer<
|
||||
typeof PatchConfigurationRequestSchema
|
||||
>;
|
||||
|
||||
export const ConfigurationSchemaResponseSchema = responseWithData(z.object({})); //TODO define schema?
|
||||
export type ConfigurationSchemaResponse = z.infer<
|
||||
typeof ConfigurationSchemaResponseSchema
|
||||
>;
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const configurationContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get configuration",
|
||||
description: "Get server configuration",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetConfigurationResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
},
|
||||
update: {
|
||||
summary: "update configuration",
|
||||
description:
|
||||
"Update the server configuration. Only provided values will be updated while the missing values will be unchanged.",
|
||||
method: "PATCH",
|
||||
path: "",
|
||||
body: PatchConfigurationRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
authenticationOptions: {
|
||||
noCache: true,
|
||||
isPublicOnDev: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
},
|
||||
getSchema: {
|
||||
summary: "get configuration schema",
|
||||
description: "Get schema definition of the server configuration.",
|
||||
method: "GET",
|
||||
path: "/schema",
|
||||
responses: {
|
||||
200: ConfigurationSchemaResponseSchema,
|
||||
},
|
||||
metadata: {
|
||||
authenticationOptions: {
|
||||
isPublicOnDev: true,
|
||||
noCache: true,
|
||||
},
|
||||
} as EndpointMetadata,
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configuration",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
openApiTags: "configuration",
|
||||
} as EndpointMetadata,
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
}
|
||||
);
|
|
@ -7,6 +7,7 @@ import { psasContract } from "./psas";
|
|||
import { publicContract } from "./public";
|
||||
import { leaderboardsContract } from "./leaderboards";
|
||||
import { resultsContract } from "./results";
|
||||
import { configurationContract } from "./configuration";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
|
@ -19,4 +20,5 @@ export const contract = c.router({
|
|||
public: publicContract,
|
||||
leaderboards: leaderboardsContract,
|
||||
results: resultsContract,
|
||||
configuration: configurationContract,
|
||||
});
|
||||
|
|
|
@ -8,7 +8,8 @@ export type OpenApiTag =
|
|||
| "psas"
|
||||
| "public"
|
||||
| "leaderboards"
|
||||
| "results";
|
||||
| "results"
|
||||
| "configuration";
|
||||
|
||||
export type EndpointMetadata = {
|
||||
/** Authentication options, by default a bearer token is required. */
|
||||
|
@ -24,6 +25,8 @@ export type RequestAuthenticationOptions = {
|
|||
/** Endpoint requires an authentication token which is younger than one minute. */
|
||||
requireFreshToken?: boolean;
|
||||
noCache?: boolean;
|
||||
/** Allow unauthenticated requests on dev */
|
||||
isPublicOnDev?: boolean;
|
||||
};
|
||||
|
||||
export const MonkeyResponseSchema = z.object({
|
||||
|
|
121
packages/contracts/src/schemas/configuration.ts
Normal file
121
packages/contracts/src/schemas/configuration.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/* ValidModeRuleSchema allows complex rules like `"mode2": "(15|60)"`. We don't want a strict validation here. */
|
||||
export const ValidModeRuleSchema = z
|
||||
.object({
|
||||
language: z.string(),
|
||||
mode: z.string(),
|
||||
mode2: z.string(),
|
||||
})
|
||||
.strict();
|
||||
export type ValidModeRule = z.infer<typeof ValidModeRuleSchema>;
|
||||
|
||||
export const RewardBracketSchema = z
|
||||
.object({
|
||||
minRank: z.number().int().nonnegative(),
|
||||
maxRank: z.number().int().nonnegative(),
|
||||
minReward: z.number().int().nonnegative(),
|
||||
maxReward: z.number().int().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
export type RewardBracket = z.infer<typeof RewardBracketSchema>;
|
||||
|
||||
export const ConfigurationSchema = z.object({
|
||||
maintenance: z.boolean(),
|
||||
dev: z.object({
|
||||
responseSlowdownMs: z.number().int().nonnegative(),
|
||||
}),
|
||||
quotes: z.object({
|
||||
reporting: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxReports: z.number().int().nonnegative(),
|
||||
contentReportLimit: z.number().int().nonnegative(),
|
||||
}),
|
||||
submissionsEnabled: z.boolean(),
|
||||
maxFavorites: z.number().int().nonnegative(),
|
||||
}),
|
||||
results: z.object({
|
||||
savingEnabled: z.boolean(),
|
||||
objectHashCheckEnabled: z.boolean(),
|
||||
filterPresets: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxPresetsPerUser: z.number().int().nonnegative(),
|
||||
}),
|
||||
limits: z.object({
|
||||
regularUser: z.number().int().nonnegative(),
|
||||
premiumUser: z.number().int().nonnegative(),
|
||||
}),
|
||||
maxBatchSize: z.number().int().nonnegative(),
|
||||
}),
|
||||
users: z.object({
|
||||
signUp: z.boolean(),
|
||||
lastHashesCheck: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxHashes: z.number().int().nonnegative(),
|
||||
}),
|
||||
autoBan: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxCount: z.number().int().nonnegative(),
|
||||
maxHours: z.number().int().nonnegative(),
|
||||
}),
|
||||
profiles: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
discordIntegration: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
xp: z.object({
|
||||
enabled: z.boolean(),
|
||||
funboxBonus: z.number(),
|
||||
gainMultiplier: z.number(),
|
||||
maxDailyBonus: z.number(),
|
||||
minDailyBonus: z.number(),
|
||||
streak: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxStreakDays: z.number().nonnegative(),
|
||||
maxStreakMultiplier: z.number(),
|
||||
}),
|
||||
}),
|
||||
inbox: z.object({
|
||||
enabled: z.boolean(),
|
||||
maxMail: z.number().int().nonnegative(),
|
||||
}),
|
||||
premium: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
admin: z.object({
|
||||
endpointsEnabled: z.boolean(),
|
||||
}),
|
||||
apeKeys: z.object({
|
||||
endpointsEnabled: z.boolean(),
|
||||
acceptKeys: z.boolean(),
|
||||
maxKeysPerUser: z.number().int().nonnegative(),
|
||||
apeKeyBytes: z.number().int().nonnegative(),
|
||||
apeKeySaltRounds: z.number().int().nonnegative(),
|
||||
}),
|
||||
rateLimiting: z.object({
|
||||
badAuthentication: z.object({
|
||||
enabled: z.boolean(),
|
||||
penalty: z.number(),
|
||||
flaggedStatusCodes: z.array(z.number().int().nonnegative()),
|
||||
}),
|
||||
}),
|
||||
dailyLeaderboards: z.object({
|
||||
enabled: z.boolean(),
|
||||
leaderboardExpirationTimeInDays: z.number().nonnegative(),
|
||||
maxResults: z.number().int().nonnegative(),
|
||||
validModeRules: z.array(ValidModeRuleSchema),
|
||||
scheduleRewardsModeRules: z.array(ValidModeRuleSchema),
|
||||
topResultsToAnnounce: z.number().int().positive(), // This should never be 0. Setting to zero will announce all results.
|
||||
xpRewardBrackets: z.array(RewardBracketSchema),
|
||||
}),
|
||||
leaderboards: z.object({
|
||||
weeklyXp: z.object({
|
||||
enabled: z.boolean(),
|
||||
expirationTimeInDays: z.number().nonnegative(),
|
||||
xpRewardBrackets: z.array(RewardBracketSchema),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
@ -2,117 +2,6 @@ type PersonalBest = import("@monkeytype/contracts/schemas/shared").PersonalBest;
|
|||
type PersonalBests =
|
||||
import("@monkeytype/contracts/schemas/shared").PersonalBests;
|
||||
|
||||
export type ValidModeRule = {
|
||||
language: string;
|
||||
mode: string;
|
||||
mode2: string;
|
||||
};
|
||||
export type RewardBracket = {
|
||||
minRank: number;
|
||||
maxRank: number;
|
||||
minReward: number;
|
||||
maxReward: number;
|
||||
};
|
||||
|
||||
export type Configuration = {
|
||||
maintenance: boolean;
|
||||
dev: {
|
||||
responseSlowdownMs: number;
|
||||
};
|
||||
quotes: {
|
||||
reporting: {
|
||||
enabled: boolean;
|
||||
maxReports: number;
|
||||
contentReportLimit: number;
|
||||
};
|
||||
submissionsEnabled: boolean;
|
||||
maxFavorites: number;
|
||||
};
|
||||
results: {
|
||||
savingEnabled: boolean;
|
||||
objectHashCheckEnabled: boolean;
|
||||
filterPresets: {
|
||||
enabled: boolean;
|
||||
maxPresetsPerUser: number;
|
||||
};
|
||||
limits: {
|
||||
regularUser: number;
|
||||
premiumUser: number;
|
||||
};
|
||||
maxBatchSize: number;
|
||||
};
|
||||
users: {
|
||||
signUp: boolean;
|
||||
lastHashesCheck: {
|
||||
enabled: boolean;
|
||||
maxHashes: number;
|
||||
};
|
||||
autoBan: {
|
||||
enabled: boolean;
|
||||
maxCount: number;
|
||||
maxHours: number;
|
||||
};
|
||||
profiles: {
|
||||
enabled: boolean;
|
||||
};
|
||||
discordIntegration: {
|
||||
enabled: boolean;
|
||||
};
|
||||
xp: {
|
||||
enabled: boolean;
|
||||
funboxBonus: number;
|
||||
gainMultiplier: number;
|
||||
maxDailyBonus: number;
|
||||
minDailyBonus: number;
|
||||
streak: {
|
||||
enabled: boolean;
|
||||
maxStreakDays: number;
|
||||
maxStreakMultiplier: number;
|
||||
};
|
||||
};
|
||||
inbox: {
|
||||
enabled: boolean;
|
||||
maxMail: number;
|
||||
};
|
||||
premium: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
admin: {
|
||||
endpointsEnabled: boolean;
|
||||
};
|
||||
apeKeys: {
|
||||
endpointsEnabled: boolean;
|
||||
acceptKeys: boolean;
|
||||
maxKeysPerUser: number;
|
||||
apeKeyBytes: number;
|
||||
apeKeySaltRounds: number;
|
||||
};
|
||||
rateLimiting: {
|
||||
badAuthentication: {
|
||||
enabled: boolean;
|
||||
penalty: number;
|
||||
flaggedStatusCodes: number[];
|
||||
};
|
||||
};
|
||||
dailyLeaderboards: {
|
||||
enabled: boolean;
|
||||
leaderboardExpirationTimeInDays: number;
|
||||
maxResults: number;
|
||||
validModeRules: ValidModeRule[];
|
||||
scheduleRewardsModeRules: ValidModeRule[];
|
||||
topResultsToAnnounce: number;
|
||||
xpRewardBrackets: RewardBracket[];
|
||||
};
|
||||
leaderboards: {
|
||||
weeklyXp: {
|
||||
enabled: boolean;
|
||||
expirationTimeInDays: number;
|
||||
xpRewardBrackets: RewardBracket[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type CustomTextLimit = {
|
||||
value: number;
|
||||
mode: import("@monkeytype/contracts/schemas/util").CustomTextLimitMode;
|
||||
|
|
Loading…
Add table
Reference in a new issue