From 7fa2827a60bdbc1ae88160b4e2e499b8b10c8851 Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Wed, 9 Mar 2022 13:40:35 -0500 Subject: [PATCH] Final touches/tuning for ApeKeys (#2663) * Final touches/tuning to ApeKeys * Add rate limiting note * Add malformed status --- backend/api/controllers/ape-keys.ts | 2 +- backend/constants/monkey-status-codes.ts | 7 ++- backend/dao/ape-keys.ts | 32 +++++++------ backend/documentation/public-swagger.json | 2 +- backend/middlewares/auth.ts | 57 +++++++++++++---------- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/backend/api/controllers/ape-keys.ts b/backend/api/controllers/ape-keys.ts index bc56a4007..40b881abb 100644 --- a/backend/api/controllers/ape-keys.ts +++ b/backend/api/controllers/ape-keys.ts @@ -54,7 +54,7 @@ class ApeKeysController { const apeKeyId = await ApeKeysDAO.addApeKey(apeKey); return new MonkeyResponse("ApeKey generated", { - apeKey: base64UrlEncode(`${uid}.${apeKeyId}.${apiKey}`), + apeKey: base64UrlEncode(`${apeKeyId}.${apiKey}`), apeKeyId, apeKeyDetails: cleanApeKey(apeKey), }); diff --git a/backend/constants/monkey-status-codes.ts b/backend/constants/monkey-status-codes.ts index c03ab25c1..c9163d83a 100644 --- a/backend/constants/monkey-status-codes.ts +++ b/backend/constants/monkey-status-codes.ts @@ -15,6 +15,7 @@ type Statuses = { GIT_GUD: Status; APE_KEY_INVALID: Status; APE_KEY_INACTIVE: Status; + APE_KEY_MALFORMED: Status; }; const statuses: Statuses = { @@ -52,7 +53,11 @@ const statuses: Statuses = { }, APE_KEY_INACTIVE: { code: 471, - message: "Ape is inactive", + message: "ApeKey is inactive", + }, + APE_KEY_MALFORMED: { + code: 472, + message: "ApeKey is malformed", }, }; diff --git a/backend/dao/ape-keys.ts b/backend/dao/ape-keys.ts index 8a607eba7..27c2cc4aa 100644 --- a/backend/dao/ape-keys.ts +++ b/backend/dao/ape-keys.ts @@ -1,6 +1,7 @@ import _ from "lodash"; import db from "../init/db"; import { Filter, ObjectId, MatchKeysAndValues } from "mongodb"; +import MonkeyError from "../utils/error"; const COLLECTION_NAME = "ape-keys"; @@ -22,13 +23,10 @@ class ApeKeysDAO { .toArray(); } - static async getApeKey( - uid: string, - keyId: string - ): Promise { + static async getApeKey(keyId: string): Promise { return await db .collection(COLLECTION_NAME) - .findOne(getApeKeyFilter(uid, keyId)); + .findOne({ _id: new ObjectId(keyId) }); } static async countApeKeysForUser(uid: string): Promise { @@ -37,14 +35,10 @@ class ApeKeysDAO { } static async addApeKey(apeKey: MonkeyTypes.ApeKey): Promise { - const apeKeyId = new ObjectId(); - - await db.collection(COLLECTION_NAME).insertOne({ - _id: apeKeyId, - ...apeKey, - }); - - return apeKeyId.toHexString(); + const insertionResult = await db + .collection(COLLECTION_NAME) + .insertOne(apeKey); + return insertionResult.insertedId.toHexString(); } private static async updateApeKey( @@ -52,12 +46,16 @@ class ApeKeysDAO { keyId: string, updates: MatchKeysAndValues ): Promise { - await db + const updateResult = await db .collection(COLLECTION_NAME) .updateOne(getApeKeyFilter(uid, keyId), { $inc: { useCount: _.has(updates, "lastUsedOn") ? 1 : 0 }, $set: _.pickBy(updates, (value) => !_.isNil(value)), }); + + if (updateResult.modifiedCount === 0) { + throw new MonkeyError(404, "ApeKey not found"); + } } static async editApeKey( @@ -84,9 +82,13 @@ class ApeKeysDAO { } static async deleteApeKey(uid: string, keyId: string): Promise { - await db + const deletionResult = await db .collection(COLLECTION_NAME) .deleteOne(getApeKeyFilter(uid, keyId)); + + if (deletionResult.deletedCount === 0) { + throw new MonkeyError(404, "ApeKey not found"); + } } } diff --git a/backend/documentation/public-swagger.json b/backend/documentation/public-swagger.json index 8a219f667..36dfb2aa4 100644 --- a/backend/documentation/public-swagger.json +++ b/backend/documentation/public-swagger.json @@ -1,7 +1,7 @@ { "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` ", + "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`\n\nThere is a rate limit of `30 requests per minute` across all endpoints. Rate limit rates are shared across all ape keys.", "version": "1.0.0", "title": "Monkeytype API", "termsOfService": "https://monkeytype.com/terms-of-service", diff --git a/backend/middlewares/auth.ts b/backend/middlewares/auth.ts index 7a4aa81d5..afe9db835 100644 --- a/backend/middlewares/auth.ts +++ b/backend/middlewares/auth.ts @@ -151,34 +151,41 @@ async function authenticateWithApeKey( throw new MonkeyError(401, "This endpoint does not accept ApeKeys"); } - const decodedKey = base64UrlDecode(key); - const [uid, keyId, apeKey] = decodedKey.split("."); + try { + const decodedKey = base64UrlDecode(key); + const [keyId, apeKey] = decodedKey.split("."); - const targetApeKey = await ApeKeysDAO.getApeKey(uid, keyId); + const targetApeKey = await ApeKeysDAO.getApeKey(keyId); + if (!targetApeKey) { + throw new MonkeyError(404, "ApeKey not found"); + } - if (!targetApeKey) { - throw new MonkeyError(404, "ApeKey not found"); + if (!targetApeKey.enabled) { + const { code, message } = statuses.APE_KEY_INACTIVE; + throw new MonkeyError(code, message); + } + + const isKeyValid = await compare(apeKey, targetApeKey.hash); + if (!isKeyValid) { + const { code, message } = statuses.APE_KEY_INVALID; + throw new MonkeyError(code, message); + } + + await ApeKeysDAO.updateLastUsedOn(targetApeKey.uid, keyId); + + return { + type: "ApeKey", + uid: targetApeKey.uid, + email: "", + }; + } catch (error) { + if (!(error instanceof MonkeyError)) { + const { code, message } = statuses.APE_KEY_MALFORMED; + throw new MonkeyError(code, message); + } + + throw error; } - - if (!targetApeKey.enabled) { - const { code, message } = statuses.APE_KEY_INACTIVE; - throw new MonkeyError(code, message); - } - - const isKeyValid = await compare(apeKey, targetApeKey?.hash ?? ""); - - if (!isKeyValid) { - const { code, message } = statuses.APE_KEY_INVALID; - throw new MonkeyError(code, message); - } - - await ApeKeysDAO.updateLastUsedOn(uid, keyId); - - return { - type: "ApeKey", - uid, - email: "", - }; } export { authenticateRequest };