Add public api docs (#2656) by bruce

* Add public documentation

* typo

* added hard limit on the leaderboards

* ignoring worker when compiling

* added leaderboard routes

* leaderboards accept apekeys

* Fix docs

* Fix

* Specify parse base

* Add ape rate limiter

* added documentation link

* updated message

Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
Bruce Berrios 2022-03-09 07:02:37 -05:00 committed by GitHub
parent 4ebfd98c2c
commit 2dba957adc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 392 additions and 47 deletions

View file

@ -7,12 +7,14 @@ class LeaderboardsController {
const { language, mode, mode2, skip, limit = 50 } = req.query;
const { uid } = req.ctx.decodedToken;
const queryLimit = Math.min(parseInt(limit as string, 10), 50);
const leaderboard = await LeaderboardsDAO.get(
mode,
mode2,
language,
parseInt(skip as string),
parseInt(limit as string)
parseInt(skip as string, 10),
queryLimit
);
const normalizedLeaderboard = _.map(leaderboard, (entry) => {

View file

@ -1,17 +1,16 @@
import _ from "lodash";
import psas from "./psas";
import users from "./users";
import quotes from "./quotes";
import configs from "./configs";
import results from "./results";
import presets from "./presets";
import psas from "./psas";
import leaderboards from "./leaderboards";
import quotes from "./quotes";
import apeKeys from "./ape-keys";
import leaderboards from "./leaderboards";
import addSwaggerMiddlewares from "./swagger";
import { asyncHandler } from "../../middlewares/api-utils";
import { MonkeyResponse } from "../../utils/monkey-response";
import { Application, NextFunction, Response, Router } from "express";
import swStats from "swagger-stats";
import SwaggerSpec from "../../swagger.json";
const pathOverride = process.env.API_PATH_OVERRIDE;
const BASE_ROUTE = pathOverride ? `/${pathOverride}` : "";
@ -31,34 +30,7 @@ const API_ROUTE_MAP = {
function addApiRoutes(app: Application): void {
let requestsProcessed = 0;
app.use(
swStats.getMiddleware({
name: "Monkeytype API",
// hostname: process.env.MODE === "dev" ? "localhost": process.env.STATS_HOSTNAME,
// ip: process.env.MODE === "dev" ? "127.0.0.1": process.env.STATS_IP,
uriPath: "/stats",
authentication: process.env.MODE !== "dev",
apdexThreshold: 100,
swaggerSpec: SwaggerSpec,
onAuthenticate: (_req, username, password) => {
return (
username === process.env.STATS_USERNAME &&
password === process.env.STATS_PASSWORD
);
},
onResponseFinish: (_req, res, rrr) => {
//@ts-ignore ignored because monkeyMessage doesnt exist on the type
rrr.http.response.message = res.monkeyMessage;
if (process.env.MODE === "dev") {
return;
}
const authHeader = rrr.http.request.headers?.authorization ?? "None";
const authType = authHeader.split(" ");
_.set(rrr.http.request, "headers.authorization", authType[0]);
_.set(rrr.http.request, "headers['x-forwarded-for']", "");
},
})
);
addSwaggerMiddlewares(app);
app.use(
(req: MonkeyTypes.Request, res: Response, next: NextFunction): void => {
@ -85,7 +57,7 @@ function addApiRoutes(app: Application): void {
})
);
app.get("/psa", (req, res) => {
app.get("/psa", (_req, res) => {
res.json([
{
message:

View file

@ -1,23 +1,24 @@
import joi from "joi";
import { Router } from "express";
import * as RateLimit from "../../middlewares/rate-limit";
import apeRateLimit from "../../middlewares/ape-rate-limit";
import { authenticateRequest } from "../../middlewares/auth";
import LeaderboardsController from "../controllers/leaderboards";
import * as RateLimit from "../../middlewares/rate-limit";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import { Router } from "express";
const router = Router();
router.get(
"/",
RateLimit.leaderboardsGet,
authenticateRequest({ isPublic: true }),
authenticateRequest({ isPublic: true, acceptApeKeys: true }),
validateRequest({
query: {
language: joi.string().required(),
mode: joi.string().required(),
mode2: joi.string().required(),
skip: joi.number().min(0).required(),
limit: joi.number(),
skip: joi.number().min(0),
limit: joi.number().min(0).max(50),
},
validationErrorMessage: "Missing parameters",
}),
@ -27,7 +28,8 @@ router.get(
router.get(
"/rank",
RateLimit.leaderboardsGet,
authenticateRequest(),
authenticateRequest({ acceptApeKeys: true }),
apeRateLimit,
validateRequest({
query: {
language: joi.string().required(),

View file

@ -0,0 +1,48 @@
import _ from "lodash";
import { Application } from "express";
import swaggerStats from "swagger-stats";
import swaggerUi from "swagger-ui-express";
import publicSwaggerSpec from "../../documentation/public-swagger.json";
import internalSwaggerSpec from "../../documentation/internal-swagger.json";
const SWAGGER_UI_OPTIONS = {
customCss: ".swagger-ui .topbar { display: none }",
customSiteTitle: "Monkeytype API Documentation",
};
function addSwaggerMiddlewares(app: Application): void {
app.use(
swaggerStats.getMiddleware({
name: "Monkeytype API",
uriPath: "/stats",
authentication: process.env.MODE !== "dev",
apdexThreshold: 100,
swaggerSpec: internalSwaggerSpec,
onAuthenticate: (_req, username, password) => {
return (
username === process.env.STATS_USERNAME &&
password === process.env.STATS_PASSWORD
);
},
onResponseFinish: (_req, res, rrr) => {
//@ts-ignore ignored because monkeyMessage doesnt exist in response
rrr.http.response.message = res.monkeyMessage;
if (process.env.MODE === "dev") {
return;
}
const authHeader = rrr.http.request.headers?.authorization ?? "None";
const authType = authHeader.split(" ");
_.set(rrr.http.request, "headers.authorization", authType[0]);
_.set(rrr.http.request, "headers['x-forwarded-for']", "");
},
})
);
app.use(
"/documentation",
swaggerUi.serve,
swaggerUi.setup(publicSwaggerSpec, SWAGGER_UI_OPTIONS)
);
}
export default addSwaggerMiddlewares;

View file

@ -4,7 +4,7 @@ import { Router } from "express";
import UserController from "../controllers/user";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
import ApeRateLimit from "../../middlewares/ape-rate-limit";
import apeRateLimit from "../../middlewares/ape-rate-limit";
import { isUsernameValid } from "../../utils/validation";
const router = Router();
@ -207,7 +207,7 @@ router.get(
authenticateRequest({
acceptApeKeys: true,
}),
ApeRateLimit,
apeRateLimit,
validateRequest({
query: {
mode: joi.string().required(),

View file

@ -0,0 +1,257 @@
{
"swagger": "2.0",
"info": {
"description": "Documentation for the public endpoints provided by the Monketype API server.\n\nNote that authentication is performed with the Authorization HTTP header in the format `Authorization: ApeKey YOUR_APE_KEY` ",
"version": "1.0.0",
"title": "Monkeytype API",
"termsOfService": "https://monkeytype.com/terms-of-service",
"contact": {
"name": "Developer",
"email": "jack@monkeytype.com"
}
},
"basePath": "/",
"consumes": ["application/json"],
"produces": ["application/json"],
"tags": [
{
"name": "users",
"description": "User data and related operations"
},
{
"name": "leaderboards",
"description": "Leaderboard data and related operations"
}
],
"paths": {
"/users/personalBests": {
"get": {
"tags": ["users"],
"summary": "Gets a user's personal best data",
"parameters": [
{
"name": "mode",
"in": "query",
"description": "The primary mode (i.e., time)",
"required": true,
"type": "string"
},
{
"name": "mode2",
"in": "query",
"description": "The secondary mode (i.e., 60)",
"required": false,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/PersonalBest"
}
}
}
}
},
"/leaderboards": {
"get": {
"tags": ["leaderboards"],
"summary": "Gets global leaderboard data",
"parameters": [
{
"name": "language",
"in": "query",
"description": "The leaderboard's language (i.e., english)",
"required": true,
"type": "string"
},
{
"name": "mode",
"in": "query",
"description": "The primary mode (i.e., time)",
"required": true,
"type": "string"
},
{
"name": "mode2",
"in": "query",
"description": "The secondary mode (i.e., 60)",
"required": true,
"type": "string"
},
{
"name": "skip",
"in": "query",
"description": "How many leaderboard entries to skip",
"required": false,
"type": "number",
"minimum": 0
},
{
"name": "limit",
"in": "query",
"description": "How many leaderboard entries to request",
"required": false,
"type": "number",
"minimum": 0,
"maximum": 50
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/LeaderboardEntry"
}
}
}
}
},
"/leaderboards/rank": {
"get": {
"tags": ["leaderboards"],
"summary": "Gets your qualifying rank from a leaderboard",
"parameters": [
{
"name": "language",
"in": "query",
"description": "The leaderboard's language (i.e., english)",
"required": true,
"type": "string"
},
{
"name": "mode",
"in": "query",
"description": "The primary mode (i.e., time)",
"required": true,
"type": "string"
},
{
"name": "mode2",
"in": "query",
"description": "The secondary mode (i.e., 60)",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/LeaderboardEntry"
}
}
}
}
}
},
"definitions": {
"Response": {
"type": "object",
"required": ["message", "data"],
"properties": {
"message": {
"type": "string"
},
"data": {
"type": "object"
}
}
},
"PersonalBest": {
"type": "object",
"properties": {
"acc": {
"type": "number",
"format": "double",
"example": 94.44
},
"consistency": {
"type": "number",
"format": "double",
"example": 75.98
},
"difficulty": {
"type": "string",
"example": "normal"
},
"lazyMode": {
"type": "boolean",
"example": false
},
"language": {
"type": "string",
"example": "english"
},
"punctuation": {
"type": "boolean",
"example": false
},
"raw": {
"type": "number",
"format": "double",
"example": 116.6
},
"wpm": {
"type": "number",
"format": "double",
"example": 107.6
},
"timestamp": {
"type": "integer",
"example": 1644438189583
}
}
},
"LeaderboardEntry": {
"type": "object",
"properties": {
"_id": {
"type": "string",
"example": "6226b17aebc27a4a8d1ce04b"
},
"acc": {
"type": "number",
"format": "double",
"example": 97.96
},
"consistency": {
"type": "number",
"format": "double",
"example": 83.29
},
"lazyMode": {
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "Miodec"
},
"punctuation": {
"type": "boolean",
"example": false
},
"rank": {
"type": "integer",
"example": 3506
},
"raw": {
"type": "number",
"format": "double",
"example": 145.18
},
"wpm": {
"type": "number",
"format": "double",
"example": 141.18
},
"timestamp": {
"type": "integer",
"example": 1644438189583
}
}
}
}
}

View file

@ -28,6 +28,7 @@
"simple-git": "2.45.1",
"string-similarity": "4.0.4",
"swagger-stats": "0.99.2",
"swagger-ui-express": "4.3.0",
"ua-parser-js": "0.7.28",
"uuid": "8.3.2"
},
@ -39,6 +40,7 @@
"@types/node": "17.0.18",
"@types/node-fetch": "2.6.1",
"@types/swagger-stats": "0.95.4",
"@types/swagger-ui-express": "4.1.3",
"@types/ua-parser-js": "0.7.36",
"@types/uuid": "8.3.4"
},
@ -796,6 +798,16 @@
"prom-client": ">=11.5.3"
}
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
@ -4474,6 +4486,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.6.1.tgz",
"integrity": "sha512-GyP8Hx9qGs7cN6gIK5rTG/NX9CmDDHjq1wzIYlRyJVWZir/D5xarkAroZDYTf4j13ontCQSUZ4Jw83XQoVbB+g=="
},
"node_modules/swagger-ui-express": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
"dependencies": {
"swagger-ui-dist": ">=4.1.3"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0"
}
},
"node_modules/tar": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
@ -5606,6 +5637,16 @@
"prom-client": ">=11.5.3"
}
},
"@types/swagger-ui-express": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz",
"integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
@ -8449,6 +8490,19 @@
}
}
},
"swagger-ui-dist": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.6.1.tgz",
"integrity": "sha512-GyP8Hx9qGs7cN6gIK5rTG/NX9CmDDHjq1wzIYlRyJVWZir/D5xarkAroZDYTf4j13ontCQSUZ4Jw83XQoVbB+g=="
},
"swagger-ui-express": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
"requires": {
"swagger-ui-dist": ">=4.1.3"
}
},
"tar": {
"version": "6.1.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",

View file

@ -35,6 +35,7 @@
"simple-git": "2.45.1",
"string-similarity": "4.0.4",
"swagger-stats": "0.99.2",
"swagger-ui-express": "4.3.0",
"ua-parser-js": "0.7.28",
"uuid": "8.3.2"
},
@ -46,6 +47,7 @@
"@types/node": "17.0.18",
"@types/node-fetch": "2.6.1",
"@types/swagger-stats": "0.95.4",
"@types/swagger-ui-express": "4.1.3",
"@types/ua-parser-js": "0.7.36",
"@types/uuid": "8.3.4"
}

View file

@ -13,5 +13,5 @@
"esModuleInterop": true,
"strictNullChecks": true
},
"exclude": ["node_modules", "build"]
"exclude": ["node_modules", "build", "worker.js"]
}

View file

@ -4159,10 +4159,18 @@
</div>
</div>
</div>
<!-- <div class="section apeKeys needsAccount">
<div class="section apeKeys needsAccount">
<h1>ape keys</h1>
<div class="text">
Generate Ape Keys to access certain API endpoints.
Documentation is available
<a
href="https://api.monkeytype.com/documentation"
target="_blank"
>
here.
</a>
More endpoints will be added in the future.
</div>
<div class="buttons">
<div
@ -4174,7 +4182,7 @@
open
</div>
</div>
</div> -->
</div>
<div class="section resetSettings">
<h1>reset settings</h1>
<div class="text">