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

@@ -24,6 +24,12 @@ class ApiKey extends Model {
@observable
expiresAt?: string;
/**
* An optional datetime that the API key was last used at.
*/
@observable
lastActiveAt?: string;
secret: string;
/**

View File

@@ -27,6 +27,12 @@ const ApiKeyListItem = ({ apiKey, isCopied, onCopy }: Props) => {
<Text type="tertiary">
{t(`Created`)} <Time dateTime={apiKey.createdAt} addSuffix /> &middot;{" "}
</Text>
{apiKey.lastActiveAt && (
<Text type={"tertiary"}>
{t("Last used")} <Time dateTime={apiKey.lastActiveAt} addSuffix />{" "}
&middot;{" "}
</Text>
)}
<Text type={apiKey.isExpired ? "danger" : "tertiary"}>
{apiKey.expiresAt
? dateToExpiry(apiKey.expiresAt, t, userLocale)

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 () => {

View File

@@ -768,6 +768,7 @@
"API key copied to clipboard": "API key copied to clipboard",
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Personal keys": "Personal keys",
"Last used": "Last used",
"No expiry": "No expiry",
"Copied": "Copied",
"Revoking": "Revoking",