feat: Add lastUsedAt to API keys (#7082)
* feat: Add lastUsedAt to API keys * rename column to lastActiveAt * switch order
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,12 @@ const ApiKeyListItem = ({ apiKey, isCopied, onCopy }: Props) => {
|
||||
<Text type="tertiary">
|
||||
{t(`Created`)} <Time dateTime={apiKey.createdAt} addSuffix /> ·{" "}
|
||||
</Text>
|
||||
{apiKey.lastActiveAt && (
|
||||
<Text type={"tertiary"}>
|
||||
{t("Last used")} <Time dateTime={apiKey.lastActiveAt} addSuffix />{" "}
|
||||
·{" "}
|
||||
</Text>
|
||||
)}
|
||||
<Text type={apiKey.isExpired ? "danger" : "tertiary"}>
|
||||
{apiKey.expiresAt
|
||||
? dateToExpiry(apiKey.expiresAt, t, userLocale)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,5 +8,6 @@ export default function presentApiKey(key: ApiKey) {
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt,
|
||||
expiresAt: key.expiresAt,
|
||||
lastActiveAt: key.lastActiveAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user