Skip to content

Commit

Permalink
impr: use tsrest for ape-keys endpoint (@fehmer) (monkeytypegame#5694)
Browse files Browse the repository at this point in the history
!nuf
  • Loading branch information
fehmer authored Jul 31, 2024
1 parent f19fb0b commit 1cbb9fa
Show file tree
Hide file tree
Showing 19 changed files with 642 additions and 263 deletions.
422 changes: 422 additions & 0 deletions backend/__tests__/api/controllers/ape-key.spec.ts

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/scripts/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export function getOpenApi(): OpenAPIObject {
description: "User specific configuration presets.",
"x-displayName": "User presets",
},
{
name: "ape-keys",
description: "Ape keys provide access to certain API endpoints.",
"x-displayName": "Ape Keys",
},
],
},

Expand Down
40 changes: 24 additions & 16 deletions backend/src/api/controllers/ape-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@ import { randomBytes } from "crypto";
import { hash } from "bcrypt";
import * as ApeKeysDAL from "../../dal/ape-keys";
import MonkeyError from "../../utils/error";
import { MonkeyResponse } from "../../utils/monkey-response";
import { MonkeyResponse2 } from "../../utils/monkey-response";
import { base64UrlEncode } from "../../utils/misc";
import { ObjectId } from "mongodb";
import { ApeKey } from "@monkeytype/shared-types";

import {
AddApeKeyRequest,
AddApeKeyResponse,
ApeKeyParams,
EditApeKeyRequest,
GetApeKeyResponse,
} from "@monkeytype/contracts/ape-keys";
import { ApeKey } from "@monkeytype/contracts/schemas/ape-keys";

function cleanApeKey(apeKey: MonkeyTypes.ApeKeyDB): ApeKey {
return _.omit(apeKey, "hash", "_id", "uid", "useCount");
}

export async function getApeKeys(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
req: MonkeyTypes.Request2
): Promise<GetApeKeyResponse> {
const { uid } = req.ctx.decodedToken;

const apeKeys = await ApeKeysDAL.getApeKeys(uid);
const cleanedKeys = _(apeKeys).keyBy("_id").mapValues(cleanApeKey).value();

return new MonkeyResponse("ApeKeys retrieved", cleanedKeys);
return new MonkeyResponse2("ApeKeys retrieved", cleanedKeys);
}

