mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
This reverts commit 4c9e949f10
.
This commit is contained in:
parent
4c9e949f10
commit
8f4d291fcf
|
@ -1,4 +1,3 @@
|
|||
backend/build
|
||||
backend/__migration__
|
||||
backend/scripts
|
||||
docker
|
||||
docker
|
4
.github/workflows/monkey-ci.yml
vendored
4
.github/workflows/monkey-ci.yml
vendored
|
@ -69,7 +69,7 @@ jobs:
|
|||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci && (cd shared && npm ci ) && cd backend && npm ci
|
||||
run: npm ci & cd backend && npm ci
|
||||
|
||||
- name: Check pretty
|
||||
run: npm run pretty-code-be
|
||||
|
@ -100,7 +100,7 @@ jobs:
|
|||
run: mv ./firebase-config-example.ts ./firebase-config.ts && cp ./firebase-config.ts ./firebase-config-live.ts
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci && (cd shared && npm ci ) && cd frontend && npm ci
|
||||
run: npm ci & cd frontend && npm ci
|
||||
|
||||
- name: Check pretty
|
||||
run: npm run pretty-code-fe
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
import request from "supertest";
|
||||
import app from "../../../src/app";
|
||||
import * as ConfigDal from "../../../src/dal/config";
|
||||
import { ObjectId } from "mongodb";
|
||||
const mockApp = request(app);
|
||||
|
||||
describe("ConfigController", () => {
|
||||
describe("get config", () => {
|
||||
const getConfigMock = vi.spyOn(ConfigDal, "getConfig");
|
||||
|
||||
afterEach(() => {
|
||||
getConfigMock.mockReset();
|
||||
});
|
||||
|
||||
it("should get the users config", async () => {
|
||||
//GIVEN
|
||||
getConfigMock.mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
uid: "123456789",
|
||||
config: { language: "english" },
|
||||
});
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.get("/configs")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toStrictEqual({
|
||||
message: "Configuration retrieved",
|
||||
data: { language: "english" },
|
||||
});
|
||||
|
||||
expect(getConfigMock).toHaveBeenCalledWith("123456789");
|
||||
});
|
||||
});
|
||||
describe("update config", () => {
|
||||
const saveConfigMock = vi.spyOn(ConfigDal, "saveConfig");
|
||||
|
||||
afterEach(() => {
|
||||
saveConfigMock.mockReset();
|
||||
});
|
||||
|
||||
it("should update the users config", async () => {
|
||||
//GIVEN
|
||||
saveConfigMock.mockResolvedValue({} as any);
|
||||
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.patch("/configs")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.accept("application/json")
|
||||
.send({ language: "english" })
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toStrictEqual({
|
||||
message: "Config updated",
|
||||
data: null,
|
||||
});
|
||||
|
||||
expect(saveConfigMock).toHaveBeenCalledWith("123456789", {
|
||||
language: "english",
|
||||
});
|
||||
});
|
||||
it("should fail with unknown config", async () => {
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.patch("/configs")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.accept("application/json")
|
||||
.send({ unknownValue: "unknown" })
|
||||
.expect(422);
|
||||
|
||||
//THEN
|
||||
expect(body).toStrictEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: [`Unrecognized key(s) in object: 'unknownValue'`],
|
||||
});
|
||||
|
||||
expect(saveConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should fail with invalid configs", async () => {
|
||||
//WHEN
|
||||
const { body } = await mockApp
|
||||
.patch("/configs")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.accept("application/json")
|
||||
.send({ autoSwitchTheme: "yes", confidenceMode: "pretty" })
|
||||
.expect(422);
|
||||
|
||||
//THEN
|
||||
expect(body).toStrictEqual({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: [
|
||||
`"autoSwitchTheme" Expected boolean, received string`,
|
||||
`"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`,
|
||||
],
|
||||
});
|
||||
|
||||
expect(saveConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("delete config", () => {
|
||||
const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig");
|
||||
|
||||
afterEach(() => {
|
||||
deleteConfigMock.mockReset();
|
||||
});
|
||||
|
||||
it("should delete the users config", async () => {
|
||||
//GIVEN
|
||||
deleteConfigMock.mockResolvedValue();
|
||||
|
||||
//WHEN
|
||||
|
||||
const { body } = await mockApp
|
||||
.delete("/configs")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(body).toStrictEqual({
|
||||
message: "Config deleted",
|
||||
data: null,
|
||||
});
|
||||
|
||||
expect(deleteConfigMock).toHaveBeenCalledWith("123456789");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,9 +8,6 @@ import { ObjectId } from "mongodb";
|
|||
import { hashSync } from "bcrypt";
|
||||
import MonkeyError from "../../src/utils/error";
|
||||
import * as Misc from "../../src/utils/misc";
|
||||
import { Metadata, RequestAuthenticationOptions } from "shared/schemas/util";
|
||||
import * as Prometheus from "../../src/utils/prometheus";
|
||||
import { unknown } from "zod";
|
||||
|
||||
const mockDecodedToken: DecodedIdToken = {
|
||||
uid: "123456789",
|
||||
|
@ -34,11 +31,12 @@ const mockApeKey = {
|
|||
vi.spyOn(ApeKeys, "getApeKey").mockResolvedValue(mockApeKey);
|
||||
vi.spyOn(ApeKeys, "updateLastUsedOn").mockResolvedValue();
|
||||
const isDevModeMock = vi.spyOn(Misc, "isDevEnvironment");
|
||||
let mockRequest: Partial<MonkeyTypes.Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let nextFunction: NextFunction;
|
||||
|
||||
describe("middlewares/auth", () => {
|
||||
let mockRequest: Partial<MonkeyTypes.Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let nextFunction: NextFunction;
|
||||
|
||||
beforeEach(async () => {
|
||||
isDevModeMock.mockReturnValue(true);
|
||||
let config = await getCachedConfiguration(true);
|
||||
|
@ -260,253 +258,4 @@ describe("middlewares/auth", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticateTsRestRequest", () => {
|
||||
const prometheusRecordAuthTimeMock = vi.spyOn(Prometheus, "recordAuthTime");
|
||||
const prometheusIncrementAuthMock = vi.spyOn(Prometheus, "incrementAuth");
|
||||
|
||||
beforeEach(() =>
|
||||
[prometheusIncrementAuthMock, prometheusRecordAuthTimeMock].forEach(
|
||||
(it) => it.mockReset()
|
||||
)
|
||||
);
|
||||
|
||||
it("should fail if token is not fresh", async () => {
|
||||
//GIVEN
|
||||
Date.now = vi.fn(() => 60001);
|
||||
|
||||
//WHEN
|
||||
expect(() =>
|
||||
authenticate({}, { requireFreshToken: true })
|
||||
).rejects.toThrowError(
|
||||
"Unauthorized\nStack: This endpoint requires a fresh token"
|
||||
);
|
||||
|
||||
//THEN
|
||||
|
||||
expect(nextFunction).not.toHaveBeenCalled();
|
||||
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
|
||||
expect(prometheusRecordAuthTimeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should allow the request if token is fresh", async () => {
|
||||
//GIVEN
|
||||
Date.now = vi.fn(() => 10000);
|
||||
|
||||
//WHEN
|
||||
const result = await authenticate({}, { requireFreshToken: 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).toHaveBeenCalledOnce();
|
||||
|
||||
expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("Bearer");
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("should allow the request if apeKey is supported", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("ApeKey");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should fail wit apeKey if apeKey is not supported", async () => {
|
||||
//WHEN
|
||||
await expect(() =>
|
||||
authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ acceptApeKeys: false }
|
||||
)
|
||||
).rejects.toThrowError("This endpoint does not accept ApeKeys");
|
||||
|
||||
//THEN
|
||||
});
|
||||
it("should allow the request with authentation on public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate({}, { isPublic: 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 public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate({ headers: {} }, { isPublic: 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 public endpoint", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate(
|
||||
{ headers: { authorization: "ApeKey aWQua2V5" } },
|
||||
{ isPublic: 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 request with Uid on dev", async () => {
|
||||
//WHEN
|
||||
const result = await authenticate({
|
||||
headers: { authorization: "Uid 123" },
|
||||
});
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("Bearer");
|
||||
expect(decodedToken?.email).toBe("");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should allow request with Uid and email on dev", async () => {
|
||||
const result = await authenticate({
|
||||
headers: { authorization: "Uid 123|test@example.com" },
|
||||
});
|
||||
|
||||
//THEN
|
||||
const decodedToken = result.decodedToken;
|
||||
expect(decodedToken?.type).toBe("Bearer");
|
||||
expect(decodedToken?.email).toBe("test@example.com");
|
||||
expect(decodedToken?.uid).toBe("123");
|
||||
expect(nextFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("should fail request with Uid on non-dev", async () => {
|
||||
//GIVEN
|
||||
isDevModeMock.mockReturnValue(false);
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(() =>
|
||||
authenticate({ headers: { authorization: "Uid 123" } })
|
||||
).rejects.toThrow(
|
||||
new MonkeyError(401, "Baerer type uid is not supported")
|
||||
);
|
||||
});
|
||||
it("should fail without authentication", async () => {
|
||||
await expect(() => authenticate({ headers: {} })).rejects.toThrowError(
|
||||
"Unauthorized\nStack: endpoint: /api/v1 no authorization header found"
|
||||
);
|
||||
|
||||
//THEH
|
||||
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
|
||||
"None",
|
||||
"failure",
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it("should fail with empty authentication", async () => {
|
||||
await expect(() =>
|
||||
authenticate({ headers: { authorization: "" } })
|
||||
).rejects.toThrowError(
|
||||
"Unauthorized\nStack: endpoint: /api/v1 no authorization header found"
|
||||
);
|
||||
|
||||
//THEH
|
||||
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
|
||||
"",
|
||||
"failure",
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it("should fail with missing authentication token", async () => {
|
||||
await expect(() =>
|
||||
authenticate({ headers: { authorization: "Bearer" } })
|
||||
).rejects.toThrowError(
|
||||
"Missing authentication token\nStack: authenticateWithAuthHeader"
|
||||
);
|
||||
|
||||
//THEH
|
||||
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
|
||||
"Bearer",
|
||||
"failure",
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it("should fail with unknown authentication scheme", async () => {
|
||||
await expect(() =>
|
||||
authenticate({ headers: { authorization: "unknown format" } })
|
||||
).rejects.toThrowError(
|
||||
'Unknown authentication scheme\nStack: The authentication scheme "unknown" is not implemented'
|
||||
);
|
||||
|
||||
//THEH
|
||||
expect(prometheusIncrementAuthMock).not.toHaveBeenCalled();
|
||||
expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith(
|
||||
"unknown",
|
||||
"failure",
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
it("should record country if provided", async () => {
|
||||
const prometheusRecordRequestCountryMock = vi.spyOn(
|
||||
Prometheus,
|
||||
"recordRequestCountry"
|
||||
);
|
||||
|
||||
await authenticate(
|
||||
{ headers: { "cf-ipcountry": "gb" } },
|
||||
{ isPublic: true }
|
||||
);
|
||||
|
||||
//THEN
|
||||
expect(prometheusRecordRequestCountryMock).toHaveBeenCalledWith(
|
||||
"gb",
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function authenticate(
|
||||
request: Partial<Request>,
|
||||
authenticationOptions?: RequestAuthenticationOptions
|
||||
): Promise<{ decodedToken: MonkeyTypes.DecodedToken }> {
|
||||
const mergedRequest = {
|
||||
...mockRequest,
|
||||
...request,
|
||||
tsRestRoute: {
|
||||
metadata: { authenticationOptions } as Metadata,
|
||||
},
|
||||
} as any;
|
||||
|
||||
await Auth.authenticateTsRestRequest()(
|
||||
mergedRequest,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
return { decodedToken: mergedRequest.ctx.decodedToken };
|
||||
}
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
"./**/*.ts",
|
||||
"./**/*.spec.ts",
|
||||
"./setup-tests.ts",
|
||||
"../../shared/types/**/*.d.ts"
|
||||
"../../shared-types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import * as misc from "../../src/utils/misc";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
describe("Misc Utils", () => {
|
||||
afterAll(() => {
|
||||
|
@ -606,18 +605,4 @@ describe("Misc Utils", () => {
|
|||
expect(misc.formatSeconds(seconds)).toBe(expected);
|
||||
});
|
||||
});
|
||||
describe("replaceObjectId", () => {
|
||||
it("replaces objecId with string", () => {
|
||||
const fromDatabase = {
|
||||
_id: new ObjectId(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
};
|
||||
expect(misc.replaceObjectId(fromDatabase)).toStrictEqual({
|
||||
_id: fromDatabase._id.toHexString(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
2449
backend/package-lock.json
generated
2449
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
|||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run gen-docs && tsc --build",
|
||||
"build": "tsc --build",
|
||||
"watch": "tsc --build --watch",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "npm run build && node ./build/server.js",
|
||||
|
@ -13,8 +13,7 @@
|
|||
"dev": "ts-node-dev --transpile-only --inspect -- ./src/server.ts",
|
||||
"knip": "knip",
|
||||
"docker-db-only": "docker compose -f docker/compose.db-only.yml up",
|
||||
"docker": "docker compose -f docker/compose.yml up",
|
||||
"gen-docs": "ts-node scripts/openapi.ts build/static/api/openapi.json && redocly build-docs -o build/static/api/internal.html internal@v2 && redocly bundle -o build/static/api/public.json public-filter && redocly build-docs -o build/static/api/public.html public@v2"
|
||||
"docker": "docker compose -f docker/compose.yml up"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.19.1",
|
||||
|
@ -22,9 +21,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@date-fns/utc": "1.2.0",
|
||||
"@ts-rest/core": "3.45.2",
|
||||
"@ts-rest/express": "3.45.2",
|
||||
"@ts-rest/open-api": "3.45.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "1.91.1",
|
||||
"chalk": "4.1.2",
|
||||
|
@ -50,18 +46,15 @@
|
|||
"path": "0.12.7",
|
||||
"prom-client": "14.0.1",
|
||||
"rate-limiter-flexible": "2.3.7",
|
||||
"shared": "file:../shared",
|
||||
"simple-git": "3.16.0",
|
||||
"string-similarity": "4.0.4",
|
||||
"swagger-stats": "0.99.5",
|
||||
"swagger-ui-express": "4.3.0",
|
||||
"ua-parser-js": "0.7.28",
|
||||
"uuid": "9.0.1",
|
||||
"winston": "3.6.0",
|
||||
"zod": "3.23.8"
|
||||
"winston": "3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "1.18.0",
|
||||
"@types/bcrypt": "5.0.0",
|
||||
"@types/cors": "2.8.12",
|
||||
"@types/cron": "1.7.3",
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
extends:
|
||||
- recommended
|
||||
|
||||
apis:
|
||||
internal@v2:
|
||||
root: build/static/api/openapi.json
|
||||
public-filter:
|
||||
root: build/static/api/openapi.json
|
||||
decorators:
|
||||
filter-in:
|
||||
property: x-public
|
||||
value: yes
|
||||
public@v2:
|
||||
root: build/static/api/public.json
|
||||
|
||||
features.openapi:
|
||||
theme:
|
||||
logo:
|
||||
gutter: "10px"
|
||||
colors:
|
||||
primary:
|
||||
main: "#e2b714"
|
||||
border:
|
||||
dark: "#e2b714"
|
||||
light: "#e2b714"
|
||||
error:
|
||||
main: "#da3333"
|
||||
success:
|
||||
main: "#009400"
|
||||
text:
|
||||
primary: "#646669"
|
||||
secondary: "#d1d0c5"
|
||||
warning:
|
||||
main: "#FF00FF"
|
||||
http:
|
||||
delete: "#da3333"
|
||||
post: "#004D94"
|
||||
patch: "#e2b714"
|
||||
get: "#009400"
|
||||
sidebar:
|
||||
backgroundColor: "#323437"
|
||||
textColor: "#d1d0c5"
|
||||
activeTextColor: "#e2b714"
|
||||
rightPanel:
|
||||
backgroundColor: "#323437"
|
||||
textColor: "#d1d0c5"
|
|
@ -1,119 +0,0 @@
|
|||
import { generateOpenApi } from "@ts-rest/open-api";
|
||||
import { contract } from "../../shared/contracts/index";
|
||||
import { writeFileSync, mkdirSync } from "fs";
|
||||
import { EndpointMetadata } from "../../shared/schemas/api";
|
||||
|
||||
type SecurityRequirementObject = {
|
||||
[name: string]: string[];
|
||||
};
|
||||
|
||||
export function getOpenApi() {
|
||||
const openApiDocument = generateOpenApi(
|
||||
contract,
|
||||
{
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "Monkeytype API",
|
||||
description:
|
||||
"Documentation for the public endpoints provided by the Monkeytype API server.\n\nNote that authentication is performed with the Authorization HTTP header in the format `Authorization: ApeKey YOUR_APE_KEY`\n\nThere is a rate limit of `30 requests per minute` across all endpoints with some endpoints being more strict. Rate limit rates are shared across all ape keys.",
|
||||
version: "2.0.0",
|
||||
termsOfService: "https://monkeytype.com/terms-of-service",
|
||||
contact: {
|
||||
name: "Support",
|
||||
email: "support@monkeytype.com",
|
||||
},
|
||||
"x-logo": {
|
||||
url: "https://monkeytype.com/images/mtfulllogo.png",
|
||||
},
|
||||
license: {
|
||||
name: "GPL-3.0",
|
||||
url: "https://www.gnu.org/licenses/gpl-3.0.html",
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://api.monkeytype.com",
|
||||
description: "Production server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
},
|
||||
ApeKey: {
|
||||
type: "http",
|
||||
scheme: "ApeKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
name: "configs",
|
||||
description:
|
||||
"User specific configurations like test settings, theme or tags.",
|
||||
"x-displayName": "User configuration",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
jsonQuery: true,
|
||||
setOperationId: "concatenated-path",
|
||||
operationMapper: (operation, route) => ({
|
||||
...operation,
|
||||
...addAuth(route.metadata as EndpointMetadata),
|
||||
...addTags(route.metadata as EndpointMetadata),
|
||||
}),
|
||||
}
|
||||
);
|
||||
return openApiDocument;
|
||||
}
|
||||
|
||||
function addAuth(metadata: EndpointMetadata | undefined): Object {
|
||||
const auth = metadata?.["authenticationOptions"] ?? {};
|
||||
const security: SecurityRequirementObject[] = [];
|
||||
if (!auth.isPublic === true) {
|
||||
security.push({ BearerAuth: [] });
|
||||
|
||||
if (auth.acceptApeKeys === true) {
|
||||
security.push({ ApeKey: [] });
|
||||
}
|
||||
}
|
||||
|
||||
const includeInPublic = auth.isPublic === true || auth.acceptApeKeys === true;
|
||||
return {
|
||||
"x-public": includeInPublic ? "yes" : "no",
|
||||
security,
|
||||
};
|
||||
}
|
||||
|
||||
function addTags(metadata: EndpointMetadata | undefined): Object {
|
||||
if (metadata === undefined || metadata.openApiTags === undefined) return {};
|
||||
return {
|
||||
tags: Array.isArray(metadata.openApiTags)
|
||||
? metadata.openApiTags
|
||||
: [metadata.openApiTags],
|
||||
};
|
||||
}
|
||||
|
||||
//detect if we run this as a main
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length !== 1) {
|
||||
console.error("Provide filename.");
|
||||
process.exit(1);
|
||||
}
|
||||
const outFile = args[0] as string;
|
||||
|
||||
//create directories if needed
|
||||
const lastSlash = outFile.lastIndexOf("/");
|
||||
if (lastSlash > 1) {
|
||||
const dir = outFile.substring(0, lastSlash);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const openapi = getOpenApi();
|
||||
writeFileSync(args[0] as string, JSON.stringify(openapi, null, 2));
|
||||
}
|
|
@ -1,33 +1,22 @@
|
|||
import { PartialConfig } from "shared/schemas/config";
|
||||
import * as ConfigDAL from "../../dal/config";
|
||||
import { MonkeyResponse2 } from "../../utils/monkey-response";
|
||||
import { GetConfigResponse } from "shared/contracts/configs";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
|
||||
export async function getConfig(
|
||||
req: MonkeyTypes.Request2
|
||||
): Promise<GetConfigResponse> {
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const data = (await ConfigDAL.getConfig(uid))?.config ?? null;
|
||||
|
||||
return new MonkeyResponse2("Configuration retrieved", data);
|
||||
const data = await ConfigDAL.getConfig(uid);
|
||||
return new MonkeyResponse("Configuration retrieved", data);
|
||||
}
|
||||
|
||||
export async function saveConfig(
|
||||
req: MonkeyTypes.Request2<undefined, PartialConfig>
|
||||
): Promise<MonkeyResponse2> {
|
||||
const config = req.body;
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { config } = req.body;
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
await ConfigDAL.saveConfig(uid, config);
|
||||
|
||||
return new MonkeyResponse2("Config updated");
|
||||
}
|
||||
|
||||
export async function deleteConfig(
|
||||
req: MonkeyTypes.Request2
|
||||
): Promise<MonkeyResponse2> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
await ConfigDAL.deleteConfig(uid);
|
||||
return new MonkeyResponse2("Config deleted");
|
||||
return new MonkeyResponse("Config updated");
|
||||
}
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
import { configsContract } from "shared/contracts/configs";
|
||||
import { initServer } from "@ts-rest/express";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import { Router } from "express";
|
||||
import { authenticateRequest } from "../../middlewares/auth";
|
||||
import configSchema from "../schemas/config-schema";
|
||||
import * as ConfigController from "../controllers/config";
|
||||
import { callController } from "../ts-rest-adapter";
|
||||
import * as RateLimit from "../../middlewares/rate-limit";
|
||||
import { asyncHandler } from "../../middlewares/utility";
|
||||
import { validateRequest } from "../../middlewares/validation";
|
||||
|
||||
const s = initServer();
|
||||
export const configsRoutes = s.router(configsContract, {
|
||||
get: {
|
||||
middleware: [RateLimit.configGet],
|
||||
handler: async (r) => callController(ConfigController.getConfig)(r),
|
||||
},
|
||||
const router = Router();
|
||||
|
||||
save: {
|
||||
middleware: [RateLimit.configUpdate],
|
||||
handler: async (r) => callController(ConfigController.saveConfig)(r),
|
||||
},
|
||||
delete: {
|
||||
middleware: [RateLimit.configDelete],
|
||||
handler: async (r) => callController(ConfigController.deleteConfig)(r),
|
||||
},
|
||||
});
|
||||
router.get(
|
||||
"/",
|
||||
authenticateRequest(),
|
||||
RateLimit.configGet,
|
||||
asyncHandler(ConfigController.getConfig)
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/",
|
||||
authenticateRequest(),
|
||||
RateLimit.configUpdate,
|
||||
validateRequest({
|
||||
body: {
|
||||
config: configSchema.required(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(ConfigController.saveConfig)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import _ from "lodash";
|
||||
import { contract } from "shared/contracts/index";
|
||||
import psas from "./psas";
|
||||
import publicStats from "./public";
|
||||
import users from "./users";
|
||||
import { join } from "path";
|
||||
import quotes from "./quotes";
|
||||
import configs from "./configs";
|
||||
import results from "./results";
|
||||
import presets from "./presets";
|
||||
import apeKeys from "./ape-keys";
|
||||
|
@ -20,7 +20,6 @@ import { MonkeyResponse } from "../../utils/monkey-response";
|
|||
import { recordClientVersion } from "../../utils/prometheus";
|
||||
import {
|
||||
Application,
|
||||
IRouter,
|
||||
NextFunction,
|
||||
Response,
|
||||
Router,
|
||||
|
@ -29,12 +28,6 @@ import {
|
|||
import { isDevEnvironment } from "../../utils/misc";
|
||||
import { getLiveConfiguration } from "../../init/configuration";
|
||||
import Logger from "../../utils/logger";
|
||||
import { createExpressEndpoints, initServer } from "@ts-rest/express";
|
||||
import { configsRoutes } from "./configs";
|
||||
import { ZodIssue } from "zod";
|
||||
import { MonkeyValidationError } from "shared/schemas/api";
|
||||
import { addRedocMiddlewares } from "./redoc";
|
||||
import { authenticateTsRestRequest } from "../../middlewares/auth";
|
||||
|
||||
const pathOverride = process.env["API_PATH_OVERRIDE"];
|
||||
const BASE_ROUTE = pathOverride !== undefined ? `/${pathOverride}` : "";
|
||||
|
@ -42,6 +35,7 @@ const APP_START_TIME = Date.now();
|
|||
|
||||
const API_ROUTE_MAP = {
|
||||
"/users": users,
|
||||
"/configs": configs,
|
||||
"/results": results,
|
||||
"/presets": presets,
|
||||
"/psas": psas,
|
||||
|
@ -53,49 +47,11 @@ const API_ROUTE_MAP = {
|
|||
"/webhooks": webhooks,
|
||||
};
|
||||
|
||||
const s = initServer();
|
||||
const router = s.router(contract, {
|
||||
configs: configsRoutes,
|
||||
});
|
||||
|
||||
export function addApiRoutes(app: Application): void {
|
||||
applyDevApiRoutes(app);
|
||||
applyApiRoutes(app);
|
||||
applyTsRestApiRoutes(app);
|
||||
|
||||
app.use(
|
||||
asyncHandler(async (req, _res) => {
|
||||
return new MonkeyResponse(
|
||||
`Unknown request URL (${req.method}: ${req.path})`,
|
||||
null,
|
||||
404
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function applyTsRestApiRoutes(app: IRouter): void {
|
||||
createExpressEndpoints(contract, router, app, {
|
||||
jsonQuery: true,
|
||||
requestValidationErrorHandler(err, req, res, next) {
|
||||
if (err.body?.issues === undefined) return next();
|
||||
const issues = err.body?.issues.map(prettyErrorMessage);
|
||||
res.status(422).json({
|
||||
message: "Invalid request data schema",
|
||||
validationErrors: issues,
|
||||
} as MonkeyValidationError);
|
||||
},
|
||||
globalMiddleware: [authenticateTsRestRequest()],
|
||||
function addApiRoutes(app: Application): void {
|
||||
app.get("/leaderboard", (_req, res) => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
}
|
||||
|
||||
function prettyErrorMessage(issue: ZodIssue | undefined): string {
|
||||
if (issue === undefined) return "";
|
||||
const path = issue.path.length > 0 ? `"${issue.path.join(".")}" ` : "";
|
||||
return `${path}${issue.message}`;
|
||||
}
|
||||
|
||||
function applyDevApiRoutes(app: Application): void {
|
||||
if (isDevEnvironment()) {
|
||||
//disable csp to allow assets to load from unsecured http
|
||||
app.use((req, res, next) => {
|
||||
|
@ -116,14 +72,11 @@ function applyDevApiRoutes(app: Application): void {
|
|||
//enable dev edpoints
|
||||
app.use("/dev", dev);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
addRedocMiddlewares(app);
|
||||
|
||||
app.use(
|
||||
(req: MonkeyTypes.Request, res: Response, next: NextFunction): void => {
|
||||
|
@ -157,10 +110,6 @@ function applyApiRoutes(app: Application): void {
|
|||
})
|
||||
);
|
||||
|
||||
//legacy routes
|
||||
app.get("/leaderboard", (_req, res) => {
|
||||
res.sendStatus(404);
|
||||
});
|
||||
app.get("/psa", (_req, res) => {
|
||||
res.json([
|
||||
{
|
||||
|
@ -175,4 +124,16 @@ function applyApiRoutes(app: Application): void {
|
|||
const apiRoute = `${BASE_ROUTE}${route}`;
|
||||
app.use(apiRoute, router);
|
||||
});
|
||||
|
||||
app.use(
|
||||
asyncHandler(async (req, _res) => {
|
||||
return new MonkeyResponse(
|
||||
`Unknown request URL (${req.method}: ${req.path})`,
|
||||
null,
|
||||
404
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default addApiRoutes;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { Application } from "express";
|
||||
export function addRedocMiddlewares(app: Application): void {
|
||||
app.use("/v2/docs-internal", (req, res) => {
|
||||
res.sendFile("api/internal.html", {
|
||||
root: __dirname + "../../../../build/static",
|
||||
});
|
||||
});
|
||||
app.use("/v2/docs-internal.json", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.sendFile("api/openapi.json", {
|
||||
root: __dirname + "../../../../build/static",
|
||||
});
|
||||
});
|
||||
app.use("/v2/docs", (req, res) => {
|
||||
res.sendFile("api/public.html", {
|
||||
root: __dirname + "../../../../build/static",
|
||||
});
|
||||
});
|
||||
app.use("/v2/docs.json", (req, res) => {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.sendFile("api/public.json", {
|
||||
root: __dirname + "../../../../build/static",
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
import _ from "lodash";
|
||||
import { Application } from "express";
|
||||
import { getMiddleware as getSwaggerMiddleware } from "swagger-stats";
|
||||
import * as swaggerUi from "swagger-ui-express";
|
||||
import internalSwaggerSpec from "../../documentation/internal-swagger.json";
|
||||
import {
|
||||
serve as serveSwagger,
|
||||
setup as setupSwaggerUi,
|
||||
} from "swagger-ui-express";
|
||||
import publicSwaggerSpec from "../../documentation/public-swagger.json";
|
||||
import internalSwaggerSpec from "../../documentation/internal-swagger.json";
|
||||
import { isDevEnvironment } from "../../utils/misc";
|
||||
|
||||
const SWAGGER_UI_OPTIONS = {
|
||||
|
@ -27,11 +31,10 @@ function addSwaggerMiddlewares(app: Application): void {
|
|||
})
|
||||
);
|
||||
|
||||
const options = {};
|
||||
app.use(
|
||||
["/documentation", "/docs"],
|
||||
swaggerUi.serveFiles(publicSwaggerSpec, options),
|
||||
swaggerUi.setup(publicSwaggerSpec, SWAGGER_UI_OPTIONS)
|
||||
serveSwagger,
|
||||
setupSwaggerUi(publicSwaggerSpec, SWAGGER_UI_OPTIONS)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ const CARET_STYLES = [
|
|||
"banana",
|
||||
];
|
||||
|
||||
//TODO replaced
|
||||
const CONFIG_SCHEMA = joi.object({
|
||||
theme: joi.string().max(50).token(),
|
||||
themeLight: joi.string().max(50).token(),
|
||||
|
@ -81,7 +80,7 @@ const CONFIG_SCHEMA = joi.object({
|
|||
.valid("lowercase", "uppercase", "blank", "dynamic"),
|
||||
keymapLayout: joi
|
||||
.string()
|
||||
.regex(/[\w\-_]+/)
|
||||
.regex(/[\w-_]+/)
|
||||
.valid()
|
||||
.max(50),
|
||||
keymapShowTopRow: joi.string().valid("always", "layout", "never"),
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import { AppRoute, AppRouter } from "@ts-rest/core";
|
||||
import { TsRestRequest } from "@ts-rest/express";
|
||||
import { MonkeyResponse2 } from "../utils/monkey-response";
|
||||
export function callController<
|
||||
TRoute extends AppRoute | AppRouter,
|
||||
TQuery,
|
||||
TBody,
|
||||
TParams,
|
||||
TResponse,
|
||||
TStatus = 200
|
||||
>(
|
||||
handler: Handler<TQuery, TBody, TParams, TResponse>
|
||||
): (all: RequestType2<TRoute, TQuery, TBody, TParams>) => Promise<{
|
||||
status: TStatus;
|
||||
body: { message: string; data: TResponse };
|
||||
}> {
|
||||
return async (all) => {
|
||||
const req: MonkeyTypes.Request2<TQuery, TBody, TParams> = {
|
||||
body: all.body as TBody,
|
||||
query: all.query as TQuery,
|
||||
params: all.params as TParams,
|
||||
raw: all.req,
|
||||
ctx: all.req["ctx"],
|
||||
};
|
||||
|
||||
const result = await handler(req);
|
||||
const response = {
|
||||
status: 200 as TStatus,
|
||||
body: {
|
||||
message: result.message,
|
||||
data: result.data as TResponse,
|
||||
},
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
type WithBody<T> = {
|
||||
body: T;
|
||||
};
|
||||
type WithQuery<T> = {
|
||||
query: T;
|
||||
};
|
||||
|
||||
type WithParams<T> = {
|
||||
params: T;
|
||||
};
|
||||
|
||||
type WithoutBody = {
|
||||
body?: never;
|
||||
};
|
||||
type WithoutQuery = {
|
||||
query?: never;
|
||||
};
|
||||
type WithoutParams = {
|
||||
params?: never;
|
||||
};
|
||||
|
||||
type Handler<TQuery, TBody, TParams, TResponse> = (
|
||||
req: MonkeyTypes.Request2<TQuery, TBody, TParams>
|
||||
) => Promise<MonkeyResponse2<TResponse>>;
|
||||
|
||||
type RequestType2<
|
||||
TRoute extends AppRoute | AppRouter,
|
||||
TQuery,
|
||||
TBody,
|
||||
TParams
|
||||
> = {
|
||||
req: TsRestRequest<TRoute>;
|
||||
} & (TQuery extends undefined ? WithoutQuery : WithQuery<TQuery>) &
|
||||
(TBody extends undefined ? WithoutBody : WithBody<TBody>) &
|
||||
(TParams extends undefined ? WithoutParams : WithParams<TParams>);
|
|
@ -1,6 +1,6 @@
|
|||
import cors from "cors";
|
||||
import helmet from "helmet";
|
||||
import { addApiRoutes } from "./api/routes";
|
||||
import addApiRoutes from "./api/routes";
|
||||
import express, { urlencoded, json } from "express";
|
||||
import contextMiddleware from "./middlewares/context";
|
||||
import errorHandlingMiddleware from "./middlewares/error";
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Collection, UpdateResult } from "mongodb";
|
||||
import { UpdateResult } from "mongodb";
|
||||
import * as db from "../init/db";
|
||||
import _ from "lodash";
|
||||
import { Config, PartialConfig } from "shared/schemas/config";
|
||||
|
||||
const configLegacyProperties = [
|
||||
"swapEscAndTab",
|
||||
|
@ -23,19 +22,9 @@ const configLegacyProperties = [
|
|||
"enableAds",
|
||||
];
|
||||
|
||||
type DBConfig = {
|
||||
_id: ObjectId;
|
||||
uid: string;
|
||||
config: PartialConfig;
|
||||
};
|
||||
|
||||
// Export for use in tests
|
||||
export const getConfigCollection = (): Collection<DBConfig> =>
|
||||
db.collection<DBConfig>("configs");
|
||||
|
||||
export async function saveConfig(
|
||||
uid: string,
|
||||
config: Partial<Config>
|
||||
config: SharedTypes.Config
|
||||
): Promise<UpdateResult> {
|
||||
const configChanges = _.mapKeys(config, (_value, key) => `config.${key}`);
|
||||
|
||||
|
@ -43,18 +32,24 @@ export async function saveConfig(
|
|||
_.map(configLegacyProperties, (key) => [`config.${key}`, ""])
|
||||
) as Record<string, "">;
|
||||
|
||||
return await getConfigCollection().updateOne(
|
||||
{ uid },
|
||||
{ $set: configChanges, $unset: unset },
|
||||
{ upsert: true }
|
||||
);
|
||||
return await db
|
||||
.collection<SharedTypes.Config>("configs")
|
||||
.updateOne(
|
||||
{ uid },
|
||||
{ $set: configChanges, $unset: unset },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getConfig(uid: string): Promise<DBConfig | null> {
|
||||
const config = await getConfigCollection().findOne({ uid });
|
||||
export async function getConfig(
|
||||
uid: string
|
||||
): Promise<SharedTypes.Config | null> {
|
||||
const config = await db
|
||||
.collection<SharedTypes.Config>("configs")
|
||||
.findOne({ uid });
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function deleteConfig(uid: string): Promise<void> {
|
||||
await getConfigCollection().deleteOne({ uid });
|
||||
await db.collection<SharedTypes.Config>("configs").deleteOne({ uid });
|
||||
}
|
||||
|
|
|
@ -9,12 +9,17 @@ import {
|
|||
incrementAuth,
|
||||
recordAuthTime,
|
||||
recordRequestCountry,
|
||||
// recordRequestForUid,
|
||||
} from "../utils/prometheus";
|
||||
import crypto from "crypto";
|
||||
import { performance } from "perf_hooks";
|
||||
import { TsRestRequestHandler } from "@ts-rest/express";
|
||||
import { AppRoute, AppRouter } from "@ts-rest/core";
|
||||
import { RequestAuthenticationOptions } from "shared/schemas/api";
|
||||
|
||||
type RequestAuthenticationOptions = {
|
||||
isPublic?: boolean;
|
||||
acceptApeKeys?: boolean;
|
||||
requireFreshToken?: boolean;
|
||||
noCache?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
||||
isPublic: false,
|
||||
|
@ -22,28 +27,7 @@ const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
|
|||
requireFreshToken: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate request based on the auth settings of the route.
|
||||
* By default a Bearer token with user authentication is required.
|
||||
* @returns
|
||||
*/
|
||||
export function authenticateTsRestRequest<
|
||||
T extends AppRouter | AppRoute
|
||||
>(): TsRestRequestHandler<T> {
|
||||
return async (
|
||||
req: MonkeyTypes.RequestTsRest,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...(req.tsRestRoute["metadata"]?.["authenticationOptions"] ?? {}),
|
||||
};
|
||||
return _authenticateRequestInternal(req, _res, next, options);
|
||||
};
|
||||
}
|
||||
|
||||
export function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler {
|
||||
function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler {
|
||||
const options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...authOptions,
|
||||
|
@ -54,78 +38,69 @@ export function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler {
|
|||
_res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void> => {
|
||||
return _authenticateRequestInternal(req, _res, next, options);
|
||||
};
|
||||
}
|
||||
const startTime = performance.now();
|
||||
let token: MonkeyTypes.DecodedToken;
|
||||
let authType = "None";
|
||||
|
||||
async function _authenticateRequestInternal(
|
||||
req: MonkeyTypes.Request | MonkeyTypes.RequestTsRest,
|
||||
_res: Response,
|
||||
next: NextFunction,
|
||||
options: RequestAuthenticationOptions
|
||||
): Promise<void> {
|
||||
const startTime = performance.now();
|
||||
let token: MonkeyTypes.DecodedToken;
|
||||
let authType = "None";
|
||||
const { authorization: authHeader } = req.headers;
|
||||
|
||||
const { authorization: authHeader } = req.headers;
|
||||
try {
|
||||
if (authHeader !== undefined && authHeader !== "") {
|
||||
token = await authenticateWithAuthHeader(
|
||||
authHeader,
|
||||
req.ctx.configuration,
|
||||
options
|
||||
);
|
||||
} else if (options.isPublic === true) {
|
||||
token = {
|
||||
type: "None",
|
||||
uid: "",
|
||||
email: "",
|
||||
};
|
||||
} else {
|
||||
throw new MonkeyError(
|
||||
401,
|
||||
"Unauthorized",
|
||||
`endpoint: ${req.baseUrl} no authorization header found`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (authHeader !== undefined && authHeader !== "") {
|
||||
token = await authenticateWithAuthHeader(
|
||||
authHeader,
|
||||
req.ctx.configuration,
|
||||
options
|
||||
);
|
||||
} else if (options.isPublic === true) {
|
||||
token = {
|
||||
type: "None",
|
||||
uid: "",
|
||||
email: "",
|
||||
incrementAuth(token.type);
|
||||
|
||||
req.ctx = {
|
||||
...req.ctx,
|
||||
decodedToken: token,
|
||||
};
|
||||
} else {
|
||||
throw new MonkeyError(
|
||||
401,
|
||||
"Unauthorized",
|
||||
`endpoint: ${req.baseUrl} no authorization header found`
|
||||
} catch (error) {
|
||||
authType = authHeader?.split(" ")[0] ?? "None";
|
||||
|
||||
recordAuthTime(
|
||||
authType,
|
||||
"failure",
|
||||
Math.round(performance.now() - startTime),
|
||||
req
|
||||
);
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
incrementAuth(token.type);
|
||||
|
||||
req.ctx = {
|
||||
...req.ctx,
|
||||
decodedToken: token,
|
||||
};
|
||||
} catch (error) {
|
||||
authType = authHeader?.split(" ")[0] ?? "None";
|
||||
|
||||
recordAuthTime(
|
||||
authType,
|
||||
"failure",
|
||||
token.type,
|
||||
"success",
|
||||
Math.round(performance.now() - startTime),
|
||||
req
|
||||
);
|
||||
|
||||
return next(error);
|
||||
}
|
||||
recordAuthTime(
|
||||
token.type,
|
||||
"success",
|
||||
Math.round(performance.now() - startTime),
|
||||
req
|
||||
);
|
||||
const country = req.headers["cf-ipcountry"] as string;
|
||||
if (country) {
|
||||
recordRequestCountry(country, req as MonkeyTypes.Request);
|
||||
}
|
||||
|
||||
const country = req.headers["cf-ipcountry"] as string;
|
||||
if (country) {
|
||||
recordRequestCountry(country, req);
|
||||
}
|
||||
// if (req.method !== "OPTIONS" && req?.ctx?.decodedToken?.uid) {
|
||||
// recordRequestForUid(req.ctx.decodedToken.uid);
|
||||
// }
|
||||
|
||||
// if (req.method !== "OPTIONS" && req?.ctx?.decodedToken?.uid) {
|
||||
// recordRequestForUid(req.ctx.decodedToken.uid);
|
||||
// }
|
||||
|
||||
next();
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
async function authenticateWithAuthHeader(
|
||||
|
@ -133,8 +108,24 @@ async function authenticateWithAuthHeader(
|
|||
configuration: SharedTypes.Configuration,
|
||||
options: RequestAuthenticationOptions
|
||||
): Promise<MonkeyTypes.DecodedToken> {
|
||||
if (authHeader === undefined || authHeader === "") {
|
||||
throw new MonkeyError(
|
||||
401,
|
||||
"Missing authentication header",
|
||||
"authenticateWithAuthHeader"
|
||||
);
|
||||
}
|
||||
|
||||
const [authScheme, token] = authHeader.split(" ");
|
||||
|
||||
if (authScheme === undefined) {
|
||||
throw new MonkeyError(
|
||||
401,
|
||||
"Missing authentication scheme",
|
||||
"authenticateWithAuthHeader"
|
||||
);
|
||||
}
|
||||
|
||||
if (token === undefined) {
|
||||
throw new MonkeyError(
|
||||
401,
|
||||
|
@ -143,7 +134,7 @@ async function authenticateWithAuthHeader(
|
|||
);
|
||||
}
|
||||
|
||||
const normalizedAuthScheme = authScheme?.trim();
|
||||
const normalizedAuthScheme = authScheme.trim();
|
||||
|
||||
switch (normalizedAuthScheme) {
|
||||
case "Bearer":
|
||||
|
@ -306,7 +297,7 @@ async function authenticateWithUid(
|
|||
};
|
||||
}
|
||||
|
||||
export function authenticateGithubWebhook(): Handler {
|
||||
function authenticateGithubWebhook(): Handler {
|
||||
return async (
|
||||
req: MonkeyTypes.Request,
|
||||
_res: Response,
|
||||
|
@ -346,3 +337,5 @@ export function authenticateGithubWebhook(): Handler {
|
|||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export { authenticateRequest, authenticateGithubWebhook };
|
||||
|
|
|
@ -120,12 +120,6 @@ export const configGet = rateLimit({
|
|||
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({
|
||||
|
|
22
backend/src/types/types.d.ts
vendored
22
backend/src/types/types.d.ts
vendored
|
@ -1,19 +1,15 @@
|
|||
type ObjectId = import("mongodb").ObjectId;
|
||||
|
||||
type ExpressRequest = import("express").Request;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type TsRestRequest = import("@ts-rest/express").TsRestRequest<any>;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
type AppRoute = import("@ts-rest/core").AppRoute;
|
||||
type AppRouter = import("@ts-rest/core").AppRouter;
|
||||
|
||||
declare namespace MonkeyTypes {
|
||||
export type DecodedToken = {
|
||||
type DecodedToken = {
|
||||
type: "Bearer" | "ApeKey" | "None";
|
||||
uid: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
type Context = {
|
||||
configuration: SharedTypes.Configuration;
|
||||
decodedToken: DecodedToken;
|
||||
};
|
||||
|
@ -22,18 +18,6 @@ declare namespace MonkeyTypes {
|
|||
ctx: Readonly<Context>;
|
||||
} & ExpressRequest;
|
||||
|
||||
type Request2<TQuery = undefined, TBody = undefined, TParams = undefined> = {
|
||||
query: Readonly<TQuery>;
|
||||
body: Readonly<TBody>;
|
||||
params: Readonly<TParams>;
|
||||
ctx: Readonly<Context>;
|
||||
raw: Readonly<TsRestRequest>;
|
||||
};
|
||||
|
||||
type RequestTsRest = {
|
||||
ctx: Readonly<Context>;
|
||||
} & TsRestRequest;
|
||||
|
||||
type DBUser = Omit<
|
||||
SharedTypes.User,
|
||||
| "resultFilterPresets"
|
||||
|
|
|
@ -176,6 +176,7 @@ export class DailyLeaderboard {
|
|||
|
||||
const { leaderboardScoresKey, leaderboardResultsKey } =
|
||||
this.getTodaysLeaderboardKeys();
|
||||
|
||||
// @ts-expect-error
|
||||
const [[, rank], [, count], [, result], [, minScore]] = await connection
|
||||
.multi()
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { isDevEnvironment } from "./misc";
|
||||
import { MonkeyServerErrorType } from "shared/schemas/api";
|
||||
|
||||
class MonkeyError extends Error implements MonkeyServerErrorType {
|
||||
class MonkeyError extends Error {
|
||||
status: number;
|
||||
errorId: string;
|
||||
uid?: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import _, { omit } from "lodash";
|
||||
import _ from "lodash";
|
||||
import uaparser from "ua-parser-js";
|
||||
|
||||
//todo split this file into smaller util files (grouped by functionality)
|
||||
|
@ -306,13 +306,3 @@ export function stringToNumberOrDefault(
|
|||
export function isDevEnvironment(): boolean {
|
||||
return process.env["MODE"] === "dev";
|
||||
}
|
||||
|
||||
export function replaceObjectId<T extends { _id: ObjectId }>(
|
||||
data: T
|
||||
): T & { _id: string } {
|
||||
const result = {
|
||||
_id: data._id.toString(),
|
||||
...omit(data, "_id"),
|
||||
} as T & { _id: string };
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { Response } from "express";
|
||||
import { MonkeyResponseType } from "shared/schemas/api";
|
||||
import { isCustomCode } from "../constants/monkey-status-codes";
|
||||
|
||||
export type MonkeyDataAware<T> = {
|
||||
data: T | null;
|
||||
};
|
||||
//TODO FIX ANYS
|
||||
|
||||
export class MonkeyResponse {
|
||||
|
@ -40,15 +36,3 @@ export function handleMonkeyResponse(
|
|||
|
||||
res.json({ message, data });
|
||||
}
|
||||
|
||||
export class MonkeyResponse2<T = null>
|
||||
implements MonkeyResponseType, MonkeyDataAware<T>
|
||||
{
|
||||
public message: string;
|
||||
public data: T | null;
|
||||
|
||||
constructor(message: string, data: T | null = null) {
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@ export function recordAuthTime(
|
|||
type: string,
|
||||
status: "success" | "failure",
|
||||
time: number,
|
||||
req: MonkeyTypes.Request | MonkeyTypes.RequestTsRest
|
||||
req: MonkeyTypes.Request
|
||||
): void {
|
||||
const reqPath = req.baseUrl + req.route.path;
|
||||
|
||||
|
@ -234,7 +234,7 @@ const requestCountry = new Counter({
|
|||
|
||||
export function recordRequestCountry(
|
||||
country: string,
|
||||
req: MonkeyTypes.Request | MonkeyTypes.RequestTsRest
|
||||
req: MonkeyTypes.Request
|
||||
): void {
|
||||
const reqPath = req.baseUrl + req.route.path;
|
||||
|
||||
|
|
|
@ -17,13 +17,20 @@
|
|||
"noImplicitReturns": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@shared/*": ["../shared-types/*"]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"files": true
|
||||
},
|
||||
"files": ["./src/types/types.d.ts"],
|
||||
"include": ["./src/**/*", "../shared/**/*.d.ts"],
|
||||
"files": [
|
||||
"./src/types/types.d.ts",
|
||||
"../shared-types/types.d.ts",
|
||||
"../shared-types/config.d.ts"
|
||||
],
|
||||
"include": ["./src/**/*", "../shared-types/**/*.d.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
|
|
|
@ -3,7 +3,5 @@ cd .\frontend
|
|||
call npm ci
|
||||
cd ..\backend
|
||||
call npm ci
|
||||
cd ..\shared
|
||||
call npm ci
|
||||
cd ..\
|
||||
PAUSE
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
npm ci &
|
||||
cd ./frontend && npm ci &
|
||||
cd ./backend && npm ci &
|
||||
cd ./shared && npm ci &
|
||||
wait
|
||||
|
|
|
@ -1,544 +0,0 @@
|
|||
import * as Config from "../../src/ts/config";
|
||||
import * as ConfigValidation from "../../src/ts/config-validation";
|
||||
import { CustomThemeColors } from "shared/schemas/config";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
describe("Config", () => {
|
||||
const asyncValidationMock = vi.spyOn(
|
||||
ConfigValidation,
|
||||
"isConfigValueValidAsync"
|
||||
);
|
||||
beforeEach(() => {
|
||||
asyncValidationMock.mockResolvedValue(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
asyncValidationMock.mockReset();
|
||||
});
|
||||
it("setMode", () => {
|
||||
expect(Config.setMode("zen")).toBe(true);
|
||||
expect(Config.setMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setPlaySoundOnError", () => {
|
||||
expect(Config.setPlaySoundOnError("off")).toBe(true);
|
||||
expect(Config.setPlaySoundOnError("1")).toBe(true);
|
||||
expect(Config.setPlaySoundOnError("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setPlaySoundOnClick", () => {
|
||||
expect(Config.setPlaySoundOnClick("off")).toBe(true);
|
||||
expect(Config.setPlaySoundOnClick("15")).toBe(true);
|
||||
expect(Config.setPlaySoundOnClick("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setSoundVolume", () => {
|
||||
expect(Config.setSoundVolume("0.1")).toBe(true);
|
||||
expect(Config.setSoundVolume("1.0")).toBe(true);
|
||||
expect(Config.setSoundVolume("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setDifficulty", () => {
|
||||
expect(Config.setDifficulty("expert")).toBe(true);
|
||||
expect(Config.setDifficulty("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setAccountChart", () => {
|
||||
expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true);
|
||||
//arrays not having 4 values will get [on, on, on, on] as default
|
||||
expect(Config.setAccountChart(["on", "off"] as any)).toBe(true);
|
||||
expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
it("setAccountChartResults", () => {
|
||||
expect(Config.setAccountChartResults(true)).toBe(true);
|
||||
expect(Config.setAccountChartResults("on" as any)).toBe(false);
|
||||
});
|
||||
it("setStopOnError", () => {
|
||||
expect(Config.setStopOnError("off")).toBe(true);
|
||||
expect(Config.setStopOnError("word")).toBe(true);
|
||||
expect(Config.setStopOnError("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setTypingSpeedUnit", () => {
|
||||
expect(Config.setTypingSpeedUnit("wpm")).toBe(true);
|
||||
expect(Config.setTypingSpeedUnit("cpm")).toBe(true);
|
||||
expect(Config.setTypingSpeedUnit("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setPaceCaret", () => {
|
||||
expect(Config.setPaceCaret("average")).toBe(true);
|
||||
expect(Config.setPaceCaret("last")).toBe(true);
|
||||
expect(Config.setPaceCaret("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setMinWpm", () => {
|
||||
expect(Config.setMinWpm("custom")).toBe(true);
|
||||
expect(Config.setMinWpm("off")).toBe(true);
|
||||
expect(Config.setMinWpm("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setMinAcc", () => {
|
||||
expect(Config.setMinAcc("custom")).toBe(true);
|
||||
expect(Config.setMinAcc("off")).toBe(true);
|
||||
expect(Config.setMinAcc("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setMinBurst", () => {
|
||||
expect(Config.setMinBurst("fixed")).toBe(true);
|
||||
expect(Config.setMinBurst("off")).toBe(true);
|
||||
expect(Config.setMinBurst("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setSingleListCommandLine", () => {
|
||||
expect(Config.setSingleListCommandLine("on")).toBe(true);
|
||||
expect(Config.setSingleListCommandLine("manual")).toBe(true);
|
||||
expect(Config.setSingleListCommandLine("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setAds", () => {
|
||||
expect(Config.setAds("on")).toBe(true);
|
||||
expect(Config.setAds("sellout")).toBe(true);
|
||||
expect(Config.setAds("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setRepeatQuotes", () => {
|
||||
expect(Config.setRepeatQuotes("off")).toBe(true);
|
||||
expect(Config.setRepeatQuotes("typing")).toBe(true);
|
||||
expect(Config.setRepeatQuotes("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setOppositeShiftMode", () => {
|
||||
expect(Config.setOppositeShiftMode("on")).toBe(true);
|
||||
expect(Config.setOppositeShiftMode("keymap")).toBe(true);
|
||||
expect(Config.setOppositeShiftMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setCaretStyle", () => {
|
||||
expect(Config.setCaretStyle("banana")).toBe(true);
|
||||
expect(Config.setCaretStyle("block")).toBe(true);
|
||||
expect(Config.setCaretStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setPaceCaretStyle", () => {
|
||||
expect(Config.setPaceCaretStyle("carrot")).toBe(true);
|
||||
expect(Config.setPaceCaretStyle("outline")).toBe(true);
|
||||
expect(Config.setPaceCaretStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setShowAverage", () => {
|
||||
expect(Config.setShowAverage("acc")).toBe(true);
|
||||
expect(Config.setShowAverage("both")).toBe(true);
|
||||
expect(Config.setShowAverage("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setHighlightMode", () => {
|
||||
expect(Config.setHighlightMode("letter")).toBe(true);
|
||||
expect(Config.setHighlightMode("next_three_words")).toBe(true);
|
||||
expect(Config.setHighlightMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setTapeMode", () => {
|
||||
expect(Config.setTapeMode("letter")).toBe(true);
|
||||
expect(Config.setTapeMode("off")).toBe(true);
|
||||
expect(Config.setTapeMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setTimerStyle", () => {
|
||||
expect(Config.setTimerStyle("bar")).toBe(true);
|
||||
expect(Config.setTimerStyle("mini")).toBe(true);
|
||||
expect(Config.setTimerStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setLiveSpeedStyle", () => {
|
||||
expect(Config.setLiveSpeedStyle("text")).toBe(true);
|
||||
expect(Config.setLiveSpeedStyle("mini")).toBe(true);
|
||||
expect(Config.setLiveSpeedStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setLiveAccStyle", () => {
|
||||
expect(Config.setLiveAccStyle("text")).toBe(true);
|
||||
expect(Config.setLiveAccStyle("mini")).toBe(true);
|
||||
expect(Config.setLiveAccStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setLiveBurstStyle", () => {
|
||||
expect(Config.setLiveBurstStyle("text")).toBe(true);
|
||||
expect(Config.setLiveBurstStyle("mini")).toBe(true);
|
||||
expect(Config.setLiveBurstStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setTimerColor", () => {
|
||||
expect(Config.setTimerColor("text")).toBe(true);
|
||||
expect(Config.setTimerColor("sub")).toBe(true);
|
||||
expect(Config.setTimerColor("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setTimerOpacity", () => {
|
||||
expect(Config.setTimerOpacity("1")).toBe(true);
|
||||
expect(Config.setTimerOpacity("0.5")).toBe(true);
|
||||
expect(Config.setTimerOpacity("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setSmoothCaret", () => {
|
||||
expect(Config.setSmoothCaret("fast")).toBe(true);
|
||||
expect(Config.setSmoothCaret("medium")).toBe(true);
|
||||
expect(Config.setSmoothCaret("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setQuickRestartMode", () => {
|
||||
expect(Config.setQuickRestartMode("off")).toBe(true);
|
||||
expect(Config.setQuickRestartMode("tab")).toBe(true);
|
||||
expect(Config.setQuickRestartMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setConfidenceMode", () => {
|
||||
expect(Config.setConfidenceMode("max")).toBe(true);
|
||||
expect(Config.setConfidenceMode("on")).toBe(true);
|
||||
expect(Config.setConfidenceMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setIndicateTypos", () => {
|
||||
expect(Config.setIndicateTypos("below")).toBe(true);
|
||||
expect(Config.setIndicateTypos("off")).toBe(true);
|
||||
expect(Config.setIndicateTypos("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setRandomTheme", () => {
|
||||
expect(Config.setRandomTheme("fav")).toBe(true);
|
||||
expect(Config.setRandomTheme("off")).toBe(true);
|
||||
expect(Config.setRandomTheme("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setKeymapMode", () => {
|
||||
expect(Config.setKeymapMode("next")).toBe(true);
|
||||
expect(Config.setKeymapMode("react")).toBe(true);
|
||||
expect(Config.setKeymapMode("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setKeymapLegendStyle", () => {
|
||||
expect(Config.setKeymapLegendStyle("blank")).toBe(true);
|
||||
expect(Config.setKeymapLegendStyle("lowercase")).toBe(true);
|
||||
expect(Config.setKeymapLegendStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setKeymapStyle", () => {
|
||||
expect(Config.setKeymapStyle("matrix")).toBe(true);
|
||||
expect(Config.setKeymapStyle("split")).toBe(true);
|
||||
expect(Config.setKeymapStyle("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setKeymapShowTopRow", () => {
|
||||
expect(Config.setKeymapShowTopRow("always")).toBe(true);
|
||||
expect(Config.setKeymapShowTopRow("never")).toBe(true);
|
||||
expect(Config.setKeymapShowTopRow("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setCustomBackgroundSize", () => {
|
||||
expect(Config.setCustomBackgroundSize("contain")).toBe(true);
|
||||
expect(Config.setCustomBackgroundSize("cover")).toBe(true);
|
||||
expect(Config.setCustomBackgroundSize("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setCustomBackgroundFilter", () => {
|
||||
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3])).toBe(true);
|
||||
//gets converted
|
||||
expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(true);
|
||||
expect(Config.setCustomBackgroundFilter([] as any)).toBe(false);
|
||||
expect(Config.setCustomBackgroundFilter(["invalid"] as any)).toBe(false);
|
||||
expect(Config.setCustomBackgroundFilter([1, 2, 3, 4, 5, 6] as any)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
it("setMonkeyPowerLevel", () => {
|
||||
expect(Config.setMonkeyPowerLevel("2")).toBe(true);
|
||||
expect(Config.setMonkeyPowerLevel("off")).toBe(true);
|
||||
|
||||
expect(Config.setMonkeyPowerLevel("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setCustomThemeColors", () => {
|
||||
expect(Config.setCustomThemeColors(customThemeColors(10))).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(true);
|
||||
|
||||
expect(Config.setCustomThemeColors([] as any)).toBe(false);
|
||||
expect(Config.setCustomThemeColors(["invalid"] as any)).toBe(false);
|
||||
expect(Config.setCustomThemeColors(customThemeColors(5))).toBe(false);
|
||||
expect(Config.setCustomThemeColors(customThemeColors(11))).toBe(false);
|
||||
|
||||
const tenColors = customThemeColors(10);
|
||||
tenColors[0] = "black";
|
||||
expect(Config.setCustomThemeColors(tenColors)).toBe(false);
|
||||
tenColors[0] = "#123456";
|
||||
expect(Config.setCustomThemeColors(tenColors)).toBe(true);
|
||||
tenColors[0] = "#1234";
|
||||
expect(Config.setCustomThemeColors(tenColors)).toBe(false);
|
||||
});
|
||||
it("setNumbers", () => {
|
||||
testBoolean(Config.setNumbers);
|
||||
});
|
||||
it("setPunctuation", () => {
|
||||
testBoolean(Config.setPunctuation);
|
||||
});
|
||||
it("setBlindMode", () => {
|
||||
testBoolean(Config.setBlindMode);
|
||||
});
|
||||
it("setAccountChartResults", () => {
|
||||
testBoolean(Config.setAccountChartResults);
|
||||
});
|
||||
it("setAccountChartAccuracy", () => {
|
||||
testBoolean(Config.setAccountChartAccuracy);
|
||||
});
|
||||
it("setAccountChartAvg10", () => {
|
||||
testBoolean(Config.setAccountChartAvg10);
|
||||
});
|
||||
it("setAccountChartAvg100", () => {
|
||||
testBoolean(Config.setAccountChartAvg100);
|
||||
});
|
||||
it("setAlwaysShowDecimalPlaces", () => {
|
||||
testBoolean(Config.setAlwaysShowDecimalPlaces);
|
||||
});
|
||||
it("setShowOutOfFocusWarning", () => {
|
||||
testBoolean(Config.setShowOutOfFocusWarning);
|
||||
});
|
||||
it("setAlwaysShowWordsHistory", () => {
|
||||
testBoolean(Config.setAlwaysShowWordsHistory);
|
||||
});
|
||||
it("setCapsLockWarning", () => {
|
||||
testBoolean(Config.setCapsLockWarning);
|
||||
});
|
||||
it("setShowAllLines", () => {
|
||||
testBoolean(Config.setShowAllLines);
|
||||
});
|
||||
it("setQuickEnd", () => {
|
||||
testBoolean(Config.setQuickEnd);
|
||||
});
|
||||
it("setFlipTestColors", () => {
|
||||
testBoolean(Config.setFlipTestColors);
|
||||
});
|
||||
it("setColorfulMode", () => {
|
||||
testBoolean(Config.setColorfulMode);
|
||||
});
|
||||
it("setStrictSpace", () => {
|
||||
testBoolean(Config.setStrictSpace);
|
||||
});
|
||||
it("setHideExtraLetters", () => {
|
||||
testBoolean(Config.setHideExtraLetters);
|
||||
});
|
||||
it("setKeyTips", () => {
|
||||
testBoolean(Config.setKeyTips);
|
||||
});
|
||||
it("setStartGraphsAtZero", () => {
|
||||
testBoolean(Config.setStartGraphsAtZero);
|
||||
});
|
||||
it("setSmoothLineScroll", () => {
|
||||
testBoolean(Config.setSmoothLineScroll);
|
||||
});
|
||||
it("setFreedomMode", () => {
|
||||
testBoolean(Config.setFreedomMode);
|
||||
});
|
||||
it("setAutoSwitchTheme", () => {
|
||||
testBoolean(Config.setAutoSwitchTheme);
|
||||
});
|
||||
it("setCustomTheme", () => {
|
||||
testBoolean(Config.setCustomTheme);
|
||||
});
|
||||
it("setBritishEnglish", () => {
|
||||
testBoolean(Config.setBritishEnglish);
|
||||
});
|
||||
it("setLazyMode", () => {
|
||||
testBoolean(Config.setLazyMode);
|
||||
});
|
||||
it("setMonkey", () => {
|
||||
testBoolean(Config.setMonkey);
|
||||
});
|
||||
it("setBurstHeatmap", () => {
|
||||
testBoolean(Config.setBurstHeatmap);
|
||||
});
|
||||
it("setRepeatedPace", () => {
|
||||
testBoolean(Config.setRepeatedPace);
|
||||
});
|
||||
it("setFavThemes", () => {
|
||||
expect(Config.setFavThemes([])).toBe(true);
|
||||
expect(Config.setFavThemes(["test"])).toBe(true);
|
||||
expect(Config.setFavThemes([stringOfLength(50)])).toBe(true);
|
||||
|
||||
expect(Config.setFavThemes("invalid" as any)).toBe(false);
|
||||
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
|
||||
});
|
||||
it("setFunbox", () => {
|
||||
expect(Config.setFunbox("one")).toBe(true);
|
||||
expect(Config.setFunbox("one#two")).toBe(true);
|
||||
expect(Config.setFunbox("one#two#")).toBe(true);
|
||||
expect(Config.setFunbox(stringOfLength(100))).toBe(true);
|
||||
|
||||
expect(Config.setFunbox(stringOfLength(101))).toBe(false);
|
||||
});
|
||||
it("setPaceCaretCustomSpeed", () => {
|
||||
expect(Config.setPaceCaretCustomSpeed(0)).toBe(true);
|
||||
expect(Config.setPaceCaretCustomSpeed(1)).toBe(true);
|
||||
expect(Config.setPaceCaretCustomSpeed(11.11)).toBe(true);
|
||||
|
||||
expect(Config.setPaceCaretCustomSpeed("invalid" as any)).toBe(false);
|
||||
expect(Config.setPaceCaretCustomSpeed(-1)).toBe(false);
|
||||
});
|
||||
it("setMinWpmCustomSpeed", () => {
|
||||
expect(Config.setMinWpmCustomSpeed(0)).toBe(true);
|
||||
expect(Config.setMinWpmCustomSpeed(1)).toBe(true);
|
||||
expect(Config.setMinWpmCustomSpeed(11.11)).toBe(true);
|
||||
|
||||
expect(Config.setMinWpmCustomSpeed("invalid" as any)).toBe(false);
|
||||
expect(Config.setMinWpmCustomSpeed(-1)).toBe(false);
|
||||
});
|
||||
it("setMinAccCustom", () => {
|
||||
expect(Config.setMinAccCustom(0)).toBe(true);
|
||||
expect(Config.setMinAccCustom(1)).toBe(true);
|
||||
expect(Config.setMinAccCustom(11.11)).toBe(true);
|
||||
//gets converted
|
||||
expect(Config.setMinAccCustom(120)).toBe(true);
|
||||
|
||||
expect(Config.setMinAccCustom("invalid" as any)).toBe(false);
|
||||
expect(Config.setMinAccCustom(-1)).toBe(false);
|
||||
});
|
||||
it("setMinBurstCustomSpeed", () => {
|
||||
expect(Config.setMinBurstCustomSpeed(0)).toBe(true);
|
||||
expect(Config.setMinBurstCustomSpeed(1)).toBe(true);
|
||||
expect(Config.setMinBurstCustomSpeed(11.11)).toBe(true);
|
||||
|
||||
expect(Config.setMinBurstCustomSpeed("invalid" as any)).toBe(false);
|
||||
expect(Config.setMinBurstCustomSpeed(-1)).toBe(false);
|
||||
});
|
||||
it("setTimeConfig", () => {
|
||||
expect(Config.setTimeConfig(0)).toBe(true);
|
||||
expect(Config.setTimeConfig(1)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setTimeConfig("invalid" as any)).toBe(true);
|
||||
expect(Config.setTimeConfig(-1)).toBe(true);
|
||||
|
||||
expect(Config.setTimeConfig(11.11)).toBe(false);
|
||||
});
|
||||
it("setWordCount", () => {
|
||||
expect(Config.setWordCount(0)).toBe(true);
|
||||
expect(Config.setWordCount(1)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setWordCount(-1)).toBe(true);
|
||||
|
||||
expect(Config.setWordCount("invalid" as any)).toBe(false);
|
||||
expect(Config.setWordCount(11.11)).toBe(false);
|
||||
});
|
||||
it("setFontFamily", () => {
|
||||
expect(Config.setFontFamily("Arial")).toBe(true);
|
||||
expect(Config.setFontFamily("roboto_mono")).toBe(true);
|
||||
expect(Config.setFontFamily("test_font")).toBe(true);
|
||||
expect(Config.setFontFamily(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setFontFamily(stringOfLength(51))).toBe(false);
|
||||
expect(Config.setFontFamily("test font")).toBe(false);
|
||||
expect(Config.setFontFamily("test!font")).toBe(false);
|
||||
});
|
||||
it("setTheme", () => {
|
||||
expect(Config.setTheme("serika")).toBe(true);
|
||||
expect(Config.setTheme("serika_dark")).toBe(true);
|
||||
expect(Config.setTheme(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setTheme("serika dark")).toBe(false);
|
||||
expect(Config.setTheme("serika-dark")).toBe(false);
|
||||
expect(Config.setTheme(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setThemeLight", () => {
|
||||
expect(Config.setThemeLight("serika")).toBe(true);
|
||||
expect(Config.setThemeLight("serika_dark")).toBe(true);
|
||||
expect(Config.setThemeLight(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setThemeLight("serika dark")).toBe(false);
|
||||
expect(Config.setThemeLight("serika-dark")).toBe(false);
|
||||
expect(Config.setThemeLight(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setThemeDark", () => {
|
||||
expect(Config.setThemeDark("serika")).toBe(true);
|
||||
expect(Config.setThemeDark("serika_dark")).toBe(true);
|
||||
expect(Config.setThemeDark(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setThemeDark("serika dark")).toBe(false);
|
||||
expect(Config.setThemeDark("serika-dark")).toBe(false);
|
||||
expect(Config.setThemeDark(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setLanguage", () => {
|
||||
expect(Config.setLanguage("english")).toBe(true);
|
||||
expect(Config.setLanguage("english_1k")).toBe(true);
|
||||
expect(Config.setLanguage(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setLanguage("english 1k")).toBe(false);
|
||||
expect(Config.setLanguage("english-1k")).toBe(false);
|
||||
expect(Config.setLanguage(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setKeymapLayout", () => {
|
||||
expect(Config.setKeymapLayout("overrideSync")).toBe(true);
|
||||
expect(Config.setKeymapLayout("override_sync")).toBe(true);
|
||||
expect(Config.setKeymapLayout("override sync")).toBe(true);
|
||||
expect(Config.setKeymapLayout("override-sync!")).toBe(true);
|
||||
expect(Config.setKeymapLayout(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setKeymapLayout(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setLayout", () => {
|
||||
expect(Config.setLayout("semimak")).toBe(true);
|
||||
expect(Config.setLayout("semi_mak")).toBe(true);
|
||||
expect(Config.setLayout(stringOfLength(50))).toBe(true);
|
||||
|
||||
expect(Config.setLayout("semi mak")).toBe(false);
|
||||
expect(Config.setLayout("semi-mak")).toBe(false);
|
||||
expect(Config.setLayout(stringOfLength(51))).toBe(false);
|
||||
});
|
||||
it("setFontSize", () => {
|
||||
expect(Config.setFontSize(1)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setFontSize(-1)).toBe(true);
|
||||
expect(Config.setFontSize("1" as any)).toBe(true);
|
||||
expect(Config.setFontSize("125" as any)).toBe(true);
|
||||
expect(Config.setFontSize("15" as any)).toBe(true);
|
||||
expect(Config.setFontSize("2" as any)).toBe(true);
|
||||
expect(Config.setFontSize("3" as any)).toBe(true);
|
||||
expect(Config.setFontSize("4" as any)).toBe(true);
|
||||
|
||||
expect(Config.setFontSize(0)).toBe(false);
|
||||
expect(Config.setFontSize("5" as any)).toBe(false);
|
||||
expect(Config.setFontSize("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setMaxLineWidth", () => {
|
||||
expect(Config.setMaxLineWidth(0)).toBe(true);
|
||||
expect(Config.setMaxLineWidth(50)).toBe(true);
|
||||
expect(Config.setMaxLineWidth(50.5)).toBe(true);
|
||||
|
||||
//gets converted
|
||||
expect(Config.setMaxLineWidth(10)).toBe(true);
|
||||
expect(Config.setMaxLineWidth(10_000)).toBe(true);
|
||||
expect(Config.setMaxLineWidth("invalid" as any)).toBe(false);
|
||||
});
|
||||
it("setCustomBackground", () => {
|
||||
expect(Config.setCustomBackground("http://example.com/test.png")).toBe(
|
||||
true
|
||||
);
|
||||
expect(Config.setCustomBackground("https://www.example.com/test.gif")).toBe(
|
||||
true
|
||||
);
|
||||
expect(Config.setCustomBackground("https://example.com/test.jpg")).toBe(
|
||||
true
|
||||
);
|
||||
expect(Config.setCustomBackground("http://www.example.com/test.jpeg")).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
//gets converted
|
||||
expect(
|
||||
Config.setCustomBackground(" http://example.com/test.png ")
|
||||
).toBe(true);
|
||||
|
||||
expect(Config.setCustomBackground("http://www.example.com/test.webp")).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
Config.setCustomBackground("http://www.example.com/test?test=foo&bar=baz")
|
||||
).toBe(false);
|
||||
expect(Config.setCustomBackground("invalid")).toBe(false);
|
||||
});
|
||||
it("setQuoteLength", () => {
|
||||
expect(Config.setQuoteLength(0)).toBe(true);
|
||||
expect(Config.setQuoteLength(-3)).toBe(true);
|
||||
expect(Config.setQuoteLength(3)).toBe(true);
|
||||
|
||||
expect(Config.setQuoteLength(-4 as any)).toBe(false);
|
||||
expect(Config.setQuoteLength(4 as any)).toBe(false);
|
||||
|
||||
expect(Config.setQuoteLength([0, -3, 2])).toBe(true);
|
||||
|
||||
expect(Config.setQuoteLength([-4 as any, 5 as any])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function customThemeColors(n: number): CustomThemeColors {
|
||||
return new Array(n).fill("#000") as CustomThemeColors;
|
||||
}
|
||||
|
||||
function testBoolean(fn: (val: boolean) => boolean): void {
|
||||
expect(fn(true)).toBe(true);
|
||||
expect(fn(false)).toBe(true);
|
||||
|
||||
expect(fn("true" as any)).toBe(false);
|
||||
expect(fn("0" as any)).toBe(false);
|
||||
expect(fn("invalid" as any)).toBe(false);
|
||||
}
|
||||
|
||||
function stringOfLength(length: number): string {
|
||||
return randomBytes(Math.ceil(length / 2))
|
||||
.toString("hex")
|
||||
.slice(0, length);
|
||||
}
|
|
@ -20,5 +20,9 @@
|
|||
"files": true
|
||||
},
|
||||
"files": ["../src/ts/types/types.d.ts", "vitest.d.ts"],
|
||||
"include": ["./**/*.spec.ts", "./setup-tests.ts", "../../shared/**/*.d.ts"]
|
||||
"include": [
|
||||
"./**/*.spec.ts",
|
||||
"./setup-tests.ts",
|
||||
"../../shared-types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
5084
frontend/package-lock.json
generated
5084
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -64,7 +64,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@date-fns/utc": "1.2.0",
|
||||
"@ts-rest/core": "3.45.2",
|
||||
"axios": "1.6.4",
|
||||
"canvas-confetti": "1.5.1",
|
||||
"chart.js": "3.7.1",
|
||||
|
@ -84,11 +83,9 @@
|
|||
"konami": "1.6.3",
|
||||
"lz-ts": "1.1.2",
|
||||
"object-hash": "3.0.0",
|
||||
"shared": "file:../shared",
|
||||
"slim-select": "2.8.1",
|
||||
"stemmer": "2.0.0",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"zod": "3.23.8"
|
||||
"throttle-debounce": "3.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"madge": {
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import { AppRouter, initClient, type ApiFetcherArgs } from "@ts-rest/core";
|
||||
import { Method } from "axios";
|
||||
import { getIdToken } from "firebase/auth";
|
||||
import { envConfig } from "../../constants/env-config";
|
||||
import { getAuthenticatedUser, isAuthenticated } from "../../firebase";
|
||||
import type { EndpointMetadata } from "shared/schemas/api";
|
||||
|
||||
function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{
|
||||
status: number;
|
||||
body: unknown;
|
||||
headers: Headers;
|
||||
}> {
|
||||
return async (request: ApiFetcherArgs) => {
|
||||
const isPublicEndpoint =
|
||||
(request.route.metadata as EndpointMetadata | undefined)
|
||||
?.authenticationOptions?.isPublic ?? false;
|
||||
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
...request.headers,
|
||||
"X-Client-Version": envConfig.clientVersion,
|
||||
};
|
||||
if (!isPublicEndpoint) {
|
||||
const token = isAuthenticated()
|
||||
? await getIdToken(getAuthenticatedUser())
|
||||
: "";
|
||||
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetch(request.path, {
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
method: request.method as Method,
|
||||
headers,
|
||||
body: request.body,
|
||||
});
|
||||
const body = await response.json();
|
||||
if (response.status >= 400) {
|
||||
console.error(`${request.method} ${request.path} failed`, {
|
||||
status: response.status,
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
body,
|
||||
headers: response.headers ?? new Headers(),
|
||||
};
|
||||
} catch (e: Error | unknown) {
|
||||
return {
|
||||
status: 500,
|
||||
body: { message: e },
|
||||
headers: new Headers(),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
export function buildClient<T extends AppRouter>(
|
||||
contract: T,
|
||||
baseUrl: string,
|
||||
timeout: number = 10_000
|
||||
) {
|
||||
return initClient(contract, {
|
||||
baseUrl: baseUrl,
|
||||
jsonQuery: true,
|
||||
api: buildApi(timeout),
|
||||
baseHeaders: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/explicit-function-return-type */
|
17
frontend/src/ts/ape/endpoints/configs.ts
Normal file
17
frontend/src/ts/ape/endpoints/configs.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
const BASE_PATH = "/configs";
|
||||
|
||||
export default class Configs {
|
||||
constructor(private httpClient: Ape.HttpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
async get(): Ape.EndpointResponse<Ape.Configs.GetConfig> {
|
||||
return await this.httpClient.get(BASE_PATH);
|
||||
}
|
||||
|
||||
async save(
|
||||
config: SharedTypes.Config
|
||||
): Ape.EndpointResponse<Ape.Configs.PostConfig> {
|
||||
return await this.httpClient.patch(BASE_PATH, { payload: { config } });
|
||||
}
|
||||
}
|
|
@ -1,15 +1,17 @@
|
|||
import ApeKeys from "./ape-keys";
|
||||
import Configuration from "./configuration";
|
||||
import Configs from "./configs";
|
||||
import Leaderboards from "./leaderboards";
|
||||
import Presets from "./presets";
|
||||
import Psas from "./psas";
|
||||
import Public from "./public";
|
||||
import Quotes from "./quotes";
|
||||
import Results from "./results";
|
||||
import Users from "./users";
|
||||
import ApeKeys from "./ape-keys";
|
||||
import Public from "./public";
|
||||
import Configuration from "./configuration";
|
||||
import Dev from "./dev";
|
||||
|
||||
export default {
|
||||
Configs,
|
||||
Leaderboards,
|
||||
Presets,
|
||||
Psas,
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import endpoints from "./endpoints";
|
||||
import { buildHttpClient } from "./adapters/axios-adapter";
|
||||
import { envConfig } from "../constants/env-config";
|
||||
import { buildClient } from "./adapters/ts-rest-adapter";
|
||||
import { configsContract } from "shared/contracts/configs";
|
||||
|
||||
const API_PATH = "";
|
||||
const BASE_URL = envConfig.backendUrl;
|
||||
const API_URL = `${BASE_URL}${API_PATH}`;
|
||||
|
||||
const httpClient = buildHttpClient(API_URL, 10_000);
|
||||
const httpClient = buildHttpClient(API_URL, 10000);
|
||||
|
||||
// API Endpoints
|
||||
const Ape = {
|
||||
users: new endpoints.Users(httpClient),
|
||||
configs: buildClient(configsContract, BASE_URL, 10_000),
|
||||
configs: new endpoints.Configs(httpClient),
|
||||
results: new endpoints.Results(httpClient),
|
||||
psas: new endpoints.Psas(httpClient),
|
||||
quotes: new endpoints.Quotes(httpClient),
|
||||
|
|
10
frontend/src/ts/ape/types/configs.d.ts
vendored
Normal file
10
frontend/src/ts/ape/types/configs.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// for some reason when using the dot notaion, the types are not being recognized as used
|
||||
declare namespace Ape.Configs {
|
||||
type GetConfig = {
|
||||
_id: string;
|
||||
uid: string;
|
||||
config: Partial<SharedTypes.Config>;
|
||||
};
|
||||
type PostConfig = null;
|
||||
}
|
|
@ -1,7 +1,18 @@
|
|||
import * as Misc from "./utils/misc";
|
||||
import * as JSONData from "./utils/json-data";
|
||||
import * as Notifications from "./elements/notifications";
|
||||
import { ZodSchema, z } from "zod";
|
||||
|
||||
type PossibleType =
|
||||
| "string"
|
||||
| "number"
|
||||
| "numberArray"
|
||||
| "boolean"
|
||||
| "undefined"
|
||||
| "null"
|
||||
| "stringArray"
|
||||
| "layoutfluid"
|
||||
| string[]
|
||||
| number[];
|
||||
|
||||
type PossibleTypeAsync = "layoutfluid";
|
||||
|
||||
|
@ -27,19 +38,70 @@ function invalid(key: string, val: unknown, customMessage?: string): void {
|
|||
console.error(`Invalid value key ${key} value ${val} type ${typeof val}`);
|
||||
}
|
||||
|
||||
export function isConfigValueValid<T>(
|
||||
function isArray(val: unknown): val is unknown[] {
|
||||
return val instanceof Array;
|
||||
}
|
||||
|
||||
export function isConfigValueValid(
|
||||
key: string,
|
||||
val: T,
|
||||
schema: ZodSchema<T>
|
||||
val: unknown,
|
||||
possibleTypes: PossibleType[]
|
||||
): boolean {
|
||||
const isValid = schema.safeParse(val).success;
|
||||
if (!isValid) invalid(key, val, undefined);
|
||||
let isValid = false;
|
||||
|
||||
// might be used in the future
|
||||
// eslint-disable-next-line
|
||||
let customMessage: string | undefined = undefined;
|
||||
|
||||
for (const possibleType of possibleTypes) {
|
||||
switch (possibleType) {
|
||||
case "boolean":
|
||||
if (typeof val === "boolean") isValid = true;
|
||||
break;
|
||||
|
||||
case "null":
|
||||
if (val === null) isValid = true;
|
||||
break;
|
||||
|
||||
case "number":
|
||||
if (typeof val === "number" && !isNaN(val)) isValid = true;
|
||||
break;
|
||||
|
||||
case "numberArray":
|
||||
if (
|
||||
isArray(val) &&
|
||||
val.every((v) => typeof v === "number" && !isNaN(v))
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "string":
|
||||
if (typeof val === "string") isValid = true;
|
||||
break;
|
||||
|
||||
case "stringArray":
|
||||
if (isArray(val) && val.every((v) => typeof v === "string")) {
|
||||
isValid = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "undefined":
|
||||
if (typeof val === "undefined" || val === undefined) isValid = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (isArray(possibleType)) {
|
||||
if (possibleType.includes(val as never)) isValid = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) invalid(key, val, customMessage);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
export function isConfigValueValidBoolean(key: string, val: boolean): boolean {
|
||||
return isConfigValueValid(key, val, z.boolean());
|
||||
}
|
||||
|
||||
export async function isConfigValueValidAsync(
|
||||
key: string,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,3 @@
|
|||
import { Config } from "shared/schemas/config";
|
||||
|
||||
export default {
|
||||
theme: "serika_dark",
|
||||
themeLight: "serika",
|
||||
|
@ -85,7 +83,7 @@ export default {
|
|||
oppositeShiftMode: "off",
|
||||
customBackground: "",
|
||||
customBackgroundSize: "cover",
|
||||
customBackgroundFilter: [0, 1, 1, 1],
|
||||
customBackgroundFilter: [0, 1, 1, 1, 1],
|
||||
customLayoutfluid: "qwerty#dvorak#colemak",
|
||||
monkeyPowerLevel: "off",
|
||||
minBurst: "off",
|
||||
|
@ -96,4 +94,4 @@ export default {
|
|||
showAverage: "off",
|
||||
tapeMode: "off",
|
||||
maxLineWidth: 0,
|
||||
} as Config;
|
||||
} as SharedTypes.Config;
|
||||
|
|
|
@ -77,7 +77,7 @@ export async function initSnapshot(): Promise<
|
|||
if (configResponse.status !== 200) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw {
|
||||
message: `${configResponse.body.message} (config)`,
|
||||
message: `${configResponse.message} (config)`,
|
||||
responseCode: configResponse.status,
|
||||
};
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export async function initSnapshot(): Promise<
|
|||
}
|
||||
|
||||
const userData = userResponse.data;
|
||||
const configData = configResponse.body.data;
|
||||
const configData = configResponse.data;
|
||||
const presetsData = presetsResponse.data;
|
||||
|
||||
if (userData === null) {
|
||||
|
@ -165,7 +165,7 @@ export async function initSnapshot(): Promise<
|
|||
...DefaultConfig,
|
||||
};
|
||||
} else {
|
||||
snap.config = mergeWithDefaultConfig(configData);
|
||||
snap.config = mergeWithDefaultConfig(configData.config);
|
||||
}
|
||||
// if (ActivePage.get() === "loading") {
|
||||
// LoadingPage.updateBar(67.5);
|
||||
|
@ -892,18 +892,9 @@ export async function updateLbMemory<M extends SharedTypes.Config.Mode>(
|
|||
|
||||
export async function saveConfig(config: SharedTypes.Config): Promise<void> {
|
||||
if (isAuthenticated()) {
|
||||
const response = await Ape.configs.save({ body: config });
|
||||
const response = await Ape.configs.save(config);
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Failed to save config: " + response.body.message, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetConfig(): Promise<void> {
|
||||
if (isAuthenticated()) {
|
||||
const response = await Ape.configs.delete();
|
||||
if (response.status !== 200) {
|
||||
Notifications.add("Failed to reset config: " + response.body.message, -1);
|
||||
Notifications.add("Failed to save config: " + response.message, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ function buildPbHtml(
|
|||
if (pbData === undefined) throw new Error("No PB data found");
|
||||
|
||||
const date = new Date(pbData.timestamp);
|
||||
if (pbData.timestamp !== undefined && pbData.timestamp > 0) {
|
||||
if (pbData.timestamp) {
|
||||
dateText = dateFormat(date, "dd MMM yyyy");
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import * as DB from "../../db";
|
|||
import * as ConfigEvent from "../../observables/config-event";
|
||||
import { isAuthenticated } from "../../firebase";
|
||||
import * as ActivePage from "../../states/active-page";
|
||||
import { CustomThemeColors } from "shared/schemas/config";
|
||||
|
||||
function updateActiveButton(): void {
|
||||
let activeThemeName = Config.theme;
|
||||
|
@ -137,7 +136,6 @@ export async function refreshButtons(): Promise<void> {
|
|||
|
||||
customThemes.forEach((customTheme) => {
|
||||
// const activeTheme =
|
||||
// const activeTheme =git st
|
||||
// Config.customThemeId === customTheme._id ? "active" : "";
|
||||
const bgColor = customTheme.colors[0];
|
||||
const mainColor = customTheme.colors[1];
|
||||
|
@ -283,7 +281,7 @@ function saveCustomThemeColors(): void {
|
|||
).attr("value") as string
|
||||
);
|
||||
}
|
||||
UpdateConfig.setCustomThemeColors(newColors as CustomThemeColors);
|
||||
UpdateConfig.setCustomThemeColors(newColors);
|
||||
Notifications.add("Custom theme saved", 1);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ import AnimatedModal, {
|
|||
} from "../utils/animated-modal";
|
||||
import { format as dateFormat } from "date-fns/format";
|
||||
import { Attributes, buildTag } from "../utils/tag-builder";
|
||||
import { CustomThemeColors } from "shared/schemas/config";
|
||||
|
||||
type CommonInput<TType, TValue> = {
|
||||
type: TType;
|
||||
|
@ -1706,7 +1705,7 @@ list.updateCustomTheme = new SimpleModal({
|
|||
|
||||
const newTheme = {
|
||||
name: name.replaceAll(" ", "_"),
|
||||
colors: newColors as CustomThemeColors,
|
||||
colors: newColors,
|
||||
};
|
||||
const validation = await DB.editCustomTheme(customTheme._id, newTheme);
|
||||
if (!validation) {
|
||||
|
@ -1715,7 +1714,7 @@ list.updateCustomTheme = new SimpleModal({
|
|||
message: "Failed to update custom theme",
|
||||
};
|
||||
}
|
||||
UpdateConfig.setCustomThemeColors(newColors as CustomThemeColors);
|
||||
UpdateConfig.setCustomThemeColors(newColors);
|
||||
void ThemePicker.refreshButtons();
|
||||
|
||||
return {
|
||||
|
|
|
@ -544,7 +544,7 @@ async function fillContent(): Promise<void> {
|
|||
histogramChartData.push(0);
|
||||
}
|
||||
}
|
||||
(histogramChartData[bucket] as number)++;
|
||||
histogramChartData[bucket]++;
|
||||
|
||||
let tt = 0;
|
||||
if (
|
||||
|
|
2
frontend/src/ts/types/types.d.ts
vendored
2
frontend/src/ts/types/types.d.ts
vendored
|
@ -179,7 +179,7 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type RawCustomTheme = {
|
||||
name: string;
|
||||
colors: SharedTypes.Config.CustomThemeColors;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type CustomTheme = {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Config, PartialConfig } from "shared/schemas/config";
|
||||
import DefaultConfig from "../constants/default-config";
|
||||
import { typedKeys } from "./misc";
|
||||
|
||||
export function mergeWithDefaultConfig(config: PartialConfig): Config {
|
||||
export function mergeWithDefaultConfig(
|
||||
config: Partial<SharedTypes.Config>
|
||||
): SharedTypes.Config {
|
||||
const mergedConfig = {} as SharedTypes.Config;
|
||||
for (const key of typedKeys(DefaultConfig)) {
|
||||
const newValue =
|
||||
|
|
|
@ -31,8 +31,11 @@
|
|||
"noPropertyAccessFromIndexSignature": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
"skipLibCheck": false
|
||||
"skipLibCheck": false,
|
||||
"paths": {
|
||||
"@shared/*": ["../shared-types/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*.ts", "../shared/**/*.d.ts"],
|
||||
"include": ["./src/**/*.ts", "../shared-types/**/*.d.ts"],
|
||||
"exclude": ["node_modules", "build", "setup-tests.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
|
|
@ -9,19 +9,19 @@
|
|||
"path": "frontend"
|
||||
},
|
||||
{
|
||||
"name": "shared",
|
||||
"path": "shared"
|
||||
"name": "shared-types",
|
||||
"path": "shared-types"
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"path": "."
|
||||
"path": "./"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"frontend": true,
|
||||
"backend": true,
|
||||
"shared": true
|
||||
"shared-types": true
|
||||
},
|
||||
"search.exclude": {
|
||||
//defaults
|
||||
|
|
80
shared-types/config.d.ts
vendored
Normal file
80
shared-types/config.d.ts
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// for some reason when using the dot notaion, the types are not being recognized as used
|
||||
declare namespace SharedTypes.Config {
|
||||
type SmoothCaret = "off" | "slow" | "medium" | "fast";
|
||||
type QuickRestart = "off" | "esc" | "tab" | "enter";
|
||||
type QuoteLength = -3 | -2 | -1 | 0 | 1 | 2 | 3;
|
||||
type CaretStyle =
|
||||
| "off"
|
||||
| "default"
|
||||
| "block"
|
||||
| "outline"
|
||||
| "underline"
|
||||
| "carrot"
|
||||
| "banana";
|
||||
type Difficulty = "normal" | "expert" | "master";
|
||||
type Mode = keyof PersonalBests;
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never;
|
||||
type Mode2Custom<M extends Mode> = Mode2<M> | "custom";
|
||||
type ConfidenceMode = "off" | "on" | "max";
|
||||
type IndicateTypos = "off" | "below" | "replace";
|
||||
type TimerStyle = "off" | "bar" | "text" | "mini";
|
||||
type LiveSpeedAccBurstStyle = "off" | "text" | "mini";
|
||||
type RandomTheme = "off" | "on" | "fav" | "light" | "dark" | "custom";
|
||||
type TimerColor = "black" | "sub" | "text" | "main";
|
||||
type TimerOpacity = "0.25" | "0.5" | "0.75" | "1";
|
||||
type StopOnError = "off" | "word" | "letter";
|
||||
type KeymapMode = "off" | "static" | "react" | "next";
|
||||
type KeymapStyle =
|
||||
| "staggered"
|
||||
| "alice"
|
||||
| "matrix"
|
||||
| "split"
|
||||
| "split_matrix"
|
||||
| "steno"
|
||||
| "steno_matrix";
|
||||
type KeymapLegendStyle = "lowercase" | "uppercase" | "blank" | "dynamic";
|
||||
type KeymapShowTopRow = "always" | "layout" | "never";
|
||||
type SingleListCommandLine = "manual" | "on";
|
||||
type PlaySoundOnClick =
|
||||
| "off"
|
||||
| "1"
|
||||
| "2"
|
||||
| "3"
|
||||
| "4"
|
||||
| "5"
|
||||
| "6"
|
||||
| "7"
|
||||
| "8"
|
||||
| "9"
|
||||
| "10"
|
||||
| "11"
|
||||
| "12"
|
||||
| "13"
|
||||
| "14"
|
||||
| "15";
|
||||
type PlaySoundOnError = "off" | "1" | "2" | "3" | "4";
|
||||
type SoundVolume = "0.1" | "0.5" | "1.0";
|
||||
type PaceCaret = "off" | "average" | "pb" | "last" | "custom" | "daily";
|
||||
type AccountChart = ["off" | "on", "off" | "on", "off" | "on", "off" | "on"];
|
||||
type MinimumWordsPerMinute = "off" | "custom";
|
||||
type HighlightMode =
|
||||
| "off"
|
||||
| "letter"
|
||||
| "word"
|
||||
| "next_word"
|
||||
| "next_two_words"
|
||||
| "next_three_words";
|
||||
type TypingSpeedUnit = "wpm" | "cpm" | "wps" | "cps" | "wph";
|
||||
type Ads = "off" | "result" | "on" | "sellout";
|
||||
type MinimumAccuracy = "off" | "custom";
|
||||
type RepeatQuotes = "off" | "typing";
|
||||
type OppositeShiftMode = "off" | "on" | "keymap";
|
||||
type CustomBackgroundSize = "cover" | "contain" | "max";
|
||||
type CustomBackgroundFilter = [number, number, number, number, number];
|
||||
type CustomLayoutFluid = `${string}#${string}#${string}`;
|
||||
type MonkeyPowerLevel = "off" | "1" | "2" | "3" | "4";
|
||||
type MinimumBurst = "off" | "fixed" | "flex";
|
||||
type ShowAverage = "off" | "speed" | "acc" | "both";
|
||||
type TapeMode = "off" | "letter" | "word";
|
||||
}
|
112
shared/types/types.d.ts → shared-types/types.d.ts
vendored
112
shared/types/types.d.ts → shared-types/types.d.ts
vendored
|
@ -111,11 +111,28 @@ declare namespace SharedTypes {
|
|||
};
|
||||
}
|
||||
|
||||
type StringNumber = import("../schemas/util").StringNumber;
|
||||
type StringNumber = `${number}`;
|
||||
|
||||
type PersonalBest = import("../schemas/users").PersonalBest;
|
||||
interface PersonalBest {
|
||||
acc: number;
|
||||
consistency?: number;
|
||||
difficulty: SharedTypes.Config.Difficulty;
|
||||
lazyMode?: boolean;
|
||||
language: string;
|
||||
punctuation?: boolean;
|
||||
numbers?: boolean;
|
||||
raw: number;
|
||||
wpm: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type PersonalBests = import("../schemas/users").PersonalBests;
|
||||
interface PersonalBests {
|
||||
time: Record<StringNumber, PersonalBest[]>;
|
||||
words: Record<StringNumber, PersonalBest[]>;
|
||||
quote: Record<StringNumber, PersonalBest[]>;
|
||||
custom: Partial<Record<"custom", PersonalBest[]>>;
|
||||
zen: Partial<Record<"zen", PersonalBest[]>>;
|
||||
}
|
||||
|
||||
interface IncompleteTest {
|
||||
acc: number;
|
||||
|
@ -329,7 +346,92 @@ declare namespace SharedTypes {
|
|||
lastUsedOn: number;
|
||||
}
|
||||
|
||||
type Config = import("../schemas/config").Config;
|
||||
interface Config {
|
||||
theme: string;
|
||||
themeLight: string;
|
||||
themeDark: string;
|
||||
autoSwitchTheme: boolean;
|
||||
customTheme: boolean;
|
||||
customThemeColors: string[];
|
||||
favThemes: string[];
|
||||
showKeyTips: boolean;
|
||||
smoothCaret: SharedTypes.Config.SmoothCaret;
|
||||
quickRestart: SharedTypes.Config.QuickRestart;
|
||||
punctuation: boolean;
|
||||
numbers: boolean;
|
||||
words: number;
|
||||
time: number;
|
||||
mode: SharedTypes.Config.Mode;
|
||||
quoteLength: SharedTypes.Config.QuoteLength[];
|
||||
language: string;
|
||||
fontSize: number;
|
||||
freedomMode: boolean;
|
||||
difficulty: SharedTypes.Config.Difficulty;
|
||||
blindMode: boolean;
|
||||
quickEnd: boolean;
|
||||
caretStyle: SharedTypes.Config.CaretStyle;
|
||||
paceCaretStyle: SharedTypes.Config.CaretStyle;
|
||||
flipTestColors: boolean;
|
||||
layout: string;
|
||||
funbox: string;
|
||||
confidenceMode: SharedTypes.Config.ConfidenceMode;
|
||||
indicateTypos: SharedTypes.Config.IndicateTypos;
|
||||
timerStyle: SharedTypes.Config.TimerStyle;
|
||||
liveSpeedStyle: SharedTypes.Config.LiveSpeedAccBurstStyle;
|
||||
liveAccStyle: SharedTypes.Config.LiveSpeedAccBurstStyle;
|
||||
liveBurstStyle: SharedTypes.Config.LiveSpeedAccBurstStyle;
|
||||
colorfulMode: boolean;
|
||||
randomTheme: SharedTypes.Config.RandomTheme;
|
||||
timerColor: SharedTypes.Config.TimerColor;
|
||||
timerOpacity: SharedTypes.Config.TimerOpacity;
|
||||
stopOnError: SharedTypes.Config.StopOnError;
|
||||
showAllLines: boolean;
|
||||
keymapMode: SharedTypes.Config.KeymapMode;
|
||||
keymapStyle: SharedTypes.Config.KeymapStyle;
|
||||
keymapLegendStyle: SharedTypes.Config.KeymapLegendStyle;
|
||||
keymapLayout: string;
|
||||
keymapShowTopRow: SharedTypes.Config.KeymapShowTopRow;
|
||||
fontFamily: string;
|
||||
smoothLineScroll: boolean;
|
||||
alwaysShowDecimalPlaces: boolean;
|
||||
alwaysShowWordsHistory: boolean;
|
||||
singleListCommandLine: SharedTypes.Config.SingleListCommandLine;
|
||||
capsLockWarning: boolean;
|
||||
playSoundOnError: SharedTypes.Config.PlaySoundOnError;
|
||||
playSoundOnClick: SharedTypes.Config.PlaySoundOnClick;
|
||||
soundVolume: SharedTypes.Config.SoundVolume;
|
||||
startGraphsAtZero: boolean;
|
||||
showOutOfFocusWarning: boolean;
|
||||
paceCaret: SharedTypes.Config.PaceCaret;
|
||||
paceCaretCustomSpeed: number;
|
||||
repeatedPace: boolean;
|
||||
accountChart: SharedTypes.Config.AccountChart;
|
||||
minWpm: SharedTypes.Config.MinimumWordsPerMinute;
|
||||
minWpmCustomSpeed: number;
|
||||
highlightMode: SharedTypes.Config.HighlightMode;
|
||||
typingSpeedUnit: SharedTypes.Config.TypingSpeedUnit;
|
||||
ads: SharedTypes.Config.Ads;
|
||||
hideExtraLetters: boolean;
|
||||
strictSpace: boolean;
|
||||
minAcc: SharedTypes.Config.MinimumAccuracy;
|
||||
minAccCustom: number;
|
||||
monkey: boolean;
|
||||
repeatQuotes: SharedTypes.Config.RepeatQuotes;
|
||||
oppositeShiftMode: SharedTypes.Config.OppositeShiftMode;
|
||||
customBackground: string;
|
||||
customBackgroundSize: SharedTypes.Config.CustomBackgroundSize;
|
||||
customBackgroundFilter: SharedTypes.Config.CustomBackgroundFilter;
|
||||
customLayoutfluid: SharedTypes.Config.CustomLayoutFluid;
|
||||
monkeyPowerLevel: SharedTypes.Config.MonkeyPowerLevel;
|
||||
minBurst: SharedTypes.Config.MinimumBurst;
|
||||
minBurstCustomSpeed: number;
|
||||
burstHeatmap: boolean;
|
||||
britishEnglish: boolean;
|
||||
lazyMode: boolean;
|
||||
showAverage: SharedTypes.Config.ShowAverage;
|
||||
tapeMode: SharedTypes.Config.TapeMode;
|
||||
maxLineWidth: number;
|
||||
}
|
||||
|
||||
type ConfigValue = Config[keyof Config];
|
||||
|
||||
|
@ -398,7 +500,7 @@ declare namespace SharedTypes {
|
|||
type CustomTheme = {
|
||||
_id: string;
|
||||
name: string;
|
||||
colors: Config.CustomThemeColors;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type PremiumInfo = {
|
|
@ -1,67 +0,0 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
EndpointMetadata,
|
||||
MonkeyClientError,
|
||||
MonkeyResponseSchema,
|
||||
MonkeyServerError,
|
||||
MonkeyValidationErrorSchema,
|
||||
responseWithNullableData,
|
||||
} from "../schemas/api";
|
||||
import { PartialConfigSchema } from "../schemas/config";
|
||||
|
||||
export const GetConfigResponseSchema =
|
||||
responseWithNullableData(PartialConfigSchema);
|
||||
|
||||
export type GetConfigResponse = z.infer<typeof GetConfigResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const configsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get config",
|
||||
description: "Get config of the current user.",
|
||||
method: "GET",
|
||||
path: "/",
|
||||
responses: {
|
||||
200: GetConfigResponseSchema,
|
||||
},
|
||||
},
|
||||
save: {
|
||||
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.",
|
||||
},
|
||||
delete: {
|
||||
method: "DELETE",
|
||||
path: "/",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
summary: "delete config",
|
||||
description: "Delete/reset the config for the current user.",
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configs",
|
||||
strictStatusCodes: true,
|
||||
metadata: {
|
||||
openApiTags: "configs",
|
||||
} as EndpointMetadata,
|
||||
|
||||
commonResponses: {
|
||||
400: MonkeyClientError,
|
||||
422: MonkeyValidationErrorSchema,
|
||||
500: MonkeyServerError,
|
||||
},
|
||||
}
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
import { initContract } from "@ts-rest/core";
|
||||
import { configsContract } from "./configs";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const contract = c.router({
|
||||
configs: configsContract,
|
||||
});
|
35
shared/package-lock.json
generated
35
shared/package-lock.json
generated
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"name": "shared",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "shared",
|
||||
"dependencies": {
|
||||
"@ts-rest/core": "3.45.2",
|
||||
"zod": "3.23.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-rest/core": {
|
||||
"version": "3.45.2",
|
||||
"resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.45.2.tgz",
|
||||
"integrity": "sha512-Eiv+Sa23MbsAd1Gx9vNJ+IFCDyLZNdJ+UuGMKbFvb+/NmgcBR1VL1UIVtEkd5DJxpYMMd8SLvW91RgB2TS8iPw==",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "shared",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@ts-rest/core": "3.45.2",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import { z, ZodSchema } from "zod";
|
||||
|
||||
export type OperationTag = "configs";
|
||||
|
||||
export type EndpointMetadata = {
|
||||
/** Authentication options, by default a bearer token is required. */
|
||||
authenticationOptions?: RequestAuthenticationOptions;
|
||||
openApiTags?: OperationTag | OperationTag[];
|
||||
};
|
||||
|
||||
export type RequestAuthenticationOptions = {
|
||||
/** Endpoint is accessible without any authentication. If `false` bearer authentication is required. */
|
||||
isPublic?: boolean;
|
||||
/** Endpoint is accessible with ape key authentication in _addition_ to the bearer authentication. */
|
||||
acceptApeKeys?: boolean;
|
||||
/** Endpoint requires an authentication token which is younger than one minute. */
|
||||
requireFreshToken?: boolean;
|
||||
noCache?: boolean;
|
||||
};
|
||||
|
||||
export const MonkeyResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
export type MonkeyResponseType = z.infer<typeof MonkeyResponseSchema>;
|
||||
|
||||
export const MonkeyValidationErrorSchema = MonkeyResponseSchema.extend({
|
||||
validationErrors: z.array(z.string()).nonempty(),
|
||||
});
|
||||
export type MonkeyValidationError = z.infer<typeof MonkeyValidationErrorSchema>;
|
||||
|
||||
export const MonkeyClientError = MonkeyResponseSchema;
|
||||
export const MonkeyServerError = MonkeyClientError.extend({
|
||||
errorId: z.string(),
|
||||
uid: z.string().optional(),
|
||||
});
|
||||
export type MonkeyServerErrorType = z.infer<typeof MonkeyServerError>;
|
||||
|
||||
export function responseWithNullableData<T extends ZodSchema>(
|
||||
dataSchema: T
|
||||
): z.ZodObject<
|
||||
z.objectUtil.extendShape<
|
||||
typeof MonkeyResponseSchema.shape,
|
||||
{
|
||||
data: z.ZodNullable<T>;
|
||||
}
|
||||
>
|
||||
> {
|
||||
return MonkeyResponseSchema.extend({
|
||||
data: dataSchema.nullable(),
|
||||
});
|
||||
}
|
||||
|
||||
export function responseWithData<T extends ZodSchema>(
|
||||
dataSchema: T
|
||||
): z.ZodObject<
|
||||
z.objectUtil.extendShape<
|
||||
typeof MonkeyResponseSchema.shape,
|
||||
{
|
||||
data: T;
|
||||
}
|
||||
>
|
||||
> {
|
||||
return MonkeyResponseSchema.extend({
|
||||
data: dataSchema,
|
||||
});
|
||||
}
|
|
@ -1,390 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import { token } from "./util";
|
||||
|
||||
export const SmoothCaretSchema = z.enum(["off", "slow", "medium", "fast"]);
|
||||
export type SmoothCaret = z.infer<typeof SmoothCaretSchema>;
|
||||
|
||||
export const QuickRestartSchema = z.enum(["off", "esc", "tab", "enter"]);
|
||||
export type QuickRestart = z.infer<typeof QuickRestartSchema>;
|
||||
|
||||
export const QuoteLengthSchema = z.union([
|
||||
z.literal(-3),
|
||||
z.literal(-2),
|
||||
z.literal(-1),
|
||||
z.literal(0),
|
||||
z.literal(1),
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
]);
|
||||
export type QuoteLength = z.infer<typeof QuoteLengthSchema>;
|
||||
|
||||
export const QuoteLengthConfigSchema = z.array(QuoteLengthSchema);
|
||||
export type QuoteLengthConfig = z.infer<typeof QuoteLengthConfigSchema>;
|
||||
|
||||
export const CaretStyleSchema = z.enum([
|
||||
"off",
|
||||
"default",
|
||||
"block",
|
||||
"outline",
|
||||
"underline",
|
||||
"carrot",
|
||||
"banana",
|
||||
]);
|
||||
export type CaretStyle = z.infer<typeof CaretStyleSchema>;
|
||||
|
||||
export const ConfidenceModeSchema = z.enum(["off", "on", "max"]);
|
||||
export type ConfidenceMode = z.infer<typeof ConfidenceModeSchema>;
|
||||
|
||||
export const IndicateTyposSchema = z.enum(["off", "below", "replace"]);
|
||||
export type IndicateTypos = z.infer<typeof IndicateTyposSchema>;
|
||||
|
||||
export const TimerStyleSchema = z.enum(["off", "bar", "text", "mini"]);
|
||||
export type TimerStyle = z.infer<typeof TimerStyleSchema>;
|
||||
|
||||
export const LiveSpeedAccBurstStyleSchema = z.enum(["off", "text", "mini"]);
|
||||
export type LiveSpeedAccBurstStyle = z.infer<
|
||||
typeof LiveSpeedAccBurstStyleSchema
|
||||
>;
|
||||
|
||||
export const RandomThemeSchema = z.enum([
|
||||
"off",
|
||||
"on",
|
||||
"fav",
|
||||
"light",
|
||||
"dark",
|
||||
"custom",
|
||||
]);
|
||||
export type RandomTheme = z.infer<typeof RandomThemeSchema>;
|
||||
|
||||
export const TimerColorSchema = z.enum(["black", "sub", "text", "main"]);
|
||||
export type TimerColor = z.infer<typeof TimerColorSchema>;
|
||||
|
||||
export const TimerOpacitySchema = z.enum(["0.25", "0.5", "0.75", "1"]);
|
||||
export type TimerOpacity = z.infer<typeof TimerOpacitySchema>;
|
||||
|
||||
export const StopOnErrorSchema = z.enum(["off", "word", "letter"]);
|
||||
export type StopOnError = z.infer<typeof StopOnErrorSchema>;
|
||||
|
||||
export const KeymapModeSchema = z.enum(["off", "static", "react", "next"]);
|
||||
export type KeymapMode = z.infer<typeof KeymapModeSchema>;
|
||||
|
||||
export const KeymapStyleSchema = z.enum([
|
||||
"staggered",
|
||||
"alice",
|
||||
"matrix",
|
||||
"split",
|
||||
"split_matrix",
|
||||
"steno",
|
||||
"steno_matrix",
|
||||
]);
|
||||
export type KeymapStyle = z.infer<typeof KeymapStyleSchema>;
|
||||
|
||||
export const KeymapLegendStyleSchema = z.enum([
|
||||
"lowercase",
|
||||
"uppercase",
|
||||
"blank",
|
||||
"dynamic",
|
||||
]);
|
||||
export type KeymapLegendStyle = z.infer<typeof KeymapLegendStyleSchema>;
|
||||
|
||||
export const KeymapShowTopRowSchema = z.enum(["always", "layout", "never"]);
|
||||
export type KeymapShowTopRow = z.infer<typeof KeymapShowTopRowSchema>;
|
||||
|
||||
export const SingleListCommandLineSchema = z.enum(["manual", "on"]);
|
||||
export type SingleListCommandLine = z.infer<typeof SingleListCommandLineSchema>;
|
||||
|
||||
export const PlaySoundOnErrorSchema = z.enum(["off", "1", "2", "3", "4"]);
|
||||
export type PlaySoundOnError = z.infer<typeof PlaySoundOnErrorSchema>;
|
||||
|
||||
export const PlaySoundOnClickSchema = z.enum([
|
||||
"off",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
]);
|
||||
export type PlaySoundOnClick = z.infer<typeof PlaySoundOnClickSchema>;
|
||||
|
||||
export const SoundVolumeSchema = z.enum(["0.1", "0.5", "1.0"]);
|
||||
export type SoundVolume = z.infer<typeof SoundVolumeSchema>;
|
||||
|
||||
export const PaceCaretSchema = z.enum([
|
||||
"off",
|
||||
"average",
|
||||
"pb",
|
||||
"last",
|
||||
"custom",
|
||||
"daily",
|
||||
]);
|
||||
export type PaceCaret = z.infer<typeof PaceCaretSchema>;
|
||||
|
||||
export const AccountChartSchema = z.tuple([
|
||||
z.enum(["on", "off"]),
|
||||
z.enum(["on", "off"]),
|
||||
z.enum(["on", "off"]),
|
||||
z.enum(["on", "off"]),
|
||||
]);
|
||||
export type AccountChart = z.infer<typeof AccountChartSchema>;
|
||||
|
||||
export const MinimumWordsPerMinuteSchema = z.enum(["off", "custom"]);
|
||||
export type MinimumWordsPerMinute = z.infer<typeof MinimumWordsPerMinuteSchema>;
|
||||
|
||||
export const HighlightModeSchema = z.enum([
|
||||
"off",
|
||||
"letter",
|
||||
"word",
|
||||
"next_word",
|
||||
"next_two_words",
|
||||
"next_three_words",
|
||||
]);
|
||||
export type HighlightMode = z.infer<typeof HighlightModeSchema>;
|
||||
|
||||
export const TapeModeSchema = z.enum(["off", "letter", "word"]);
|
||||
export type TapeMode = z.infer<typeof TapeModeSchema>;
|
||||
|
||||
export const TypingSpeedUnitSchema = z.enum([
|
||||
"wpm",
|
||||
"cpm",
|
||||
"wps",
|
||||
"cps",
|
||||
"wph",
|
||||
]);
|
||||
export type TypingSpeedUnit = z.infer<typeof TypingSpeedUnitSchema>;
|
||||
|
||||
export const AdsSchema = z.enum(["off", "result", "on", "sellout"]);
|
||||
export type Ads = z.infer<typeof AdsSchema>;
|
||||
|
||||
export const MinimumAccuracySchema = z.enum(["off", "custom"]);
|
||||
export type MinimumAccuracy = z.infer<typeof MinimumAccuracySchema>;
|
||||
|
||||
export const RepeatQuotesSchema = z.enum(["off", "typing"]);
|
||||
export type RepeatQuotes = z.infer<typeof RepeatQuotesSchema>;
|
||||
|
||||
export const OppositeShiftModeSchema = z.enum(["off", "on", "keymap"]);
|
||||
export type OppositeShiftMode = z.infer<typeof OppositeShiftModeSchema>;
|
||||
|
||||
export const CustomBackgroundSizeSchema = z.enum(["cover", "contain", "max"]);
|
||||
export type CustomBackgroundSize = z.infer<typeof CustomBackgroundSizeSchema>;
|
||||
|
||||
export const CustomBackgroundFilterSchema = z.tuple([
|
||||
z.number(),
|
||||
z.number(),
|
||||
z.number(),
|
||||
z.number(),
|
||||
]);
|
||||
export type CustomBackgroundFilter = z.infer<
|
||||
typeof CustomBackgroundFilterSchema
|
||||
>;
|
||||
|
||||
export const CustomLayoutFluidSchema = z.string().regex(/^[0-9a-zA-Z_#]+$/); //TODO better regex
|
||||
export type CustomLayoutFluid = z.infer<typeof CustomLayoutFluidSchema>;
|
||||
|
||||
export const MonkeyPowerLevelSchema = z.enum(["off", "1", "2", "3", "4"]);
|
||||
export type MonkeyPowerLevel = z.infer<typeof MonkeyPowerLevelSchema>;
|
||||
|
||||
export const MinimumBurstSchema = z.enum(["off", "fixed", "flex"]);
|
||||
export type MinimumBurst = z.infer<typeof MinimumBurstSchema>;
|
||||
|
||||
export const ShowAverageSchema = z.enum(["off", "speed", "acc", "both"]);
|
||||
export type ShowAverage = z.infer<typeof ShowAverageSchema>;
|
||||
|
||||
export const ColorHexValueSchema = z.string().regex(/^#([\da-f]{3}){1,2}$/i);
|
||||
export type ColorHexValue = z.infer<typeof ColorHexValueSchema>;
|
||||
|
||||
export const DifficultySchema = z.enum(["normal", "expert", "master"]);
|
||||
export type Difficulty = z.infer<typeof DifficultySchema>;
|
||||
|
||||
export const NumberModeSchema = z.enum(["time", "words", "quote"]);
|
||||
export const CustomModeSchema = z.enum(["custom"]);
|
||||
export const ZenModeSchema = z.enum(["zen"]);
|
||||
|
||||
export const ModeSchema = z.union([
|
||||
NumberModeSchema,
|
||||
CustomModeSchema,
|
||||
ZenModeSchema,
|
||||
]);
|
||||
export type Mode = z.infer<typeof ModeSchema>;
|
||||
|
||||
export const CustomThemeColorsSchema = z.tuple([
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
ColorHexValueSchema,
|
||||
]);
|
||||
export type CustomThemeColors = z.infer<typeof CustomThemeColorsSchema>;
|
||||
|
||||
export const FavThemesSchema = z.array(token().max(50));
|
||||
export type FavThemes = z.infer<typeof FavThemesSchema>;
|
||||
|
||||
export const FunboxSchema = z
|
||||
.string()
|
||||
.max(100)
|
||||
.regex(/[\w#]+/);
|
||||
export type Funbox = z.infer<typeof FunboxSchema>;
|
||||
|
||||
export const PaceCaretCustomSpeedSchema = z.number().nonnegative();
|
||||
export type PaceCaretCustomSpeed = z.infer<typeof PaceCaretCustomSpeedSchema>;
|
||||
|
||||
export const MinWpmCustomSpeedSchema = z.number().nonnegative();
|
||||
export type MinWpmCustomSpeed = z.infer<typeof MinWpmCustomSpeedSchema>;
|
||||
|
||||
export const MinimumAccuracyCustomSchema = z.number().nonnegative().max(100);
|
||||
export type MinimumAccuracyCustom = z.infer<typeof MinimumAccuracyCustomSchema>;
|
||||
|
||||
export const MinimumBurstCustomSpeedSchema = z.number().nonnegative();
|
||||
export type MinimumBurstCustomSpeed = z.infer<
|
||||
typeof MinimumBurstCustomSpeedSchema
|
||||
>;
|
||||
|
||||
export const TimeConfigSchema = z.number().int().nonnegative();
|
||||
export type TimeConfig = z.infer<typeof TimeConfigSchema>;
|
||||
|
||||
export const WordCountSchema = z.number().int().nonnegative();
|
||||
export type WordCount = z.infer<typeof WordCountSchema>;
|
||||
|
||||
export const FontFamilySchema = z
|
||||
.string()
|
||||
.max(50)
|
||||
.regex(/^[a-zA-Z0-9_\-+.]+$/);
|
||||
export type FontFamily = z.infer<typeof FontFamilySchema>;
|
||||
|
||||
export const ThemeNameSchema = token().max(50);
|
||||
export type ThemeName = z.infer<typeof ThemeNameSchema>;
|
||||
|
||||
export const LanguageSchema = z
|
||||
.string()
|
||||
.max(50)
|
||||
.regex(/^[a-zA-Z0-9_+]+$/);
|
||||
export type Language = z.infer<typeof LanguageSchema>;
|
||||
|
||||
export const KeymapLayoutSchema = z
|
||||
.string()
|
||||
.max(50)
|
||||
.regex(/[\w\-_]+/);
|
||||
export type KeymapLayout = z.infer<typeof KeymapLayoutSchema>;
|
||||
|
||||
export const LayoutSchema = token().max(50);
|
||||
export type Layout = z.infer<typeof LayoutSchema>;
|
||||
|
||||
export const FontSizeSchema = z.number().positive();
|
||||
export type FontSize = z.infer<typeof FontSizeSchema>;
|
||||
|
||||
export const MaxLineWidthSchema = z.number().min(20).max(1000).or(z.literal(0));
|
||||
export type MaxLineWidth = z.infer<typeof MaxLineWidthSchema>;
|
||||
|
||||
export const CustomBackgroundSchema = z
|
||||
.string()
|
||||
.regex(/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi)
|
||||
.or(z.literal(""));
|
||||
export type CustomBackground = z.infer<typeof CustomBackgroundSchema>;
|
||||
|
||||
export const ConfigSchema = z
|
||||
.object({
|
||||
theme: ThemeNameSchema,
|
||||
themeLight: ThemeNameSchema,
|
||||
themeDark: ThemeNameSchema,
|
||||
autoSwitchTheme: z.boolean(),
|
||||
customTheme: z.boolean(),
|
||||
//customThemeId: token().nonnegative().max(24),
|
||||
customThemeColors: CustomThemeColorsSchema,
|
||||
favThemes: FavThemesSchema,
|
||||
showKeyTips: z.boolean(),
|
||||
smoothCaret: SmoothCaretSchema,
|
||||
quickRestart: QuickRestartSchema,
|
||||
punctuation: z.boolean(),
|
||||
numbers: z.boolean(),
|
||||
words: WordCountSchema,
|
||||
time: TimeConfigSchema,
|
||||
mode: ModeSchema,
|
||||
quoteLength: QuoteLengthConfigSchema,
|
||||
language: LanguageSchema,
|
||||
fontSize: FontSizeSchema,
|
||||
freedomMode: z.boolean(),
|
||||
difficulty: DifficultySchema,
|
||||
blindMode: z.boolean(),
|
||||
quickEnd: z.boolean(),
|
||||
caretStyle: CaretStyleSchema,
|
||||
paceCaretStyle: CaretStyleSchema,
|
||||
flipTestColors: z.boolean(),
|
||||
layout: LayoutSchema,
|
||||
funbox: FunboxSchema,
|
||||
confidenceMode: ConfidenceModeSchema,
|
||||
indicateTypos: IndicateTyposSchema,
|
||||
timerStyle: TimerStyleSchema,
|
||||
liveSpeedStyle: LiveSpeedAccBurstStyleSchema,
|
||||
liveAccStyle: LiveSpeedAccBurstStyleSchema,
|
||||
liveBurstStyle: LiveSpeedAccBurstStyleSchema,
|
||||
colorfulMode: z.boolean(),
|
||||
randomTheme: RandomThemeSchema,
|
||||
timerColor: TimerColorSchema,
|
||||
timerOpacity: TimerOpacitySchema,
|
||||
stopOnError: StopOnErrorSchema,
|
||||
showAllLines: z.boolean(),
|
||||
keymapMode: KeymapModeSchema,
|
||||
keymapStyle: KeymapStyleSchema,
|
||||
keymapLegendStyle: KeymapLegendStyleSchema,
|
||||
keymapLayout: KeymapLayoutSchema,
|
||||
keymapShowTopRow: KeymapShowTopRowSchema,
|
||||
fontFamily: FontFamilySchema,
|
||||
smoothLineScroll: z.boolean(),
|
||||
alwaysShowDecimalPlaces: z.boolean(),
|
||||
alwaysShowWordsHistory: z.boolean(),
|
||||
singleListCommandLine: SingleListCommandLineSchema,
|
||||
capsLockWarning: z.boolean(),
|
||||
playSoundOnError: PlaySoundOnErrorSchema,
|
||||
playSoundOnClick: PlaySoundOnClickSchema,
|
||||
soundVolume: SoundVolumeSchema,
|
||||
startGraphsAtZero: z.boolean(),
|
||||
showOutOfFocusWarning: z.boolean(),
|
||||
paceCaret: PaceCaretSchema,
|
||||
paceCaretCustomSpeed: PaceCaretCustomSpeedSchema,
|
||||
repeatedPace: z.boolean(),
|
||||
accountChart: AccountChartSchema,
|
||||
minWpm: MinimumWordsPerMinuteSchema,
|
||||
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
|
||||
highlightMode: HighlightModeSchema,
|
||||
tapeMode: TapeModeSchema,
|
||||
typingSpeedUnit: TypingSpeedUnitSchema,
|
||||
ads: AdsSchema,
|
||||
hideExtraLetters: z.boolean(),
|
||||
strictSpace: z.boolean(),
|
||||
minAcc: MinimumAccuracySchema,
|
||||
minAccCustom: MinimumAccuracyCustomSchema,
|
||||
monkey: z.boolean(),
|
||||
repeatQuotes: RepeatQuotesSchema,
|
||||
oppositeShiftMode: OppositeShiftModeSchema,
|
||||
customBackground: CustomBackgroundSchema,
|
||||
customBackgroundSize: CustomBackgroundSizeSchema,
|
||||
customBackgroundFilter: CustomBackgroundFilterSchema,
|
||||
customLayoutfluid: CustomLayoutFluidSchema,
|
||||
monkeyPowerLevel: MonkeyPowerLevelSchema,
|
||||
minBurst: MinimumBurstSchema,
|
||||
minBurstCustomSpeed: MinimumBurstCustomSpeedSchema,
|
||||
burstHeatmap: z.boolean(),
|
||||
britishEnglish: z.boolean(),
|
||||
lazyMode: z.boolean(),
|
||||
showAverage: ShowAverageSchema,
|
||||
maxLineWidth: MaxLineWidthSchema,
|
||||
})
|
||||
.strict();
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
export const PartialConfigSchema = ConfigSchema.partial();
|
||||
export type PartialConfig = z.infer<typeof PartialConfigSchema>;
|
|
@ -1,29 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import { DifficultySchema } from "./config";
|
||||
import { StringNumberSchema } from "./util";
|
||||
|
||||
export const PersonalBestSchema = z.object({
|
||||
acc: z.number().nonnegative().max(100),
|
||||
consistency: z.number().nonnegative().max(100),
|
||||
difficulty: DifficultySchema,
|
||||
lazyMode: z.boolean().optional(),
|
||||
language: z
|
||||
.string()
|
||||
.max(100)
|
||||
.regex(/[\w+]+/),
|
||||
punctuation: z.boolean().optional(),
|
||||
numbers: z.boolean().optional(),
|
||||
raw: z.number().nonnegative(),
|
||||
wpm: z.number().nonnegative(),
|
||||
timestamp: z.number().nonnegative(),
|
||||
});
|
||||
export type PersonalBest = z.infer<typeof PersonalBestSchema>;
|
||||
|
||||
export const PersonalBestsSchema = z.object({
|
||||
time: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
words: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
quote: z.record(StringNumberSchema, z.array(PersonalBestSchema)),
|
||||
custom: z.record(z.literal("custom"), z.array(PersonalBestSchema)),
|
||||
zen: z.record(z.literal("zen"), z.array(PersonalBestSchema)),
|
||||
});
|
||||
export type PersonalBests = z.infer<typeof PersonalBestsSchema>;
|
|
@ -1,9 +0,0 @@
|
|||
import { z, ZodString } from "zod";
|
||||
|
||||
export const StringNumberSchema = z.custom<`${number}`>((val) => {
|
||||
return typeof val === "string" ? /^\d+$/.test(val) : false;
|
||||
});
|
||||
|
||||
export type StringNumber = z.infer<typeof StringNumberSchema>;
|
||||
|
||||
export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/);
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"module": "ESNext", //TODO CommonJS
|
||||
"moduleResolution": "Bundler", //TODO remove?
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"types": ["node"],
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": ["node_modules", "./dist/**/*"]
|
||||
}
|
47
shared/types/config.d.ts
vendored
47
shared/types/config.d.ts
vendored
|
@ -1,47 +0,0 @@
|
|||
declare namespace SharedTypes.Config {
|
||||
type SmoothCaret = import("../schemas/config").SmoothCaret;
|
||||
type QuickRestart = import("../schemas/config").QuickRestart;
|
||||
type QuoteLength = import("../schemas/config").QuoteLength;
|
||||
type CaretStyle = import("../schemas/config").CaretStyle;
|
||||
type Difficulty = import("../schemas/config").Difficulty;
|
||||
type Mode = import("../schemas/config").Mode;
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never;
|
||||
type Mode2Custom<M extends Mode> = Mode2<M> | "custom";
|
||||
type ConfidenceMode = import("../schemas/config").ConfidenceMode;
|
||||
type IndicateTypos = import("../schemas/config").IndicateTypos;
|
||||
type TimerStyle = import("../schemas/config").TimerStyle;
|
||||
type LiveSpeedAccBurstStyle =
|
||||
import("../schemas/config").LiveSpeedAccBurstStyle;
|
||||
type RandomTheme = import("../schemas/config").RandomTheme;
|
||||
type TimerColor = import("../schemas/config").TimerColor;
|
||||
type TimerOpacity = import("../schemas/config").TimerOpacity;
|
||||
type StopOnError = import("../schemas/config").StopOnError;
|
||||
type KeymapMode = import("../schemas/config").KeymapMode;
|
||||
type KeymapStyle = import("../schemas/config").KeymapStyle;
|
||||
type KeymapLegendStyle = import("../schemas/config").KeymapLegendStyle;
|
||||
type KeymapShowTopRow = import("../schemas/config").KeymapShowTopRow;
|
||||
type SingleListCommandLine =
|
||||
import("../schemas/config").SingleListCommandLine;
|
||||
type PlaySoundOnClick = import("../schemas/config").PlaySoundOnClick;
|
||||
type PlaySoundOnError = import("../schemas/config").PlaySoundOnError;
|
||||
type SoundVolume = import("../schemas/config").SoundVolume;
|
||||
type PaceCaret = import("../schemas/config").PaceCaret;
|
||||
type AccountChart = import("../schemas/config").AccountChart;
|
||||
type MinimumWordsPerMinute =
|
||||
import("../schemas/config").MinimumWordsPerMinute;
|
||||
type HighlightMode = import("../schemas/config").HighlightMode;
|
||||
type TypingSpeedUnit = import("../schemas/config").TypingSpeedUnit;
|
||||
type Ads = import("../schemas/config").Ads;
|
||||
type MinimumAccuracy = import("../schemas/config").MinimumAccuracy;
|
||||
type RepeatQuotes = import("../schemas/config").RepeatQuotes;
|
||||
type OppositeShiftMode = import("../schemas/config").OppositeShiftMode;
|
||||
type CustomBackgroundSize = import("../schemas/config").CustomBackgroundSize;
|
||||
type CustomBackgroundFilter =
|
||||
import("../schemas/config").CustomBackgroundFilter;
|
||||
type CustomLayoutFluid = import("../schemas/config").CustomLayoutFluid;
|
||||
type MonkeyPowerLevel = import("../schemas/config").MonkeyPowerLevel;
|
||||
type MinimumBurst = import("../schemas/config").MinimumBurst;
|
||||
type ShowAverage = import("../schemas/config").ShowAverage;
|
||||
type TapeMode = import("../schemas/config").TapeMode;
|
||||
type CustomThemeColors = import("../schemas/config").CustomThemeColors;
|
||||
}
|
Loading…
Reference in a new issue