feat: last used column for api tokens (#3230)

This commit is contained in:
Catalin Pit
2026-08-25 18:07:31 +10:00
committed by GitHub
parent 73d58bdf31
commit 10f2b80025
6 changed files with 58 additions and 10 deletions
@@ -68,6 +68,20 @@ export default function ApiTokensPage() {
return i18n.date(row.original.expires);
},
},
{
header: t`Last Used`,
cell: ({ row }) => {
if (!row.original.lastUsedAt) {
return (
<span className="text-muted-foreground">
<Trans>Never</Trans>
</span>
);
}
return <span className="text-foreground">{i18n.date(row.original.lastUsedAt)}</span>;
},
},
{
header: t`Actions`,
cell: ({ row }) => (
@@ -146,6 +160,9 @@ export default function ApiTokensPage() {
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
@@ -1,9 +1,12 @@
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { logger } from '../../utils/logger';
import { hashString } from '../auth/hash';
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
const LAST_USED_AT_UPDATE_INTERVAL = 60_000; // 1 minute
type GetApiTokenByTokenOptions = {
token: string;
@@ -96,6 +99,29 @@ export const getApiTokenByToken = async ({ token, bypassRateLimit = false }: Get
});
}
// Only update the lastUsedAt after X amount of time has passed to reduce
// the number of writes to the database
if (!apiToken.lastUsedAt || apiToken.lastUsedAt.getTime() + LAST_USED_AT_UPDATE_INTERVAL < Date.now()) {
void prisma.apiToken
.updateMany({
where: {
id: apiToken.id,
// Optimistic guard: skip if another request beat us
lastUsedAt: apiToken.lastUsedAt,
},
data: {
lastUsedAt: new Date(),
},
})
.catch((err) => {
logger.warn({
msg: 'Failed to update API token lastUsedAt',
apiTokenId: apiToken.id,
err,
});
});
}
return {
...apiToken,
user,
@@ -22,6 +22,7 @@ export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
name: true,
createdAt: true,
expires: true,
lastUsedAt: true,
},
orderBy: {
createdAt: 'desc',
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApiToken" ADD COLUMN "lastUsedAt" TIMESTAMP(3);
+11 -10
View File
@@ -220,16 +220,17 @@ enum ApiTokenAlgorithm {
}
model ApiToken {
id Int @id @default(autoincrement())
name String
token String @unique
algorithm ApiTokenAlgorithm @default(SHA512)
expires DateTime?
createdAt DateTime @default(now())
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
id Int @id @default(autoincrement())
name String
token String @unique
algorithm ApiTokenAlgorithm @default(SHA512)
expires DateTime?
createdAt DateTime @default(now())
lastUsedAt DateTime?
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
}
enum SubscriptionStatus {
@@ -9,6 +9,7 @@ export const ZGetApiTokensResponseSchema = z.array(
name: true,
createdAt: true,
expires: true,
lastUsedAt: true,
}),
);