This commit is contained in:
7
packages/contracts/.oxlintrc.json
Normal file
7
packages/contracts/.oxlintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ignorePatterns": ["node_modules", "dist", ".turbo"],
|
||||
"extends": [
|
||||
"../oxlint-config/index.jsonc"
|
||||
// "@monkeytype/oxlint-config"
|
||||
]
|
||||
}
|
||||
7
packages/contracts/__test__/tsconfig.json
Normal file
7
packages/contracts/__test__/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"]
|
||||
}
|
||||
45
packages/contracts/__test__/validation/validation.spec.ts
Normal file
45
packages/contracts/__test__/validation/validation.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as Validation from "@monkeytype/schemas/validation/validation";
|
||||
|
||||
const containsDisallowedWords = Validation.__testing.containsDisallowedWords;
|
||||
|
||||
describe("validation", () => {
|
||||
it("containsDisallowedWords", () => {
|
||||
const testCases = [
|
||||
{
|
||||
text: "https://www.fuckyou.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
text: "fucking_profane",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
text: "fucker",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
text: "Hello world!",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
text: "I fucking hate you",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
text: "I love you",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
text: "\n.fuck!",
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
expect(containsDisallowedWords(testCase.text, "substring")).toBe(
|
||||
testCase.expected,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
42
packages/contracts/package.json
Normal file
42
packages/contracts/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@monkeytype/contracts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./dist/*.mjs",
|
||||
"require": "./dist/*.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsup-node --watch",
|
||||
"build": "npm run madge && tsup-node",
|
||||
"test": "vitest run",
|
||||
"madge": " madge --circular --extensions ts ./src",
|
||||
"ts-check": "tsc --noEmit",
|
||||
"lint": "oxlint . --type-aware --type-check",
|
||||
"lint-fast": "oxlint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@monkeytype/schemas": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@monkeytype/tsup-config": "workspace:*",
|
||||
"@monkeytype/typescript-config": "workspace:*",
|
||||
"madge": "8.0.0",
|
||||
"oxlint": "1.60.0",
|
||||
"oxlint-tsgolint": "0.21.0",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "6.0.2",
|
||||
"vitest": "4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ts-rest/core": "3.52.1",
|
||||
"zod": "3.23.8"
|
||||
}
|
||||
}
|
||||
142
packages/contracts/src/admin.ts
Normal file
142
packages/contracts/src/admin.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
|
||||
export const ToggleBanRequestSchema = z
|
||||
.object({
|
||||
uid: IdSchema,
|
||||
})
|
||||
.strict();
|
||||
export type ToggleBanRequest = z.infer<typeof ToggleBanRequestSchema>;
|
||||
|
||||
export const ClearStreakHourOffsetRequestSchema = z
|
||||
.object({
|
||||
uid: IdSchema,
|
||||
})
|
||||
.strict();
|
||||
export type ClearStreakHourOffsetRequest = z.infer<
|
||||
typeof ClearStreakHourOffsetRequestSchema
|
||||
>;
|
||||
|
||||
export const ToggleBanResponseSchema = responseWithData(
|
||||
z.object({
|
||||
banned: z.boolean(),
|
||||
}),
|
||||
).strict();
|
||||
export type ToggleBanResponse = z.infer<typeof ToggleBanResponseSchema>;
|
||||
|
||||
export const AcceptReportsRequestSchema = z
|
||||
.object({
|
||||
reports: z.array(z.object({ reportId: z.string() }).strict()).nonempty(),
|
||||
})
|
||||
.strict();
|
||||
export type AcceptReportsRequest = z.infer<typeof AcceptReportsRequestSchema>;
|
||||
|
||||
export const RejectReportsRequestSchema = z
|
||||
.object({
|
||||
reports: z
|
||||
.array(
|
||||
z
|
||||
.object({ reportId: z.string(), reason: z.string().optional() })
|
||||
.strict(),
|
||||
)
|
||||
.nonempty(),
|
||||
})
|
||||
.strict();
|
||||
export type RejectReportsRequest = z.infer<typeof RejectReportsRequestSchema>;
|
||||
|
||||
export const SendForgotPasswordEmailRequestSchema = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
.strict();
|
||||
export type SendForgotPasswordEmailRequest = z.infer<
|
||||
typeof SendForgotPasswordEmailRequestSchema
|
||||
>;
|
||||
|
||||
const c = initContract();
|
||||
export const adminContract = c.router(
|
||||
{
|
||||
test: {
|
||||
summary: "test permission",
|
||||
description: "Check for admin permission for the current user",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
toggleBan: {
|
||||
summary: "toggle user ban",
|
||||
description: "Ban an unbanned user or unban a banned user.",
|
||||
method: "POST",
|
||||
path: "/toggleBan",
|
||||
body: ToggleBanRequestSchema,
|
||||
responses: {
|
||||
200: ToggleBanResponseSchema,
|
||||
},
|
||||
},
|
||||
clearStreakHourOffset: {
|
||||
summary: "clear streak hour offset",
|
||||
description: "Clear the streak hour offset for a user",
|
||||
method: "POST",
|
||||
path: "/clearStreakHourOffset",
|
||||
body: ClearStreakHourOffsetRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
acceptReports: {
|
||||
summary: "accept reports",
|
||||
description: "Accept one or many reports",
|
||||
method: "POST",
|
||||
path: "/report/accept",
|
||||
body: AcceptReportsRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
rejectReports: {
|
||||
summary: "reject reports",
|
||||
description: "Reject one or many reports",
|
||||
method: "POST",
|
||||
path: "/report/reject",
|
||||
body: RejectReportsRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
sendForgotPasswordEmail: {
|
||||
summary: "send forgot password email",
|
||||
description: "Send a forgot password email to the given user email",
|
||||
method: "POST",
|
||||
path: "/sendForgotPasswordEmail",
|
||||
body: SendForgotPasswordEmailRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/admin",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "admin",
|
||||
authenticationOptions: { noCache: true },
|
||||
rateLimit: "adminLimit",
|
||||
requirePermission: "admin",
|
||||
requireConfiguration: {
|
||||
path: "admin.endpointsEnabled",
|
||||
invalidMessage: "Admin endpoints are currently disabled.",
|
||||
},
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
110
packages/contracts/src/ape-keys.ts
Normal file
110
packages/contracts/src/ape-keys.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
ApeKeySchema,
|
||||
ApeKeysSchema,
|
||||
ApeKeyUserDefinedSchema,
|
||||
} from "@monkeytype/schemas/ape-keys";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
export const GetApeKeyResponseSchema = responseWithData(ApeKeysSchema);
|
||||
export type GetApeKeyResponse = z.infer<typeof GetApeKeyResponseSchema>;
|
||||
|
||||
export const AddApeKeyRequestSchema = ApeKeyUserDefinedSchema;
|
||||
export type AddApeKeyRequest = z.infer<typeof AddApeKeyRequestSchema>;
|
||||
|
||||
export const AddApeKeyResponseSchema = responseWithData(
|
||||
z.object({
|
||||
apeKeyId: IdSchema,
|
||||
apeKey: z.string().base64(),
|
||||
apeKeyDetails: ApeKeySchema,
|
||||
}),
|
||||
);
|
||||
export type AddApeKeyResponse = z.infer<typeof AddApeKeyResponseSchema>;
|
||||
|
||||
export const EditApeKeyRequestSchema = AddApeKeyRequestSchema.partial();
|
||||
export type EditApeKeyRequest = z.infer<typeof EditApeKeyRequestSchema>;
|
||||
|
||||
export const ApeKeyParamsSchema = z.object({
|
||||
apeKeyId: IdSchema,
|
||||
});
|
||||
export type ApeKeyParams = z.infer<typeof ApeKeyParamsSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const apeKeysContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get ape keys",
|
||||
description: "Get ape keys of the current user.",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetApeKeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGet",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add ape key",
|
||||
description: "Add an ape key for the current user.",
|
||||
method: "POST",
|
||||
path: "",
|
||||
body: AddApeKeyRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddApeKeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update ape key",
|
||||
description: "Update an existing ape key for the current user.",
|
||||
method: "PATCH",
|
||||
path: "/:apeKeyId",
|
||||
pathParams: ApeKeyParamsSchema,
|
||||
body: EditApeKeyRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete ape key",
|
||||
description: "Delete ape key by id.",
|
||||
method: "DELETE",
|
||||
path: "/:apeKeyId",
|
||||
pathParams: ApeKeyParamsSchema,
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "apeKeysGenerate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/ape-keys",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "ape-keys",
|
||||
requirePermission: "canManageApeKeys",
|
||||
requireConfiguration: {
|
||||
path: "apeKeys.endpointsEnabled",
|
||||
invalidMessage: "ApeKeys are currently disabled.",
|
||||
},
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
69
packages/contracts/src/configs.ts
Normal file
69
packages/contracts/src/configs.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import { PartialConfigSchema } from "@monkeytype/schemas/configs";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithNullableData,
|
||||
} from "./util/api";
|
||||
|
||||
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,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "configGet",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update config",
|
||||
description:
|
||||
"Update the config of the current user. Only provided values will be updated while the missing values will be unchanged.",
|
||||
method: "PATCH",
|
||||
path: "",
|
||||
body: PartialConfigSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "configUpdate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete config",
|
||||
description: "Delete/reset the config for the current user.",
|
||||
method: "DELETE",
|
||||
path: "",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "configDelete",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configs",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "configs",
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
102
packages/contracts/src/configuration.ts
Normal file
102
packages/contracts/src/configuration.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ConfigurationSchema } from "@monkeytype/schemas/configuration";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
|
||||
export const GetConfigurationResponseSchema =
|
||||
responseWithData(ConfigurationSchema);
|
||||
|
||||
export type GetConfigurationResponse = z.infer<
|
||||
typeof GetConfigurationResponseSchema
|
||||
>;
|
||||
|
||||
// marked as deprecated but zod team might reconsider according to https://github.com/colinhacks/zod/issues/2854#issuecomment-3100623150
|
||||
// oxlint-disable-next-line no-deprecated
|
||||
export const PartialConfigurationSchema = ConfigurationSchema.deepPartial();
|
||||
export type PartialConfiguration = z.infer<typeof PartialConfigurationSchema>;
|
||||
|
||||
export const PatchConfigurationRequestSchema = z
|
||||
.object({
|
||||
configuration: PartialConfigurationSchema.strict(),
|
||||
})
|
||||
.strict();
|
||||
export type PatchConfigurationRequest = z.infer<
|
||||
typeof PatchConfigurationRequestSchema
|
||||
>;
|
||||
|
||||
export const ConfigurationSchemaResponseSchema = responseWithData(z.object({})); //TODO define schema?
|
||||
export type ConfigurationSchemaResponse = z.infer<
|
||||
typeof ConfigurationSchemaResponseSchema
|
||||
>;
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const configurationContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get configuration",
|
||||
description: "Get server configuration",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetConfigurationResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
summary: "update configuration",
|
||||
description:
|
||||
"Update the server configuration. Only provided values will be updated while the missing values will be unchanged.",
|
||||
method: "PATCH",
|
||||
path: "",
|
||||
body: PatchConfigurationRequestSchema,
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
noCache: true,
|
||||
isPublicOnDev: true,
|
||||
},
|
||||
rateLimit: "adminLimit",
|
||||
requirePermission: "admin",
|
||||
}),
|
||||
},
|
||||
getSchema: {
|
||||
summary: "get configuration schema",
|
||||
description: "Get schema definition of the server configuration.",
|
||||
method: "GET",
|
||||
path: "/schema",
|
||||
responses: {
|
||||
200: ConfigurationSchemaResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
isPublicOnDev: true,
|
||||
noCache: true,
|
||||
},
|
||||
rateLimit: "adminLimit",
|
||||
requirePermission: "admin",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/configuration",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "configuration",
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
136
packages/contracts/src/connections.ts
Normal file
136
packages/contracts/src/connections.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
|
||||
import {
|
||||
ConnectionSchema,
|
||||
ConnectionStatusSchema,
|
||||
ConnectionTypeSchema,
|
||||
} from "@monkeytype/schemas/connections";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const GetConnectionsResponseSchema = responseWithData(
|
||||
z.array(ConnectionSchema),
|
||||
);
|
||||
export type GetConnectionsResponse = z.infer<
|
||||
typeof GetConnectionsResponseSchema
|
||||
>;
|
||||
|
||||
export const GetConnectionsQuerySchema = z.object({
|
||||
status: z
|
||||
.array(ConnectionStatusSchema)
|
||||
.or(ConnectionStatusSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
type: z
|
||||
.array(ConnectionTypeSchema)
|
||||
.or(ConnectionTypeSchema.transform((it) => [it]))
|
||||
.optional(),
|
||||
});
|
||||
export type GetConnectionsQuery = z.infer<typeof GetConnectionsQuerySchema>;
|
||||
|
||||
export const CreateConnectionRequestSchema = ConnectionSchema.pick({
|
||||
receiverName: true,
|
||||
});
|
||||
export type CreateConnectionRequest = z.infer<
|
||||
typeof CreateConnectionRequestSchema
|
||||
>;
|
||||
|
||||
export const CreateConnectionResponseSchema =
|
||||
responseWithData(ConnectionSchema);
|
||||
export type CreateConnectionResponse = z.infer<
|
||||
typeof CreateConnectionResponseSchema
|
||||
>;
|
||||
|
||||
export const IdPathParamsSchema = z.object({
|
||||
id: IdSchema,
|
||||
});
|
||||
export type IdPathParams = z.infer<typeof IdPathParamsSchema>;
|
||||
|
||||
export const UpdateConnectionRequestSchema = z.object({
|
||||
status: ConnectionStatusSchema.exclude(["pending"]),
|
||||
});
|
||||
export type UpdateConnectionRequest = z.infer<
|
||||
typeof UpdateConnectionRequestSchema
|
||||
>;
|
||||
|
||||
export const connectionsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get connections",
|
||||
description: "Get connections of the current user",
|
||||
method: "GET",
|
||||
path: "/",
|
||||
query: GetConnectionsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetConnectionsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionGet",
|
||||
}),
|
||||
},
|
||||
create: {
|
||||
summary: "create connection",
|
||||
description: "Request a connection to a user ",
|
||||
method: "POST",
|
||||
path: "/",
|
||||
body: CreateConnectionRequestSchema.strict(),
|
||||
responses: {
|
||||
200: CreateConnectionResponseSchema,
|
||||
404: MonkeyResponseSchema.describe("ReceiverUid unknown"),
|
||||
409: MonkeyResponseSchema.describe(
|
||||
"Duplicate connection, blocked or max connections reached",
|
||||
),
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionCreate",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete connection",
|
||||
description: "Delete a connection",
|
||||
method: "DELETE",
|
||||
path: "/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionDelete",
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
summary: "update connection",
|
||||
description: "Update a connection status",
|
||||
method: "PATCH",
|
||||
path: "/:id",
|
||||
pathParams: IdPathParamsSchema.strict(),
|
||||
body: UpdateConnectionRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "connectionUpdate",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/connections",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "connections",
|
||||
requireConfiguration: {
|
||||
path: "connections.enabled",
|
||||
invalidMessage: "Connections are not available at this time.",
|
||||
},
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
79
packages/contracts/src/dev.ts
Normal file
79
packages/contracts/src/dev.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
export const GenerateDataRequestSchema = z.object({
|
||||
username: z.string(),
|
||||
createUser: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"If `true` create user with <username>@example.com and password `password`. If false user has to exist.",
|
||||
),
|
||||
firstTestTimestamp: z.number().int().nonnegative().optional(),
|
||||
lastTestTimestamp: z.number().int().nonnegative().optional(),
|
||||
minTestsPerDay: z.number().int().nonnegative().optional(),
|
||||
maxTestsPerDay: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export type GenerateDataRequest = z.infer<typeof GenerateDataRequestSchema>;
|
||||
|
||||
export const GenerateDataResponseSchema = responseWithData(
|
||||
z.object({
|
||||
uid: IdSchema,
|
||||
email: z.string().email(),
|
||||
}),
|
||||
);
|
||||
export type GenerateDataResponse = z.infer<typeof GenerateDataResponseSchema>;
|
||||
|
||||
export const AddDebugInboxItemRequestSchema = z.object({
|
||||
rewardType: z.enum(["xp", "badge", "none"]),
|
||||
});
|
||||
export type AddDebugInboxItemRequest = z.infer<
|
||||
typeof AddDebugInboxItemRequestSchema
|
||||
>;
|
||||
|
||||
const c = initContract();
|
||||
export const devContract = c.router(
|
||||
{
|
||||
generateData: {
|
||||
summary: "generate data",
|
||||
description: "Generate test results for the given user.",
|
||||
method: "POST",
|
||||
path: "/generateData",
|
||||
body: GenerateDataRequestSchema.strict(),
|
||||
responses: {
|
||||
200: GenerateDataResponseSchema,
|
||||
},
|
||||
},
|
||||
addDebugInboxItem: {
|
||||
summary: "add debug inbox item",
|
||||
description: "Add a debug inbox item with optional reward.",
|
||||
method: "POST",
|
||||
path: "/addDebugInboxItem",
|
||||
body: AddDebugInboxItemRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
openApiTags: "development",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/dev",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "development",
|
||||
authenticationOptions: {
|
||||
isPublicOnDev: true,
|
||||
},
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
41
packages/contracts/src/index.ts
Normal file
41
packages/contracts/src/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { adminContract } from "./admin";
|
||||
import { apeKeysContract } from "./ape-keys";
|
||||
import { configsContract } from "./configs";
|
||||
import { presetsContract } from "./presets";
|
||||
import { psasContract } from "./psas";
|
||||
import { publicContract } from "./public";
|
||||
import { leaderboardsContract } from "./leaderboards";
|
||||
import { resultsContract } from "./results";
|
||||
import { configurationContract } from "./configuration";
|
||||
import { devContract } from "./dev";
|
||||
import { usersContract } from "./users";
|
||||
import { quotesContract } from "./quotes";
|
||||
import { webhooksContract } from "./webhooks";
|
||||
import { connectionsContract } from "./connections";
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const contract = c.router({
|
||||
admin: adminContract,
|
||||
apeKeys: apeKeysContract,
|
||||
configs: configsContract,
|
||||
presets: presetsContract,
|
||||
psas: psasContract,
|
||||
public: publicContract,
|
||||
leaderboards: leaderboardsContract,
|
||||
results: resultsContract,
|
||||
configuration: configurationContract,
|
||||
dev: devContract,
|
||||
users: usersContract,
|
||||
quotes: quotesContract,
|
||||
webhooks: webhooksContract,
|
||||
connections: connectionsContract,
|
||||
});
|
||||
|
||||
/**
|
||||
* Whenever there is a breaking change with old frontend clients increase this number.
|
||||
* This will inform the frontend to refresh.
|
||||
*/
|
||||
export const COMPATIBILITY_CHECK = 4;
|
||||
export const COMPATIBILITY_CHECK_HEADER = "X-Compatibility-Check";
|
||||
260
packages/contracts/src/leaderboards.ts
Normal file
260
packages/contracts/src/leaderboards.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyClientError,
|
||||
responseWithData,
|
||||
responseWithNullableData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
LeaderboardEntrySchema,
|
||||
XpLeaderboardEntrySchema,
|
||||
} from "@monkeytype/schemas/leaderboards";
|
||||
import { Mode2Schema, ModeSchema } from "@monkeytype/schemas/shared";
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { LanguageSchema } from "@monkeytype/schemas/languages";
|
||||
|
||||
const LanguageAndModeQuerySchema = z.object({
|
||||
language: LanguageSchema,
|
||||
mode: ModeSchema,
|
||||
mode2: Mode2Schema,
|
||||
});
|
||||
|
||||
const PaginationQuerySchema = z.object({
|
||||
page: z.number().int().safe().nonnegative().default(0),
|
||||
pageSize: z.number().int().safe().positive().min(10).max(200).default(50),
|
||||
});
|
||||
|
||||
export type PaginationQuery = z.infer<typeof PaginationQuerySchema>;
|
||||
|
||||
const FriendsOnlyQuerySchema = z.object({
|
||||
friendsOnly: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("include only users from your friends list, defaults to false."),
|
||||
});
|
||||
export type FriendsOnlyQuery = z.infer<typeof FriendsOnlyQuerySchema>;
|
||||
|
||||
const LeaderboardResponseSchema = z.object({
|
||||
count: z.number().int().nonnegative(),
|
||||
pageSize: z.number().int().positive(),
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
export const GetLeaderboardQuerySchema = LanguageAndModeQuerySchema.merge(
|
||||
PaginationQuerySchema,
|
||||
).merge(FriendsOnlyQuerySchema);
|
||||
export type GetLeaderboardQuery = z.infer<typeof GetLeaderboardQuerySchema>;
|
||||
|
||||
export const GetLeaderboardResponseSchema = responseWithData(
|
||||
LeaderboardResponseSchema.extend({
|
||||
entries: z.array(LeaderboardEntrySchema),
|
||||
}),
|
||||
);
|
||||
export type GetLeaderboardResponse = z.infer<
|
||||
typeof GetLeaderboardResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
export const GetLeaderboardRankQuerySchema = LanguageAndModeQuerySchema.merge(
|
||||
FriendsOnlyQuerySchema,
|
||||
);
|
||||
export type GetLeaderboardRankQuery = z.infer<
|
||||
typeof GetLeaderboardRankQuerySchema
|
||||
>;
|
||||
export const GetLeaderboardRankResponseSchema = responseWithNullableData(
|
||||
LeaderboardEntrySchema,
|
||||
);
|
||||
export type GetLeaderboardRankResponse = z.infer<
|
||||
typeof GetLeaderboardRankResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
export const DailyLeaderboardQuerySchema = LanguageAndModeQuerySchema.extend({
|
||||
daysBefore: z.literal(1).optional(),
|
||||
}).merge(FriendsOnlyQuerySchema);
|
||||
export type DailyLeaderboardQuery = z.infer<typeof DailyLeaderboardQuerySchema>;
|
||||
|
||||
export const GetDailyLeaderboardQuerySchema = DailyLeaderboardQuerySchema.merge(
|
||||
PaginationQuerySchema,
|
||||
);
|
||||
export type GetDailyLeaderboardQuery = z.infer<
|
||||
typeof GetDailyLeaderboardQuerySchema
|
||||
>;
|
||||
export const GetDailyLeaderboardResponseSchema = responseWithData(
|
||||
LeaderboardResponseSchema.extend({
|
||||
entries: z.array(LeaderboardEntrySchema),
|
||||
minWpm: z.number().nonnegative(),
|
||||
}),
|
||||
);
|
||||
export type GetDailyLeaderboardResponse = z.infer<
|
||||
typeof GetDailyLeaderboardResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
export const GetDailyLeaderboardRankQuerySchema = DailyLeaderboardQuerySchema;
|
||||
|
||||
export type GetDailyLeaderboardRankQuery = z.infer<
|
||||
typeof GetDailyLeaderboardRankQuerySchema
|
||||
>;
|
||||
export const GetLeaderboardDailyRankResponseSchema = responseWithNullableData(
|
||||
LeaderboardEntrySchema,
|
||||
);
|
||||
export type GetLeaderboardDailyRankResponse = z.infer<
|
||||
typeof GetLeaderboardDailyRankResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const WeeklyXpLeaderboardQuerySchema = z
|
||||
.object({
|
||||
weeksBefore: z.literal(1).optional(),
|
||||
})
|
||||
.merge(FriendsOnlyQuerySchema);
|
||||
|
||||
export const GetWeeklyXpLeaderboardQuerySchema =
|
||||
WeeklyXpLeaderboardQuerySchema.merge(PaginationQuerySchema);
|
||||
|
||||
export type GetWeeklyXpLeaderboardQuery = z.infer<
|
||||
typeof GetWeeklyXpLeaderboardQuerySchema
|
||||
>;
|
||||
export const GetWeeklyXpLeaderboardResponseSchema = responseWithData(
|
||||
LeaderboardResponseSchema.extend({
|
||||
entries: z.array(XpLeaderboardEntrySchema),
|
||||
}),
|
||||
);
|
||||
export type GetWeeklyXpLeaderboardResponse = z.infer<
|
||||
typeof GetWeeklyXpLeaderboardResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
export const GetWeeklyXpLeaderboardRankQuerySchema =
|
||||
WeeklyXpLeaderboardQuerySchema;
|
||||
export type GetWeeklyXpLeaderboardRankQuery = z.infer<
|
||||
typeof GetWeeklyXpLeaderboardRankQuerySchema
|
||||
>;
|
||||
|
||||
export const GetWeeklyXpLeaderboardRankResponseSchema =
|
||||
responseWithNullableData(XpLeaderboardEntrySchema);
|
||||
export type GetWeeklyXpLeaderboardRankResponse = z.infer<
|
||||
typeof GetWeeklyXpLeaderboardRankResponseSchema
|
||||
>;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const c = initContract();
|
||||
export const leaderboardsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get leaderboard",
|
||||
description: "Get all-time leaderboard.",
|
||||
method: "GET",
|
||||
path: "",
|
||||
query: GetLeaderboardQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetLeaderboardResponseSchema,
|
||||
404: MonkeyClientError,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
}),
|
||||
},
|
||||
getRank: {
|
||||
summary: "get leaderboard rank",
|
||||
description:
|
||||
"Get the rank of the current user on the all-time leaderboard",
|
||||
method: "GET",
|
||||
path: "/rank",
|
||||
query: GetLeaderboardRankQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetLeaderboardRankResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
}),
|
||||
},
|
||||
getDaily: {
|
||||
summary: "get daily leaderboard",
|
||||
description: "Get daily leaderboard.",
|
||||
method: "GET",
|
||||
path: "/daily",
|
||||
query: GetDailyLeaderboardQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetDailyLeaderboardResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
requireConfiguration: {
|
||||
path: "dailyLeaderboards.enabled",
|
||||
invalidMessage: "Daily leaderboards are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getDailyRank: {
|
||||
summary: "get daily leaderboard rank",
|
||||
description: "Get the rank of the current user on the daily leaderboard",
|
||||
method: "GET",
|
||||
path: "/daily/rank",
|
||||
query: GetDailyLeaderboardRankQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetLeaderboardDailyRankResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
requireConfiguration: {
|
||||
path: "dailyLeaderboards.enabled",
|
||||
invalidMessage: "Daily leaderboards are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getWeeklyXp: {
|
||||
summary: "get weekly xp leaderboard",
|
||||
description: "Get weekly xp leaderboard",
|
||||
method: "GET",
|
||||
path: "/xp/weekly",
|
||||
query: GetWeeklyXpLeaderboardQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetWeeklyXpLeaderboardResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
requireConfiguration: {
|
||||
path: "leaderboards.weeklyXp.enabled",
|
||||
invalidMessage:
|
||||
"Weekly XP leaderboards are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getWeeklyXpRank: {
|
||||
summary: "get weekly xp leaderboard rank",
|
||||
description:
|
||||
"Get the rank of the current user on the weekly xp leaderboard",
|
||||
method: "GET",
|
||||
path: "/xp/weekly/rank",
|
||||
query: GetWeeklyXpLeaderboardRankQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetWeeklyXpLeaderboardRankResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
requireConfiguration: {
|
||||
path: "leaderboards.weeklyXp.enabled",
|
||||
invalidMessage:
|
||||
"Weekly XP leaderboards are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/leaderboards",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "leaderboards",
|
||||
rateLimit: "leaderboardsGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
98
packages/contracts/src/presets.ts
Normal file
98
packages/contracts/src/presets.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
EditPresetRequestSchema,
|
||||
PresetSchema,
|
||||
} from "@monkeytype/schemas/presets";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
export const GetPresetResponseSchema = responseWithData(z.array(PresetSchema));
|
||||
export type GetPresetResponse = z.infer<typeof GetPresetResponseSchema>;
|
||||
|
||||
export const AddPresetRequestSchema = PresetSchema.omit({ _id: true });
|
||||
export type AddPresetRequest = z.infer<typeof AddPresetRequestSchema>;
|
||||
|
||||
export const AddPresetResponseSchemna = responseWithData(
|
||||
z.object({ presetId: IdSchema }),
|
||||
);
|
||||
export type AddPresetResponse = z.infer<typeof AddPresetResponseSchemna>;
|
||||
|
||||
export const DeletePresetsParamsSchema = z.object({
|
||||
presetId: IdSchema,
|
||||
});
|
||||
export type DeletePresetsParams = z.infer<typeof DeletePresetsParamsSchema>;
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const presetsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get presets",
|
||||
description: "Get presets of the current user.",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetPresetResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsGet",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add preset",
|
||||
description: "Add a new preset for the current user.",
|
||||
method: "POST",
|
||||
path: "",
|
||||
body: AddPresetRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddPresetResponseSchemna,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsAdd",
|
||||
}),
|
||||
},
|
||||
save: {
|
||||
summary: "update preset",
|
||||
description: "Update an existing preset for the current user.",
|
||||
method: "PATCH",
|
||||
path: "",
|
||||
body: EditPresetRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsEdit",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete preset",
|
||||
description: "Delete preset by id.",
|
||||
method: "DELETE",
|
||||
path: "/:presetId",
|
||||
pathParams: DeletePresetsParamsSchema,
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "presetsRemove",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/presets",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "presets",
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
34
packages/contracts/src/psas.ts
Normal file
34
packages/contracts/src/psas.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import { PSASchema } from "@monkeytype/schemas/psas";
|
||||
import { CommonResponses, meta, responseWithData } from "./util/api";
|
||||
|
||||
export const GetPsaResponseSchema = responseWithData(z.array(PSASchema));
|
||||
export type GetPsaResponse = z.infer<typeof GetPsaResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const psasContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get psas",
|
||||
description: "Get list of public service announcements",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetPsaResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/psas",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "psas",
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
rateLimit: "psaGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
70
packages/contracts/src/public.ts
Normal file
70
packages/contracts/src/public.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import { CommonResponses, meta, responseWithData } from "./util/api";
|
||||
import {
|
||||
SpeedHistogramSchema,
|
||||
TypingStatsSchema,
|
||||
} from "@monkeytype/schemas/public";
|
||||
import { Mode2Schema, ModeSchema } from "@monkeytype/schemas/shared";
|
||||
import { LanguageSchema } from "@monkeytype/schemas/languages";
|
||||
|
||||
export const GetSpeedHistogramQuerySchema = z
|
||||
.object({
|
||||
language: LanguageSchema,
|
||||
mode: ModeSchema,
|
||||
mode2: Mode2Schema,
|
||||
})
|
||||
.strict();
|
||||
export type GetSpeedHistogramQuery = z.infer<
|
||||
typeof GetSpeedHistogramQuerySchema
|
||||
>;
|
||||
|
||||
export const GetSpeedHistogramResponseSchema =
|
||||
responseWithData(SpeedHistogramSchema);
|
||||
export type GetSpeedHistogramResponse = z.infer<
|
||||
typeof GetSpeedHistogramResponseSchema
|
||||
>;
|
||||
|
||||
export const GetTypingStatsResponseSchema = responseWithData(TypingStatsSchema);
|
||||
export type GetTypingStatsResponse = z.infer<
|
||||
typeof GetTypingStatsResponseSchema
|
||||
>;
|
||||
|
||||
const c = initContract();
|
||||
export const publicContract = c.router(
|
||||
{
|
||||
getSpeedHistogram: {
|
||||
summary: "get speed histogram",
|
||||
description:
|
||||
"get number of users personal bests grouped by wpm level (multiples of ten)",
|
||||
method: "GET",
|
||||
path: "/speedHistogram",
|
||||
query: GetSpeedHistogramQuerySchema,
|
||||
responses: {
|
||||
200: GetSpeedHistogramResponseSchema,
|
||||
},
|
||||
},
|
||||
|
||||
getTypingStats: {
|
||||
summary: "get typing stats",
|
||||
description: "get number of tests and time users spend typing.",
|
||||
method: "GET",
|
||||
path: "/typingStats",
|
||||
responses: {
|
||||
200: GetTypingStatsResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/public",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "public",
|
||||
authenticationOptions: {
|
||||
isPublic: true,
|
||||
},
|
||||
rateLimit: "publicStatsGet",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
217
packages/contracts/src/quotes.ts
Normal file
217
packages/contracts/src/quotes.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
responseWithNullableData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
ApproveQuoteSchema,
|
||||
QuoteIdSchema,
|
||||
QuoteRatingSchema,
|
||||
QuoteReportReasonSchema,
|
||||
QuoteSchema,
|
||||
} from "@monkeytype/schemas/quotes";
|
||||
import { IdSchema, NullableStringSchema } from "@monkeytype/schemas/util";
|
||||
import { LanguageSchema } from "@monkeytype/schemas/languages";
|
||||
|
||||
export const GetQuotesResponseSchema = responseWithData(z.array(QuoteSchema));
|
||||
export type GetQuotesResponse = z.infer<typeof GetQuotesResponseSchema>;
|
||||
|
||||
export const IsSubmissionEnabledResponseSchema = responseWithData(
|
||||
z.object({
|
||||
isEnabled: z.boolean(),
|
||||
}),
|
||||
);
|
||||
export type IsSubmissionEnabledResponse = z.infer<
|
||||
typeof IsSubmissionEnabledResponseSchema
|
||||
>;
|
||||
|
||||
export const AddQuoteRequestSchema = z.object({
|
||||
text: z.string().min(60),
|
||||
source: z.string(),
|
||||
language: LanguageSchema,
|
||||
captcha: z.string(), //we don't generate the captcha so there should be no validation
|
||||
});
|
||||
export type AddQuoteRequest = z.infer<typeof AddQuoteRequestSchema>;
|
||||
|
||||
export const ApproveQuoteRequestSchema = z.object({
|
||||
quoteId: IdSchema,
|
||||
editText: NullableStringSchema,
|
||||
editSource: NullableStringSchema,
|
||||
});
|
||||
export type ApproveQuoteRequest = z.infer<typeof ApproveQuoteRequestSchema>;
|
||||
|
||||
export const ApproveQuoteResponseSchema = responseWithData(ApproveQuoteSchema);
|
||||
export type ApproveQuoteResponse = z.infer<typeof ApproveQuoteResponseSchema>;
|
||||
|
||||
export const RejectQuoteRequestSchema = z.object({
|
||||
quoteId: IdSchema,
|
||||
});
|
||||
export type RejectQuoteRequest = z.infer<typeof RejectQuoteRequestSchema>;
|
||||
|
||||
export const GetQuoteRatingQuerySchema = z.object({
|
||||
quoteId: QuoteIdSchema,
|
||||
language: LanguageSchema,
|
||||
});
|
||||
export type GetQuoteRatingQuery = z.infer<typeof GetQuoteRatingQuerySchema>;
|
||||
|
||||
export const GetQuoteRatingResponseSchema =
|
||||
responseWithNullableData(QuoteRatingSchema);
|
||||
export type GetQuoteRatingResponse = z.infer<
|
||||
typeof GetQuoteRatingResponseSchema
|
||||
>;
|
||||
|
||||
export const AddQuoteRatingRequestSchema = z.object({
|
||||
quoteId: QuoteIdSchema,
|
||||
language: LanguageSchema,
|
||||
rating: z.number().int().min(1).max(5),
|
||||
});
|
||||
export type AddQuoteRatingRequest = z.infer<typeof AddQuoteRatingRequestSchema>;
|
||||
|
||||
export const ReportQuoteRequestSchema = z.object({
|
||||
quoteId: QuoteIdSchema,
|
||||
quoteLanguage: LanguageSchema,
|
||||
reason: QuoteReportReasonSchema,
|
||||
comment: z
|
||||
.string()
|
||||
.regex(/^([.]|[^/<>])+$/)
|
||||
.max(250)
|
||||
.optional()
|
||||
.or(z.string().length(0)),
|
||||
captcha: z.string(), //we don't generate the captcha so there should be no validation
|
||||
});
|
||||
export type ReportQuoteRequest = z.infer<typeof ReportQuoteRequestSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const quotesContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get quote submissions",
|
||||
description: "Get list of quote submissions",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetQuotesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesGet",
|
||||
requirePermission: "quoteMod",
|
||||
}),
|
||||
},
|
||||
isSubmissionEnabled: {
|
||||
summary: "is submission enabled",
|
||||
description: "Check if submissions are enabled.",
|
||||
method: "GET",
|
||||
path: "/isSubmissionEnabled",
|
||||
responses: {
|
||||
200: IsSubmissionEnabledResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
rateLimit: "newQuotesIsSubmissionEnabled",
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "submit quote",
|
||||
description: "Add a quote submission",
|
||||
method: "POST",
|
||||
path: "",
|
||||
body: AddQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAdd",
|
||||
requireConfiguration: {
|
||||
path: "quotes.submissionsEnabled",
|
||||
invalidMessage:
|
||||
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
approveSubmission: {
|
||||
summary: "submit quote",
|
||||
description: "Add a quote submission",
|
||||
method: "POST",
|
||||
path: "/approve",
|
||||
body: ApproveQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: ApproveQuoteResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAction",
|
||||
requirePermission: "quoteMod",
|
||||
}),
|
||||
},
|
||||
rejectSubmission: {
|
||||
summary: "reject quote",
|
||||
description: "Reject a quote submission",
|
||||
method: "POST",
|
||||
path: "/reject",
|
||||
body: RejectQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "newQuotesAction",
|
||||
requirePermission: "quoteMod",
|
||||
}),
|
||||
},
|
||||
getRating: {
|
||||
summary: "get rating",
|
||||
description: "Get quote rating",
|
||||
method: "GET",
|
||||
path: "/rating",
|
||||
query: GetQuoteRatingQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetQuoteRatingResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteRatingsGet",
|
||||
}),
|
||||
},
|
||||
addRating: {
|
||||
summary: "add rating",
|
||||
description: "Add a quote rating",
|
||||
method: "POST",
|
||||
path: "/rating",
|
||||
body: AddQuoteRatingRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteRatingsSubmit",
|
||||
}),
|
||||
},
|
||||
report: {
|
||||
summary: "report quote",
|
||||
description: "Report a quote",
|
||||
method: "POST",
|
||||
path: "/report",
|
||||
body: ReportQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteReportSubmit",
|
||||
requirePermission: "canReport",
|
||||
requireConfiguration: {
|
||||
path: "quotes.reporting.enabled",
|
||||
invalidMessage: "Quote reporting is unavailable.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/quotes",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "quotes",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
411
packages/contracts/src/rate-limit/index.ts
Normal file
411
packages/contracts/src/rate-limit/index.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
export type Window = "second" | "minute" | "hour" | "day" | number;
|
||||
export type RateLimitOptions = {
|
||||
/** Timeframe or time in milliseconds */
|
||||
window: Window;
|
||||
/** Max request within the given window */
|
||||
max: number;
|
||||
};
|
||||
|
||||
export const limits = {
|
||||
defaultApeRateLimit: {
|
||||
window: "minute",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
adminLimit: {
|
||||
window: 5000,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
// Config Routing
|
||||
configUpdate: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
configGet: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
configDelete: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
// Leaderboards Routing
|
||||
leaderboardsGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// New Quotes Routing
|
||||
newQuotesGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
newQuotesIsSubmissionEnabled: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
newQuotesAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
newQuotesAction: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// Quote Ratings Routing
|
||||
quoteRatingsGet: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
quoteRatingsSubmit: {
|
||||
window: "hour",
|
||||
max: 500,
|
||||
},
|
||||
|
||||
// Quote reporting
|
||||
quoteReportSubmit: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
// Quote favorites
|
||||
quoteFavoriteGet: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
quoteFavoritePost: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
quoteFavoriteDelete: {
|
||||
window: 30 * 60 * 1000, // 30 min
|
||||
max: 50,
|
||||
},
|
||||
|
||||
// Presets Routing
|
||||
presetsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsRemove: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
presetsEdit: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// PSA (Public Service Announcement) Routing
|
||||
psaGet: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// get public speed stats
|
||||
publicStatsGet: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Results Routing
|
||||
resultsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Results Routing
|
||||
resultsGetApe: {
|
||||
window: "day",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
// Result by id
|
||||
resultByIdGet: {
|
||||
window: "hour",
|
||||
max: 300,
|
||||
},
|
||||
|
||||
// Result by id
|
||||
resultByIdGetApe: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
resultsAdd: {
|
||||
window: "hour",
|
||||
max: 300,
|
||||
},
|
||||
|
||||
resultsTagsUpdate: {
|
||||
window: "hour",
|
||||
max: 100,
|
||||
},
|
||||
|
||||
resultsDeleteAll: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
resultsLeaderboardGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
resultsLeaderboardQualificationGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// Users Routing
|
||||
userGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
setStreakHourOffset: {
|
||||
window: "hour",
|
||||
max: 5,
|
||||
},
|
||||
|
||||
userSignup: {
|
||||
window: "day",
|
||||
max: 2,
|
||||
},
|
||||
|
||||
userDelete: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userReset: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userCheckName: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userUpdateName: {
|
||||
window: "day",
|
||||
max: 3,
|
||||
},
|
||||
|
||||
userUpdateLBMemory: {
|
||||
window: "minute",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userUpdateEmail: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userClearPB: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userOptOutOfLeaderboards: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
userCustomFilterAdd: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userCustomFilterRemove: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsRemove: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userTagsClearPB: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTagsEdit: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userTagsAdd: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeGet: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeAdd: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeRemove: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userCustomThemeEdit: {
|
||||
window: "hour",
|
||||
max: 30,
|
||||
},
|
||||
|
||||
userDiscordLink: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
userDiscordUnlink: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
userRequestVerificationEmail: {
|
||||
window: 15 * 60 * 1000, //15 minutes
|
||||
max: 1,
|
||||
},
|
||||
|
||||
userForgotPasswordEmail: {
|
||||
window: "minute",
|
||||
max: 1,
|
||||
},
|
||||
|
||||
userRevokeAllTokens: {
|
||||
window: "hour",
|
||||
max: 10,
|
||||
},
|
||||
|
||||
userProfileGet: {
|
||||
window: "hour",
|
||||
max: 100,
|
||||
},
|
||||
|
||||
userProfileUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userMailGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userMailUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userTestActivity: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userCurrentTestActivity: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userStreak: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
userFriendGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
// ApeKeys Routing
|
||||
apeKeysGet: {
|
||||
window: "hour",
|
||||
max: 120,
|
||||
},
|
||||
|
||||
apeKeysGenerate: {
|
||||
window: "hour",
|
||||
max: 15,
|
||||
},
|
||||
|
||||
webhookLimit: {
|
||||
window: "second",
|
||||
max: 1,
|
||||
},
|
||||
|
||||
connectionGet: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
connectionCreate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
connectionDelete: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
|
||||
connectionUpdate: {
|
||||
window: "hour",
|
||||
max: 60,
|
||||
},
|
||||
} satisfies Record<string, RateLimitOptions>;
|
||||
|
||||
export type RateLimiterId = keyof typeof limits;
|
||||
export type RateLimitIds = {
|
||||
/** rate limiter options for non-apeKey requests */
|
||||
normal: RateLimiterId;
|
||||
/** Rate limiter options for apeKey requests */
|
||||
apeKey: RateLimiterId;
|
||||
};
|
||||
|
||||
export function getLimits(limit: RateLimiterId | RateLimitIds): {
|
||||
limiter: RateLimitOptions;
|
||||
apeKeyLimiter?: RateLimitOptions;
|
||||
} {
|
||||
const isApeKeyLimiter = typeof limit === "object";
|
||||
const limiter = isApeKeyLimiter ? limit.normal : limit;
|
||||
const apeLimiter = isApeKeyLimiter ? limit.apeKey : undefined;
|
||||
|
||||
return {
|
||||
limiter: limits[limiter],
|
||||
apeKeyLimiter: apeLimiter !== undefined ? limits[apeLimiter] : undefined,
|
||||
};
|
||||
}
|
||||
27
packages/contracts/src/require-configuration/index.ts
Normal file
27
packages/contracts/src/require-configuration/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Configuration } from "@monkeytype/schemas/configuration";
|
||||
|
||||
type BooleanPaths<T, P extends string = ""> = {
|
||||
[K in keyof T]: T[K] extends boolean
|
||||
? P extends ""
|
||||
? K
|
||||
: `${P}.${Extract<K, string | number>}`
|
||||
: T[K] extends object
|
||||
? `${P}.${Extract<K, string | number>}` extends infer D
|
||||
? D extends string
|
||||
? BooleanPaths<T[K], D>
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
// Helper type to remove leading dot
|
||||
type RemoveLeadingDot<T> = T extends `.${infer U}` ? U : T;
|
||||
|
||||
export type ConfigurationPath = RemoveLeadingDot<BooleanPaths<Configuration>>;
|
||||
|
||||
export type RequireConfiguration = {
|
||||
/** path to the configuration, needs to be a boolean value */
|
||||
path: ConfigurationPath;
|
||||
/** message of the ErrorResponse in case the value is `false` */
|
||||
invalidMessage?: string;
|
||||
};
|
||||
204
packages/contracts/src/results.ts
Normal file
204
packages/contracts/src/results.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyClientError,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
CompletedEventSchema,
|
||||
PostResultResponseSchema,
|
||||
ResultMinifiedSchema,
|
||||
ResultSchema,
|
||||
} from "@monkeytype/schemas/results";
|
||||
import { IdSchema } from "@monkeytype/schemas/util";
|
||||
|
||||
export const GetResultsQuerySchema = z.object({
|
||||
onOrAfterTimestamp: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1589428800000)
|
||||
.optional()
|
||||
.describe(
|
||||
"Timestamp of the earliest result to fetch. If omitted the most recent results are fetched.",
|
||||
),
|
||||
offset: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.describe("Offset of the item at which to begin the response."),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.max(1000)
|
||||
.optional()
|
||||
.describe("Limit results to the given amount."),
|
||||
});
|
||||
export type GetResultsQuery = z.infer<typeof GetResultsQuerySchema>;
|
||||
|
||||
export const GetResultsResponseSchema = responseWithData(
|
||||
z.array(ResultMinifiedSchema),
|
||||
);
|
||||
export type GetResultsResponse = z.infer<typeof GetResultsResponseSchema>;
|
||||
|
||||
export const GetResultByIdPathSchema = z.object({
|
||||
resultId: IdSchema,
|
||||
});
|
||||
export type GetResultByIdPath = z.infer<typeof GetResultByIdPathSchema>;
|
||||
|
||||
export const GetResultByIdResponseSchema = responseWithData(ResultSchema);
|
||||
|
||||
export type GetResultByIdResponse = z.infer<typeof GetResultByIdResponseSchema>;
|
||||
|
||||
export const AddResultRequestSchema = z.object({
|
||||
result: CompletedEventSchema,
|
||||
});
|
||||
export type AddResultRequest = z.infer<typeof AddResultRequestSchema>;
|
||||
|
||||
export const AddResultResponseSchema = responseWithData(
|
||||
PostResultResponseSchema,
|
||||
);
|
||||
export type AddResultResponse = z.infer<typeof AddResultResponseSchema>;
|
||||
|
||||
export const UpdateResultTagsRequestSchema = z.object({
|
||||
tagIds: z.array(IdSchema),
|
||||
resultId: IdSchema,
|
||||
});
|
||||
export type UpdateResultTagsRequest = z.infer<
|
||||
typeof UpdateResultTagsRequestSchema
|
||||
>;
|
||||
export const UpdateResultTagsResponseSchema = responseWithData(
|
||||
z.object({
|
||||
tagPbs: z.array(IdSchema),
|
||||
}),
|
||||
);
|
||||
export type UpdateResultTagsResponse = z.infer<
|
||||
typeof UpdateResultTagsResponseSchema
|
||||
>;
|
||||
|
||||
export const GetLastResultResponseSchema = responseWithData(ResultSchema);
|
||||
export type GetLastResultResponse = z.infer<typeof GetLastResultResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const resultsContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get results",
|
||||
description: "Gets up to 1000 results",
|
||||
method: "GET",
|
||||
path: "",
|
||||
query: GetResultsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetResultsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
acceptApeKeys: true,
|
||||
},
|
||||
rateLimit: {
|
||||
normal: "resultsGet",
|
||||
apeKey: "resultsGetApe",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getById: {
|
||||
summary: "get result by id",
|
||||
description: "Get result by id",
|
||||
method: "GET",
|
||||
path: "/id/:resultId",
|
||||
pathParams: GetResultByIdPathSchema,
|
||||
responses: {
|
||||
200: GetResultByIdResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
acceptApeKeys: true,
|
||||
},
|
||||
rateLimit: {
|
||||
normal: "resultByIdGet",
|
||||
apeKey: "resultByIdGetApe",
|
||||
},
|
||||
}),
|
||||
},
|
||||
add: {
|
||||
summary: "add result",
|
||||
description: "Add a test result for the current user",
|
||||
method: "POST",
|
||||
path: "",
|
||||
body: AddResultRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddResultResponseSchema,
|
||||
460: MonkeyClientError.describe("Test too short"),
|
||||
461: MonkeyClientError.describe("Result hash invalid"),
|
||||
462: MonkeyClientError.describe("Result spacing invalid"),
|
||||
463: MonkeyClientError.describe("Result data invalid"),
|
||||
464: MonkeyClientError.describe("Missing key data"),
|
||||
465: MonkeyClientError.describe("Bot detected"),
|
||||
466: MonkeyClientError.describe("Duplicate result"),
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "resultsAdd",
|
||||
requireConfiguration: {
|
||||
path: "results.savingEnabled",
|
||||
invalidMessage: "Results are not being saved at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
updateTags: {
|
||||
summary: "update result tags",
|
||||
description: "Labels a result with the specified tags",
|
||||
method: "PATCH",
|
||||
path: "/tags",
|
||||
body: UpdateResultTagsRequestSchema.strict(),
|
||||
responses: {
|
||||
200: UpdateResultTagsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "resultsTagsUpdate",
|
||||
}),
|
||||
},
|
||||
deleteAll: {
|
||||
summary: "delete all results",
|
||||
description: "Delete all results for the current user",
|
||||
method: "DELETE",
|
||||
path: "",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
requireFreshToken: true,
|
||||
},
|
||||
rateLimit: "resultsDeleteAll",
|
||||
}),
|
||||
},
|
||||
getLast: {
|
||||
summary: "get last result",
|
||||
description: "Gets a user's last saved result",
|
||||
path: "/last",
|
||||
method: "GET",
|
||||
responses: {
|
||||
200: GetLastResultResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: {
|
||||
acceptApeKeys: true,
|
||||
},
|
||||
rateLimit: "resultsGet",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/results",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "results",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
965
packages/contracts/src/users.ts
Normal file
965
packages/contracts/src/users.ts
Normal file
@@ -0,0 +1,965 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyClientError,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
responseWithNullableData,
|
||||
} from "./util/api";
|
||||
import {
|
||||
CountByYearAndDaySchema,
|
||||
CustomThemeNameSchema,
|
||||
CustomThemeSchema,
|
||||
FavoriteQuotesSchema,
|
||||
MonkeyMailSchema,
|
||||
ResultFiltersSchema,
|
||||
StreakHourOffsetSchema,
|
||||
TagNameSchema,
|
||||
TestActivitySchema,
|
||||
UserProfileDetailsSchema,
|
||||
UserProfileSchema,
|
||||
ReportUserReasonSchema,
|
||||
UserSchema,
|
||||
UserStreakSchema,
|
||||
UserTagSchema,
|
||||
UserEmailSchema,
|
||||
UserNameSchema,
|
||||
FriendSchema,
|
||||
} from "@monkeytype/schemas/users";
|
||||
import {
|
||||
Mode2Schema,
|
||||
ModeSchema,
|
||||
PersonalBestSchema,
|
||||
} from "@monkeytype/schemas/shared";
|
||||
import { IdSchema, StringNumberSchema } from "@monkeytype/schemas/util";
|
||||
import { LanguageSchema } from "@monkeytype/schemas/languages";
|
||||
import { CustomThemeColorsSchema } from "@monkeytype/schemas/configs";
|
||||
|
||||
export const GetUserResponseSchema = responseWithData(
|
||||
UserSchema.extend({
|
||||
inboxUnreadSize: z.number().int().nonnegative(),
|
||||
}),
|
||||
);
|
||||
export type GetUserResponse = z.infer<typeof GetUserResponseSchema>;
|
||||
|
||||
export const CreateUserRequestSchema = z.object({
|
||||
email: UserEmailSchema.optional(),
|
||||
name: UserNameSchema,
|
||||
uid: z.string().optional(), //defined by firebase, no validation should be applied
|
||||
captcha: z.string(), //defined by google recaptcha, no validation should be applied
|
||||
});
|
||||
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
|
||||
|
||||
export const CheckNamePathParametersSchema = z.object({
|
||||
name: UserNameSchema,
|
||||
});
|
||||
export type CheckNamePathParameters = z.infer<
|
||||
typeof CheckNamePathParametersSchema
|
||||
>;
|
||||
|
||||
export const CheckNameResponseSchema = responseWithData(
|
||||
z.object({
|
||||
available: z.boolean(),
|
||||
}),
|
||||
);
|
||||
export type CheckNameResponse = z.infer<typeof CheckNameResponseSchema>;
|
||||
|
||||
export const UpdateUserNameRequestSchema = z.object({
|
||||
name: UserNameSchema,
|
||||
});
|
||||
export type UpdateUserNameRequest = z.infer<typeof UpdateUserNameRequestSchema>;
|
||||
|
||||
export const UpdateLeaderboardMemoryRequestSchema = z.object({
|
||||
mode: ModeSchema,
|
||||
mode2: Mode2Schema,
|
||||
language: LanguageSchema,
|
||||
rank: z.number().int().nonnegative(),
|
||||
});
|
||||
export type UpdateLeaderboardMemoryRequest = z.infer<
|
||||
typeof UpdateLeaderboardMemoryRequestSchema
|
||||
>;
|
||||
|
||||
export const UpdateEmailRequestSchema = z.object({
|
||||
newEmail: UserEmailSchema,
|
||||
previousEmail: UserEmailSchema,
|
||||
});
|
||||
export type UpdateEmailRequest = z.infer<typeof UpdateEmailRequestSchema>;
|
||||
|
||||
export const UpdatePasswordRequestSchema = z.object({
|
||||
newPassword: z.string().min(6),
|
||||
});
|
||||
export type UpdatePasswordRequest = z.infer<typeof UpdatePasswordRequestSchema>;
|
||||
|
||||
export const GetPersonalBestsQuerySchema = z.object({
|
||||
mode: ModeSchema,
|
||||
mode2: Mode2Schema.optional(),
|
||||
});
|
||||
export type GetPersonalBestsQuery = z.infer<typeof GetPersonalBestsQuerySchema>;
|
||||
|
||||
export const GetPersonalBestsResponseSchema =
|
||||
responseWithNullableData(PersonalBestSchema);
|
||||
export type GetPersonalBestsResponse = z.infer<
|
||||
typeof GetPersonalBestsResponseSchema
|
||||
>;
|
||||
|
||||
export const AddResultFilterPresetRequestSchema = ResultFiltersSchema;
|
||||
export type AddResultFilterPresetRequest = z.infer<
|
||||
typeof AddResultFilterPresetRequestSchema
|
||||
>;
|
||||
export const AddResultFilterPresetResponseSchema = responseWithData(
|
||||
IdSchema.describe("Id of the created result filter preset"),
|
||||
);
|
||||
export type AddResultFilterPresetResponse = z.infer<
|
||||
typeof AddResultFilterPresetResponseSchema
|
||||
>;
|
||||
|
||||
export const RemoveResultFilterPresetPathParamsSchema = z.object({
|
||||
presetId: IdSchema,
|
||||
});
|
||||
export type RemoveResultFilterPresetPathParams = z.infer<
|
||||
typeof RemoveResultFilterPresetPathParamsSchema
|
||||
>;
|
||||
|
||||
export const GetTagsResponseSchema = responseWithData(z.array(UserTagSchema));
|
||||
export type GetTagsResponse = z.infer<typeof GetTagsResponseSchema>;
|
||||
|
||||
export const AddTagRequestSchema = z.object({
|
||||
tagName: TagNameSchema,
|
||||
});
|
||||
export type AddTagRequest = z.infer<typeof AddTagRequestSchema>;
|
||||
|
||||
export const AddTagResponseSchema = responseWithData(UserTagSchema);
|
||||
export type AddTagResponse = z.infer<typeof AddTagResponseSchema>;
|
||||
|
||||
export const EditTagRequestSchema = z.object({
|
||||
tagId: IdSchema,
|
||||
newName: TagNameSchema,
|
||||
});
|
||||
export type EditTagRequest = z.infer<typeof EditTagRequestSchema>;
|
||||
|
||||
export const TagIdPathParamsSchema = z.object({
|
||||
tagId: IdSchema,
|
||||
});
|
||||
export type TagIdPathParams = z.infer<typeof TagIdPathParamsSchema>;
|
||||
|
||||
export const GetCustomThemesResponseSchema = responseWithData(
|
||||
z.array(CustomThemeSchema),
|
||||
);
|
||||
export type GetCustomThemesResponse = z.infer<
|
||||
typeof GetCustomThemesResponseSchema
|
||||
>;
|
||||
|
||||
export const AddCustomThemeRequestSchema = z.object({
|
||||
name: CustomThemeNameSchema,
|
||||
colors: CustomThemeColorsSchema,
|
||||
});
|
||||
export type AddCustomThemeRequest = z.infer<typeof AddCustomThemeRequestSchema>;
|
||||
|
||||
export const AddCustomThemeResponseSchema = responseWithData(
|
||||
CustomThemeSchema.pick({ _id: true, name: true }),
|
||||
);
|
||||
export type AddCustomThemeResponse = z.infer<
|
||||
typeof AddCustomThemeResponseSchema
|
||||
>;
|
||||
|
||||
export const DeleteCustomThemeRequestSchema = z.object({
|
||||
themeId: IdSchema,
|
||||
});
|
||||
export type DeleteCustomThemeRequest = z.infer<
|
||||
typeof DeleteCustomThemeRequestSchema
|
||||
>;
|
||||
|
||||
export const EditCustomThemeRequstSchema = z.object({
|
||||
themeId: IdSchema,
|
||||
theme: CustomThemeSchema.pick({ name: true, colors: true }),
|
||||
});
|
||||
export type EditCustomThemeRequst = z.infer<typeof EditCustomThemeRequstSchema>;
|
||||
|
||||
export const GetDiscordOauthLinkResponseSchema = responseWithData(
|
||||
z.object({
|
||||
url: z.string().url(),
|
||||
}),
|
||||
);
|
||||
export type GetDiscordOauthLinkResponse = z.infer<
|
||||
typeof GetDiscordOauthLinkResponseSchema
|
||||
>;
|
||||
|
||||
export const LinkDiscordRequestSchema = z.object({
|
||||
tokenType: z.string(),
|
||||
accessToken: z.string(),
|
||||
state: z.string().length(20),
|
||||
});
|
||||
export type LinkDiscordRequest = z.infer<typeof LinkDiscordRequestSchema>;
|
||||
|
||||
export const LinkDiscordResponseSchema = responseWithData(
|
||||
UserSchema.pick({ discordId: true, discordAvatar: true }),
|
||||
);
|
||||
export type LinkDiscordResponse = z.infer<typeof LinkDiscordResponseSchema>;
|
||||
|
||||
export const GetStatsResponseSchema = responseWithData(
|
||||
UserSchema.pick({
|
||||
completedTests: true,
|
||||
startedTests: true,
|
||||
timeTyping: true,
|
||||
}),
|
||||
);
|
||||
export type GetStatsResponse = z.infer<typeof GetStatsResponseSchema>;
|
||||
|
||||
export const SetStreakHourOffsetRequestSchema = z.object({
|
||||
hourOffset: StreakHourOffsetSchema,
|
||||
});
|
||||
export type SetStreakHourOffsetRequest = z.infer<
|
||||
typeof SetStreakHourOffsetRequestSchema
|
||||
>;
|
||||
|
||||
export const GetFavoriteQuotesResponseSchema =
|
||||
responseWithData(FavoriteQuotesSchema);
|
||||
export type GetFavoriteQuotesResponse = z.infer<
|
||||
typeof GetFavoriteQuotesResponseSchema
|
||||
>;
|
||||
|
||||
export const AddFavoriteQuoteRequestSchema = z.object({
|
||||
language: LanguageSchema,
|
||||
quoteId: StringNumberSchema,
|
||||
});
|
||||
export type AddFavoriteQuoteRequest = z.infer<
|
||||
typeof AddFavoriteQuoteRequestSchema
|
||||
>;
|
||||
|
||||
export const RemoveFavoriteQuoteRequestSchema = z.object({
|
||||
language: LanguageSchema,
|
||||
quoteId: StringNumberSchema,
|
||||
});
|
||||
export type RemoveFavoriteQuoteRequest = z.infer<
|
||||
typeof RemoveFavoriteQuoteRequestSchema
|
||||
>;
|
||||
|
||||
export const GetProfilePathParamsSchema = z.object({
|
||||
uidOrName: z.string(),
|
||||
});
|
||||
export type GetProfilePathParams = z.infer<typeof GetProfilePathParamsSchema>;
|
||||
|
||||
//TODO test?!
|
||||
export const GetProfileQuerySchema = z.object({
|
||||
isUid: z
|
||||
.string()
|
||||
.length(0)
|
||||
.transform((it) => it === "")
|
||||
.or(z.boolean())
|
||||
.default(false),
|
||||
});
|
||||
export type GetProfileQuery = z.infer<typeof GetProfileQuerySchema>;
|
||||
|
||||
export const GetProfileResponseSchema = responseWithData(UserProfileSchema);
|
||||
export type GetProfileResponse = z.infer<typeof GetProfileResponseSchema>;
|
||||
|
||||
export const UpdateUserProfileRequestSchema = UserProfileDetailsSchema.extend({
|
||||
selectedBadgeId: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.or(z.literal(-1).describe("no badge selected")), //TODO remove the -1, use optional?
|
||||
});
|
||||
export type UpdateUserProfileRequest = z.infer<
|
||||
typeof UpdateUserProfileRequestSchema
|
||||
>;
|
||||
|
||||
export const UpdateUserProfileResponseSchema = responseWithData(
|
||||
UserProfileDetailsSchema,
|
||||
);
|
||||
export type UpdateUserProfileResponse = z.infer<
|
||||
typeof UpdateUserProfileResponseSchema
|
||||
>;
|
||||
|
||||
export const GetUserInboxResponseSchema = responseWithData(
|
||||
z.object({
|
||||
inbox: z.array(MonkeyMailSchema),
|
||||
maxMail: z.number().int(),
|
||||
}),
|
||||
);
|
||||
export type GetUserInboxResponse = z.infer<typeof GetUserInboxResponseSchema>;
|
||||
|
||||
export const UpdateUserInboxRequestSchema = z.object({
|
||||
mailIdsToDelete: z.array(z.string().uuid()).min(1).optional(),
|
||||
mailIdsToMarkRead: z.array(z.string().uuid()).min(1).optional(),
|
||||
});
|
||||
export type UpdateUserInboxRequest = z.infer<
|
||||
typeof UpdateUserInboxRequestSchema
|
||||
>;
|
||||
|
||||
export const ReportUserRequestSchema = z.object({
|
||||
uid: z.string(),
|
||||
reason: ReportUserReasonSchema,
|
||||
comment: z
|
||||
.string()
|
||||
.regex(/^([.]|[^/<>])+$/)
|
||||
.max(250)
|
||||
.optional()
|
||||
.or(z.string().length(0)),
|
||||
captcha: z.string(), //we don't generate the captcha so there should be no validation
|
||||
});
|
||||
export type ReportUserRequest = z.infer<typeof ReportUserRequestSchema>;
|
||||
|
||||
export const ForgotPasswordEmailRequestSchema = z.object({
|
||||
captcha: z.string(),
|
||||
email: UserEmailSchema,
|
||||
});
|
||||
export type ForgotPasswordEmailRequest = z.infer<
|
||||
typeof ForgotPasswordEmailRequestSchema
|
||||
>;
|
||||
|
||||
export const GetTestActivityResponseSchema = responseWithNullableData(
|
||||
CountByYearAndDaySchema,
|
||||
);
|
||||
export type GetTestActivityResponse = z.infer<
|
||||
typeof GetTestActivityResponseSchema
|
||||
>;
|
||||
|
||||
export const GetCurrentTestActivityResponseSchema =
|
||||
responseWithNullableData(TestActivitySchema);
|
||||
export type GetCurrentTestActivityResponse = z.infer<
|
||||
typeof GetCurrentTestActivityResponseSchema
|
||||
>;
|
||||
|
||||
export const GetStreakResponseSchema =
|
||||
responseWithNullableData(UserStreakSchema);
|
||||
export type GetStreakResponse = z.infer<typeof GetStreakResponseSchema>;
|
||||
|
||||
export const GetFriendsResponseSchema = responseWithData(z.array(FriendSchema));
|
||||
export type GetFriendsResponse = z.infer<typeof GetFriendsResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
|
||||
export const usersContract = c.router(
|
||||
{
|
||||
get: {
|
||||
summary: "get user",
|
||||
description: "Get a user's data.",
|
||||
method: "GET",
|
||||
path: "",
|
||||
responses: {
|
||||
200: GetUserResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
create: {
|
||||
summary: "create user",
|
||||
description: "Creates a new user",
|
||||
method: "POST",
|
||||
path: "/signup",
|
||||
body: CreateUserRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userSignup",
|
||||
requireConfiguration: {
|
||||
path: "users.signUp",
|
||||
invalidMessage: "Sign up is temporarily disabled",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getNameAvailability: {
|
||||
summary: "check name",
|
||||
description: "Checks to see if a username is available",
|
||||
method: "GET",
|
||||
path: "/checkName/:name",
|
||||
pathParams: CheckNamePathParametersSchema.strict(),
|
||||
responses: {
|
||||
200: CheckNameResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
rateLimit: "userCheckName",
|
||||
}),
|
||||
},
|
||||
delete: {
|
||||
summary: "delete user",
|
||||
description: "Deletes a user's account",
|
||||
method: "DELETE",
|
||||
path: "",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userDelete",
|
||||
}),
|
||||
},
|
||||
reset: {
|
||||
summary: "reset user",
|
||||
description: "Completely resets a user's account to a blank state",
|
||||
method: "PATCH",
|
||||
path: "/reset",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userReset",
|
||||
}),
|
||||
},
|
||||
updateName: {
|
||||
summary: "update username",
|
||||
description: "Updates a user's name",
|
||||
method: "PATCH",
|
||||
path: "/name",
|
||||
body: UpdateUserNameRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userUpdateName",
|
||||
}),
|
||||
},
|
||||
updateLeaderboardMemory: {
|
||||
summary: "update lbMemory",
|
||||
description: "Updates a user's cached leaderboard state",
|
||||
method: "PATCH",
|
||||
path: "/leaderboardMemory",
|
||||
body: UpdateLeaderboardMemoryRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userUpdateLBMemory",
|
||||
}),
|
||||
},
|
||||
updateEmail: {
|
||||
summary: "update email",
|
||||
description: "Updates a user's email",
|
||||
method: "PATCH",
|
||||
path: "/email",
|
||||
body: UpdateEmailRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userUpdateEmail",
|
||||
}),
|
||||
},
|
||||
updatePassword: {
|
||||
summary: "update password",
|
||||
description: "Updates a user's password",
|
||||
method: "PATCH",
|
||||
path: "/password",
|
||||
body: UpdatePasswordRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userUpdateEmail",
|
||||
}),
|
||||
},
|
||||
getPersonalBests: {
|
||||
summary: "get personal bests",
|
||||
description: "Get user's personal bests",
|
||||
method: "GET",
|
||||
path: "/personalBests",
|
||||
query: GetPersonalBestsQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetPersonalBestsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
deletePersonalBests: {
|
||||
summary: "delete personal bests",
|
||||
description: "Deletes a user's personal bests",
|
||||
method: "DELETE",
|
||||
path: "/personalBests",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userClearPB",
|
||||
}),
|
||||
},
|
||||
optOutOfLeaderboards: {
|
||||
summary: "leaderboards opt out",
|
||||
description: "Opt out of the leaderboards",
|
||||
method: "POST",
|
||||
path: "/optOutOfLeaderboards",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true },
|
||||
rateLimit: "userOptOutOfLeaderboards",
|
||||
}),
|
||||
},
|
||||
addResultFilterPreset: {
|
||||
summary: "add result filter preset",
|
||||
description: "Add a result filter preset",
|
||||
method: "POST",
|
||||
path: "/resultFilterPresets",
|
||||
body: AddResultFilterPresetRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddResultFilterPresetResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomFilterAdd",
|
||||
requireConfiguration: {
|
||||
path: "results.filterPresets.enabled",
|
||||
invalidMessage:
|
||||
"Result filter presets are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
removeResultFilterPreset: {
|
||||
summary: "remove result filter preset",
|
||||
description: "Remove a result filter preset",
|
||||
method: "DELETE",
|
||||
path: "/resultFilterPresets/:presetId",
|
||||
pathParams: RemoveResultFilterPresetPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomFilterRemove",
|
||||
requireConfiguration: {
|
||||
path: "results.filterPresets.enabled",
|
||||
invalidMessage:
|
||||
"Result filter presets are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getTags: {
|
||||
summary: "get tags",
|
||||
description: "Get the users tags",
|
||||
method: "GET",
|
||||
path: "/tags",
|
||||
responses: {
|
||||
200: GetTagsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
rateLimit: "userTagsGet",
|
||||
}),
|
||||
},
|
||||
createTag: {
|
||||
summary: "add tag",
|
||||
description: "Add a tag for the current user",
|
||||
method: "POST",
|
||||
path: "/tags",
|
||||
body: AddTagRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddTagResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsAdd",
|
||||
}),
|
||||
},
|
||||
editTag: {
|
||||
summary: "edit tag",
|
||||
description: "Edit a tag",
|
||||
method: "PATCH",
|
||||
path: "/tags",
|
||||
body: EditTagRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsEdit",
|
||||
}),
|
||||
},
|
||||
deleteTag: {
|
||||
summary: "delete tag",
|
||||
description: "Delete a tag",
|
||||
method: "DELETE",
|
||||
path: "/tags/:tagId",
|
||||
pathParams: TagIdPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsRemove",
|
||||
}),
|
||||
},
|
||||
deleteTagPersonalBest: {
|
||||
summary: "delete tag PBs",
|
||||
description: "Delete personal bests of a tag",
|
||||
method: "DELETE",
|
||||
path: "/tags/:tagId/personalBest",
|
||||
pathParams: TagIdPathParamsSchema.strict(),
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTagsClearPB",
|
||||
}),
|
||||
},
|
||||
getCustomThemes: {
|
||||
summary: "get custom themes",
|
||||
description: "Get custom themes for the current user",
|
||||
method: "GET",
|
||||
path: "/customThemes",
|
||||
responses: {
|
||||
200: GetCustomThemesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeGet",
|
||||
}),
|
||||
},
|
||||
addCustomTheme: {
|
||||
summary: "add custom themes",
|
||||
description: "Add a custom theme for the current user",
|
||||
method: "POST",
|
||||
path: "/customThemes",
|
||||
body: AddCustomThemeRequestSchema.strict(),
|
||||
responses: {
|
||||
200: AddCustomThemeResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeAdd",
|
||||
}),
|
||||
},
|
||||
deleteCustomTheme: {
|
||||
summary: "delete custom themes",
|
||||
description: "Delete a custom theme",
|
||||
method: "DELETE",
|
||||
path: "/customThemes",
|
||||
body: DeleteCustomThemeRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeRemove",
|
||||
}),
|
||||
},
|
||||
editCustomTheme: {
|
||||
summary: "edit custom themes",
|
||||
description: "Edit a custom theme",
|
||||
method: "PATCH",
|
||||
path: "/customThemes",
|
||||
body: EditCustomThemeRequstSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userCustomThemeEdit",
|
||||
}),
|
||||
},
|
||||
getDiscordOAuth: {
|
||||
summary: "discord oauth",
|
||||
description: "Start OAuth authentication with discord",
|
||||
method: "GET",
|
||||
path: "/discord/oauth",
|
||||
responses: {
|
||||
200: GetDiscordOauthLinkResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordLink",
|
||||
requireConfiguration: {
|
||||
path: "users.discordIntegration.enabled",
|
||||
invalidMessage: "Discord integration is not available at this time",
|
||||
},
|
||||
}),
|
||||
},
|
||||
linkDiscord: {
|
||||
summary: "link with discord",
|
||||
description: "Links a user's account with a discord account",
|
||||
method: "POST",
|
||||
path: "/discord/link",
|
||||
body: LinkDiscordRequestSchema.strict(),
|
||||
responses: {
|
||||
200: LinkDiscordResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordLink",
|
||||
requireConfiguration: {
|
||||
path: "users.discordIntegration.enabled",
|
||||
invalidMessage: "Discord integration is not available at this time",
|
||||
},
|
||||
}),
|
||||
},
|
||||
unlinkDiscord: {
|
||||
summary: "unlink discord",
|
||||
description: "Unlinks a user's account with a discord account",
|
||||
method: "POST",
|
||||
path: "/discord/unlink",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userDiscordUnlink",
|
||||
}),
|
||||
},
|
||||
getStats: {
|
||||
summary: "get stats",
|
||||
description: "Gets a user's typing stats data",
|
||||
method: "GET",
|
||||
path: "/stats",
|
||||
responses: {
|
||||
200: GetStatsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
rateLimit: "userGet",
|
||||
}),
|
||||
},
|
||||
setStreakHourOffset: {
|
||||
summary: "set streak hour offset",
|
||||
description: "Sets a user's streak hour offset",
|
||||
method: "POST",
|
||||
path: "/setStreakHourOffset",
|
||||
body: SetStreakHourOffsetRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "setStreakHourOffset",
|
||||
}),
|
||||
},
|
||||
getFavoriteQuotes: {
|
||||
summary: "get favorite quotes",
|
||||
description: "Gets a user's favorite quotes",
|
||||
method: "GET",
|
||||
path: "/favoriteQuotes",
|
||||
responses: {
|
||||
200: GetFavoriteQuotesResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoriteGet",
|
||||
}),
|
||||
},
|
||||
addQuoteToFavorites: {
|
||||
summary: "add favorite quotes",
|
||||
description: "Add a quote to the user's favorite quotes",
|
||||
method: "POST",
|
||||
path: "/favoriteQuotes",
|
||||
body: AddFavoriteQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoritePost",
|
||||
}),
|
||||
},
|
||||
removeQuoteFromFavorites: {
|
||||
summary: "remove favorite quotes",
|
||||
description: "Remove a quote to the user's favorite quotes",
|
||||
method: "DELETE",
|
||||
path: "/favoriteQuotes",
|
||||
body: RemoveFavoriteQuoteRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteFavoriteDelete",
|
||||
}),
|
||||
},
|
||||
getProfile: {
|
||||
summary: "get profile",
|
||||
description: "Gets a user's profile",
|
||||
method: "GET",
|
||||
path: "/:uidOrName/profile",
|
||||
pathParams: GetProfilePathParamsSchema.strict(),
|
||||
query: GetProfileQuerySchema.strict(),
|
||||
responses: {
|
||||
200: GetProfileResponseSchema,
|
||||
404: MonkeyClientError.describe("User not found"),
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
rateLimit: "userProfileGet",
|
||||
requireConfiguration: {
|
||||
path: "users.profiles.enabled",
|
||||
invalidMessage: "Profiles are not available at this time",
|
||||
},
|
||||
}),
|
||||
},
|
||||
updateProfile: {
|
||||
summary: "update profile",
|
||||
description: "Update a user's profile",
|
||||
method: "PATCH",
|
||||
path: "/profile",
|
||||
body: UpdateUserProfileRequestSchema.strict(),
|
||||
responses: {
|
||||
200: UpdateUserProfileResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userProfileUpdate",
|
||||
requireConfiguration: {
|
||||
path: "users.profiles.enabled",
|
||||
invalidMessage: "Profiles are not available at this time",
|
||||
},
|
||||
}),
|
||||
},
|
||||
getInbox: {
|
||||
summary: "get inbox",
|
||||
description: "Gets the user's inbox",
|
||||
method: "GET",
|
||||
path: "/inbox",
|
||||
responses: {
|
||||
200: GetUserInboxResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userMailGet",
|
||||
requireConfiguration: {
|
||||
path: "users.inbox.enabled",
|
||||
invalidMessage: "Your inbox is not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
updateInbox: {
|
||||
summary: "update inbox",
|
||||
description: "Updates the user's inbox",
|
||||
method: "PATCH",
|
||||
body: UpdateUserInboxRequestSchema.strict(),
|
||||
path: "/inbox",
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userMailUpdate",
|
||||
requireConfiguration: {
|
||||
path: "users.inbox.enabled",
|
||||
invalidMessage: "Your inbox is not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
report: {
|
||||
summary: "report user",
|
||||
description: "Report a user",
|
||||
method: "POST",
|
||||
path: "/report",
|
||||
body: ReportUserRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "quoteReportSubmit",
|
||||
requirePermission: "canReport",
|
||||
requireConfiguration: {
|
||||
path: "quotes.reporting.enabled",
|
||||
invalidMessage: "User reporting is unavailable.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
verificationEmail: {
|
||||
summary: "send verification email",
|
||||
description: "Send a verification email",
|
||||
method: "GET",
|
||||
path: "/verificationEmail",
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { noCache: true },
|
||||
rateLimit: "userRequestVerificationEmail",
|
||||
}),
|
||||
},
|
||||
forgotPasswordEmail: {
|
||||
summary: "send forgot password email",
|
||||
description: "Send a forgot password email",
|
||||
method: "POST",
|
||||
path: "/forgotPasswordEmail",
|
||||
body: ForgotPasswordEmailRequestSchema.strict(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { isPublic: true },
|
||||
rateLimit: "userForgotPasswordEmail",
|
||||
}),
|
||||
},
|
||||
revokeAllTokens: {
|
||||
summary: "revoke all tokens",
|
||||
description: "Revoke all tokens for the current user.",
|
||||
method: "POST",
|
||||
path: "/revokeAllTokens",
|
||||
body: c.noBody(),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { requireFreshToken: true, noCache: true },
|
||||
rateLimit: "userRevokeAllTokens",
|
||||
}),
|
||||
},
|
||||
getTestActivity: {
|
||||
summary: "get test activity",
|
||||
description: "Get user's test activity",
|
||||
method: "GET",
|
||||
path: "/testActivity",
|
||||
responses: {
|
||||
200: GetTestActivityResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userTestActivity",
|
||||
}),
|
||||
},
|
||||
getCurrentTestActivity: {
|
||||
summary: "get current test activity",
|
||||
description:
|
||||
"Get test activity for the last up to 372 days for the current user ",
|
||||
method: "GET",
|
||||
path: "/currentTestActivity",
|
||||
responses: {
|
||||
200: GetCurrentTestActivityResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
rateLimit: "userCurrentTestActivity",
|
||||
}),
|
||||
},
|
||||
getStreak: {
|
||||
summary: "get streak",
|
||||
description: "Get user's streak data",
|
||||
method: "GET",
|
||||
path: "/streak",
|
||||
responses: {
|
||||
200: GetStreakResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
authenticationOptions: { acceptApeKeys: true },
|
||||
rateLimit: "userStreak",
|
||||
}),
|
||||
},
|
||||
getFriends: {
|
||||
summary: "get friends",
|
||||
description: "get friends list",
|
||||
method: "GET",
|
||||
path: "/friends",
|
||||
responses: {
|
||||
200: GetFriendsResponseSchema,
|
||||
},
|
||||
metadata: meta({
|
||||
rateLimit: "userFriendGet",
|
||||
requireConfiguration: {
|
||||
path: "connections.enabled",
|
||||
invalidMessage: "Connections are not available at this time.",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/users",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "users",
|
||||
}),
|
||||
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
148
packages/contracts/src/util/api.ts
Normal file
148
packages/contracts/src/util/api.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { z, ZodSchema } from "zod";
|
||||
import { RateLimitIds, RateLimiterId } from "../rate-limit";
|
||||
import { RequireConfiguration } from "../require-configuration";
|
||||
|
||||
export type OpenApiTag =
|
||||
| "configs"
|
||||
| "presets"
|
||||
| "ape-keys"
|
||||
| "admin"
|
||||
| "psas"
|
||||
| "public"
|
||||
| "leaderboards"
|
||||
| "results"
|
||||
| "configuration"
|
||||
| "development"
|
||||
| "users"
|
||||
| "quotes"
|
||||
| "webhooks"
|
||||
| "connections";
|
||||
|
||||
export type PermissionId =
|
||||
| "quoteMod"
|
||||
| "canReport"
|
||||
| "canManageApeKeys"
|
||||
| "admin";
|
||||
|
||||
export type EndpointMetadata = {
|
||||
/** Authentication options, by default a bearer token is required. */
|
||||
authenticationOptions?: RequestAuthenticationOptions;
|
||||
|
||||
openApiTags?: OpenApiTag | OpenApiTag[];
|
||||
|
||||
/** RateLimitId or RateLimitIds.
|
||||
* Only specifying RateLimiterId will use a default limiter with 30 requests/minute for ApeKey requests.
|
||||
*/
|
||||
rateLimit?: RateLimiterId | RateLimitIds;
|
||||
|
||||
/** Role/Rples needed to access the endpoint*/
|
||||
requirePermission?: PermissionId | PermissionId[];
|
||||
|
||||
/** Endpoint is only available if configuration allows it */
|
||||
requireConfiguration?: RequireConfiguration | RequireConfiguration[];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metadata Ensure the type of metadata is `EndpointMetadata`.
|
||||
* Ts-rest does not allow to specify the type of `metadata`.
|
||||
* @returns
|
||||
*/
|
||||
export function meta(metadata: EndpointMetadata): EndpointMetadata {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
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;
|
||||
/** Allow unauthenticated requests on dev */
|
||||
isPublicOnDev?: boolean;
|
||||
/** Endpoint is a webhook only to be called by Github */
|
||||
isGithubWebhook?: 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()),
|
||||
});
|
||||
export type MonkeyValidationError = z.infer<typeof MonkeyValidationErrorSchema>;
|
||||
|
||||
export const MonkeyClientError = MonkeyResponseSchema;
|
||||
export type MonkeyClientErrorType = z.infer<typeof MonkeyClientError>;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export const CommonResponses = {
|
||||
400: MonkeyClientError.describe("Generic client error"),
|
||||
401: MonkeyClientError.describe(
|
||||
"Authentication required but not provided or invalid",
|
||||
),
|
||||
403: MonkeyClientError.describe("Operation not permitted"),
|
||||
422: MonkeyValidationErrorSchema.describe("Request validation failed"),
|
||||
429: MonkeyClientError.describe("Rate limit exceeded"),
|
||||
470: MonkeyClientError.describe("Invalid ApeKey"),
|
||||
471: MonkeyClientError.describe("ApeKey is inactive"),
|
||||
472: MonkeyClientError.describe("ApeKey is malformed"),
|
||||
479: MonkeyClientError.describe("ApeKey rate limit exceeded"),
|
||||
500: MonkeyServerError.describe("Generic server error"),
|
||||
503: MonkeyServerError.describe(
|
||||
"Endpoint disabled or server is under maintenance",
|
||||
),
|
||||
};
|
||||
|
||||
export type CommonResponsesType =
|
||||
| {
|
||||
status: 400 | 401 | 403 | 429 | 470 | 471 | 472 | 479;
|
||||
body: MonkeyClientErrorType;
|
||||
}
|
||||
| {
|
||||
status: 422;
|
||||
body: MonkeyValidationError;
|
||||
}
|
||||
| {
|
||||
status: 500 | 503;
|
||||
body: MonkeyServerErrorType;
|
||||
};
|
||||
60
packages/contracts/src/webhooks.ts
Normal file
60
packages/contracts/src/webhooks.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { initContract } from "@ts-rest/core";
|
||||
import { z } from "zod";
|
||||
import { PSASchema } from "@monkeytype/schemas/psas";
|
||||
|
||||
import {
|
||||
CommonResponses,
|
||||
meta,
|
||||
MonkeyResponseSchema,
|
||||
responseWithData,
|
||||
} from "./util/api";
|
||||
|
||||
/**
|
||||
*Schema: https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=published#release
|
||||
We only specify the values we read and don't validate any other values.
|
||||
*/
|
||||
export const PostGithubReleaseRequestSchema = z.object({
|
||||
action: z.literal("published").or(z.string()),
|
||||
release: z
|
||||
.object({
|
||||
id: z.string().or(z.number().transform(String)), //we use string, github defines this as a number.
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type PostGithubReleaseRequest = z.infer<
|
||||
typeof PostGithubReleaseRequestSchema
|
||||
>;
|
||||
|
||||
export const GetPsaResponseSchema = responseWithData(z.array(PSASchema));
|
||||
export type GetPsaResponse = z.infer<typeof GetPsaResponseSchema>;
|
||||
|
||||
const c = initContract();
|
||||
export const webhooksContract = c.router(
|
||||
{
|
||||
postGithubRelease: {
|
||||
summary: "Github release",
|
||||
description: "Announce github release.",
|
||||
method: "POST",
|
||||
path: "/githubRelease",
|
||||
body: PostGithubReleaseRequestSchema, //don't use strict
|
||||
headers: z.object({
|
||||
"x-hub-signature-256": z.string(),
|
||||
}),
|
||||
responses: {
|
||||
200: MonkeyResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pathPrefix: "/webhooks",
|
||||
strictStatusCodes: true,
|
||||
metadata: meta({
|
||||
openApiTags: "webhooks",
|
||||
authenticationOptions: {
|
||||
isGithubWebhook: true,
|
||||
},
|
||||
rateLimit: "webhookLimit",
|
||||
}),
|
||||
commonResponses: CommonResponses,
|
||||
},
|
||||
);
|
||||
10
packages/contracts/tsconfig.json
Normal file
10
packages/contracts/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@monkeytype/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"target": "ES6"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
packages/contracts/tsup.config.js
Normal file
3
packages/contracts/tsup.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { extendConfig } from "@monkeytype/tsup-config";
|
||||
|
||||
export default extendConfig();
|
||||
10
packages/contracts/vitest.config.ts
Normal file
10
packages/contracts/vitest.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
coverage: {
|
||||
include: ["**/*.ts"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user