Final touches/tuning for ApeKeys (#2663)

* Final touches/tuning to ApeKeys

* Add rate limiting note

* Add malformed status
This commit is contained in:
Bruce Berrios 2022-03-09 13:40:35 -05:00 committed by GitHub
parent 67e7c44e89
commit 7fa2827a60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 43 deletions

View file

@ -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),
});

View file

@ -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",
},
};

View file

@ -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<MonkeyTypes.ApeKey | null> {
static async getApeKey(keyId: string): Promise<MonkeyTypes.ApeKey | null> {
return await db
.collection<MonkeyTypes.ApeKey>(COLLECTION_NAME)
.findOne(getApeKeyFilter(uid, keyId));
.findOne({ _id: new ObjectId(keyId) });
}
static async countApeKeysForUser(uid: string): Promise<number> {
@ -37,14 +35,10 @@ class ApeKeysDAO {
}
static async addApeKey(apeKey: MonkeyTypes.ApeKey): Promise<string> {
const apeKeyId = new ObjectId();
await db.collection<MonkeyTypes.ApeKey>(COLLECTION_NAME).insertOne({
_id: apeKeyId,
...apeKey,
});
return apeKeyId.toHexString();
const insertionResult = await db
.collection<MonkeyTypes.ApeKey>(COLLECTION_NAME)
.insertOne(apeKey);
return insertionResult.insertedId.toHexString();
}
private static async updateApeKey(
@ -52,12 +46,16 @@ class ApeKeysDAO {
keyId: string,
updates: MatchKeysAndValues<MonkeyTypes.ApeKey>
): Promise<void> {
await db
const updateResult = await db
.collection<MonkeyTypes.ApeKey>(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<void> {
await db
const deletionResult = await db
.collection<MonkeyTypes.ApeKey>(COLLECTION_NAME)
.deleteOne(getApeKeyFilter(uid, keyId));
if (deletionResult.deletedCount === 0) {
throw new MonkeyError(404, "ApeKey not found");
}
}
}

View file

@ -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",

View file

@ -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 };