export async function generateApeKey(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
req: MonkeyTypes.Request2<undefined, AddApeKeyRequest>
): Promise<AddApeKeyResponse> {
const { name, enabled } = req.body;
const { uid } = req.ctx.decodedToken;
const { maxKeysPerUser, apeKeyBytes, apeKeySaltRounds } =
Expand Down Expand Up @@ -54,32 +62,32 @@ export async function generateApeKey(

const apeKeyId = await ApeKeysDAL.addApeKey(apeKey);

return new MonkeyResponse("ApeKey generated", {
return new MonkeyResponse2("ApeKey generated", {
apeKey: base64UrlEncode(`${apeKeyId}.${apiKey}`),
apeKeyId,
apeKeyDetails: cleanApeKey(apeKey),
});
}

export async function editApeKey(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
req: MonkeyTypes.Request2<undefined, EditApeKeyRequest, ApeKeyParams>
): Promise<MonkeyResponse2> {
const { apeKeyId } = req.params;
const { name, enabled } = req.body;
const { uid } = req.ctx.decodedToken;

await ApeKeysDAL.editApeKey(uid, apeKeyId as string, name, enabled);
await ApeKeysDAL.editApeKey(uid, apeKeyId, name, enabled);

return new MonkeyResponse("ApeKey updated");
return new MonkeyResponse2("ApeKey updated", null);
}

export async function deleteApeKey(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
req: MonkeyTypes.Request2<undefined, undefined, ApeKeyParams>
): Promise<MonkeyResponse2> {
const { apeKeyId } = req.params;
const { uid } = req.ctx.decodedToken;

await ApeKeysDAL.deleteApeKey(uid, apeKeyId as string);
await ApeKeysDAL.deleteApeKey(uid, apeKeyId);

return new MonkeyResponse("ApeKey deleted");
return new MonkeyResponse2("ApeKey deleted", null);
}
107 changes: 29 additions & 78 deletions backend/src/api/routes/ape-keys.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,42 @@
import joi from "joi";
import { Router } from "express";
import { authenticateRequest } from "../../middlewares/auth";
import * as ApeKeyController from "../controllers/ape-key";
import { apeKeysContract } from "@monkeytype/contracts/ape-keys";
import { initServer } from "@ts-rest/express";
import * as RateLimit from "../../middlewares/rate-limit";
import * as ApeKeyController from "../controllers/ape-key";
import { callController } from "../ts-rest-adapter";
import { checkUserPermissions } from "../../middlewares/permission";
import { validate } from "../../middlewares/configuration";
import { asyncHandler } from "../../middlewares/utility";
import { validateRequest } from "../../middlewares/validation";

const apeKeyNameSchema = joi
.string()
.regex(/^[0-9a-zA-Z_.-]+$/)
.max(20)
.messages({
"string.pattern.base": "Invalid ApeKey name",
"string.max": "ApeKey name exceeds maximum of 20 characters",
});

const checkIfUserCanManageApeKeys = checkUserPermissions({
criteria: (user) => {
// Must be an exact check
return user.canManageApeKeys !== false;
},
invalidMessage: "You have lost access to ape keys, please contact support",
});

const router = Router();

router.use(
const commonMiddleware = [
validate({
criteria: (configuration) => {
return configuration.apeKeys.endpointsEnabled;
},
invalidMessage: "ApeKeys are currently disabled.",
})
);

router.get(
"/",
authenticateRequest(),
RateLimit.apeKeysGet,
checkIfUserCanManageApeKeys,
asyncHandler(ApeKeyController.getApeKeys)
);

router.post(
"/",
authenticateRequest(),
RateLimit.apeKeysGenerate,
checkIfUserCanManageApeKeys,
validateRequest({
body: {
name: apeKeyNameSchema.required(),
enabled: joi.boolean().required(),
},
}),
asyncHandler(ApeKeyController.generateApeKey)
);

router.patch(
"/:apeKeyId",
authenticateRequest(),
RateLimit.apeKeysUpdate,
checkIfUserCanManageApeKeys,
validateRequest({
params: {
apeKeyId: joi.string().token().required(),
},
body: {
name: apeKeyNameSchema,
enabled: joi.boolean(),
},
}),
asyncHandler(ApeKeyController.editApeKey)
);

router.delete(
"/:apeKeyId",
authenticateRequest(),
RateLimit.apeKeysDelete,
checkIfUserCanManageApeKeys,
validateRequest({
params: {
apeKeyId: joi.string().token().required(),
checkUserPermissions({
criteria: (user) => {
return user.canManageApeKeys ?? false;
},
invalidMessage: "You have lost access to ape keys, please contact support",
}),
asyncHandler(ApeKeyController.deleteApeKey)
);
];

export default router;
const s = initServer();
export default s.router(apeKeysContract, {
get: {
middleware: [...commonMiddleware, RateLimit.apeKeysGet],
handler: async (r) => callController(ApeKeyController.getApeKeys)(r),
},
add: {
middleware: [...commonMiddleware, RateLimit.apeKeysGenerate],
handler: async (r) => callController(ApeKeyController.generateApeKey)(r),
},
save: {
middleware: [...commonMiddleware, RateLimit.apeKeysUpdate],
handler: async (r) => callController(ApeKeyController.editApeKey)(r),
},
delete: {
middleware: [...commonMiddleware, RateLimit.apeKeysDelete],
handler: async (r) => callController(ApeKeyController.deleteApeKey)(r),
},
});
2 changes: 1 addition & 1 deletion backend/src/api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ const API_ROUTE_MAP = {
"/public": publicStats,
"/leaderboards": leaderboards,
"/quotes": quotes,
"/ape-keys": apeKeys,
"/admin": admin,
"/webhooks": webhooks,
"/docs": docs,
};

const s = initServer();
const router = s.router(contract, {
apeKeys,
configs,
presets,
});
Expand Down
9 changes: 5 additions & 4 deletions backend/src/dal/ape-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export async function getApeKey(
}

export async function countApeKeysForUser(uid: string): Promise<number> {
const apeKeys = await getApeKeys(uid);
return _.size(apeKeys);
return getApeKeysCollection().countDocuments({ uid });
}

export async function addApeKey(apeKey: MonkeyTypes.ApeKeyDB): Promise<string> {
Expand Down Expand Up @@ -64,9 +63,11 @@ async function updateApeKey(
export async function editApeKey(
uid: string,
keyId: string,
name: string,
enabled: boolean
name?: string,
enabled?: boolean
): Promise<void> {
//check if there is a change
if (name === undefined && enabled === undefined) return;
const apeKeyUpdates = {
name,
enabled,
Expand Down
90 changes: 0 additions & 90 deletions backend/src/documentation/internal-swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@
"name": "psas",
"description": "Public service announcements"
},
{
"name": "ape-keys",
"description": "ApeKey data and related operations"
},
{
"name": "leaderboards",
"description": "Leaderboard data"
Expand Down Expand Up @@ -434,92 +430,6 @@
}
}
},
"/ape-keys": {
"get": {
"tags": ["ape-keys"],
"summary": "Gets ApeKeys created by a user",
"responses": {
"default": {
"description": "",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
},
"post": {
"tags": ["ape-keys"],
"summary": "Creates an ApeKey",
"parameters": [
{
"in": "body",
"name": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
}
}
}
}
],
"responses": {
"default": {
"description": "",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
},
"/ape-keys/{apeKeyId}": {
"patch": {
"tags": ["ape-keys"],
"summary": "Updates an ApeKey",
"parameters": [
{
"in": "path",
"name": "apeKeyId",
"required": true,
"type": "string"
}
],
"responses": {
"default": {
"description": "",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
},
"delete": {
"tags": ["ape-keys"],
"summary": "Deletes an ApeKey",
"parameters": [
{
"in": "path",
"name": "apeKeyId",
"required": true,
"type": "string"
}
],
"responses": {
"default": {
"description": "",
"schema": {
"$ref": "#/definitions/Response"
}
}
}
}
},
"/leaderboards": {
"get": {
"tags": ["leaderboards"],
Expand Down
2 changes: 1 addition & 1 deletion backend/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ declare namespace MonkeyTypes {
_id: ObjectId;
};

type ApeKeyDB = import("@monkeytype/shared-types").ApeKey & {
type ApeKeyDB = import("@monkeytype/contracts/schemas/ape-keys").ApeKey & {
_id: ObjectId;
uid: string;
hash: string;
Expand Down
Loading

0 comments on commit 1cbb9fa

Please sign in to comment.