feat: Add lastUsedAt to API keys (#7082)

* feat: Add lastUsedAt to API keys

* rename column to lastActiveAt

* switch order
This commit is contained in:
Hemachandar
2024-06-20 18:48:35 +05:30
committed by GitHub
parent a19fb25bea
commit 1bf9012992
9 changed files with 72 additions and 0 deletions

View File

@@ -100,6 +100,8 @@ export default function auth(options: AuthenticationOptions = {}) {
if (!user) {
throw AuthenticationError("Invalid API key");
}
await apiKey.updateActiveAt();
} else {
type = AuthenticationType.APP;
user = await getUserForJWT(String(token));

View File

@@ -0,0 +1,15 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("apiKeys", "lastActiveAt", {
type: Sequelize.DATE,
allowNull: true,
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("apiKeys", "lastActiveAt");
},
};

View File

@@ -17,4 +17,26 @@ describe("#ApiKey", () => {
expect(ApiKey.match("1234567890")).toBe(false);
});
});
describe("lastActiveAt", () => {
test("should update lastActiveAt", async () => {
const apiKey = await buildApiKey({
name: "Dev",
});
await apiKey.updateActiveAt();
expect(apiKey.lastActiveAt).toBeTruthy();
});
test("should not update lastActiveAt within 5 minutes", async () => {
const apiKey = await buildApiKey({
name: "Dev",
});
await apiKey.updateActiveAt();
expect(apiKey.lastActiveAt).toBeTruthy();
const lastActiveAt = apiKey.lastActiveAt;
await apiKey.updateActiveAt();
expect(apiKey.lastActiveAt).toEqual(lastActiveAt);
});
});
});

View File

@@ -1,3 +1,4 @@
import { subMinutes } from "date-fns";
import randomstring from "randomstring";
import { InferAttributes, InferCreationAttributes } from "sequelize";
import {
@@ -39,6 +40,10 @@ class ApiKey extends ParanoidModel<
@Column
expiresAt: Date | null;
@IsDate
@Column
lastActiveAt: Date | null;
// hooks
@BeforeValidate
@@ -67,6 +72,18 @@ class ApiKey extends ParanoidModel<
@ForeignKey(() => User)
@Column
userId: string;
updateActiveAt = async () => {
const fiveMinutesAgo = subMinutes(new Date(), 5);
// ensure this is updated only every few minutes otherwise
// we'll be constantly writing to the DB as API requests happen
if (!this.lastActiveAt || this.lastActiveAt < fiveMinutesAgo) {
this.lastActiveAt = new Date();
}
return this.save();
};
}
export default ApiKey;

View File

@@ -8,5 +8,6 @@ export default function presentApiKey(key: ApiKey) {
createdAt: key.createdAt,
updatedAt: key.updatedAt,
expiresAt: key.expiresAt,
lastActiveAt: key.lastActiveAt,
};
}

View File

@@ -20,6 +20,7 @@ describe("#apiKeys.create", () => {
expect(res.status).toEqual(200);
expect(body.data.name).toEqual("My API Key");
expect(body.data.expiresAt).toEqual(now.toISOString());
expect(body.data.lastActiveAt).toBeNull();
});
it("should allow creating an api key without expiry", async () => {
@@ -36,6 +37,7 @@ describe("#apiKeys.create", () => {
expect(res.status).toEqual(200);
expect(body.data.name).toEqual("My API Key");
expect(body.data.expiresAt).toBeNull();
expect(body.data.lastActiveAt).toBeNull();
});
it("should require authentication", async () => {