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); 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`, header: t`Actions`,
cell: ({ row }) => ( cell: ({ row }) => (
@@ -146,6 +160,9 @@ export default function ApiTokensPage() {
<TableCell> <TableCell>
<Skeleton className="h-4 w-16 rounded-full" /> <Skeleton className="h-4 w-16 rounded-full" />
</TableCell> </TableCell>
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
<TableCell> <TableCell>
<Skeleton className="h-4 w-12 rounded-full" /> <Skeleton className="h-4 w-12 rounded-full" />
</TableCell> </TableCell>
@@ -1,9 +1,12 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { logger } from '../../utils/logger';
import { hashString } from '../auth/hash'; import { hashString } from '../auth/hash';
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
const LAST_USED_AT_UPDATE_INTERVAL = 60_000; // 1 minute
type GetApiTokenByTokenOptions = { type GetApiTokenByTokenOptions = {
token: string; 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 { return {
...apiToken, ...apiToken,
user, user,
@@ -22,6 +22,7 @@ export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
name: true, name: true,
createdAt: true, createdAt: true,
expires: true, expires: true,
lastUsedAt: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApiToken" ADD COLUMN "lastUsedAt" TIMESTAMP(3);
+1
View File
@@ -226,6 +226,7 @@ model ApiToken {
algorithm ApiTokenAlgorithm @default(SHA512) algorithm ApiTokenAlgorithm @default(SHA512)
expires DateTime? expires DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
lastUsedAt DateTime?
userId Int? userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId Int teamId Int
@@ -9,6 +9,7 @@ export const ZGetApiTokensResponseSchema = z.array(
name: true, name: true,
createdAt: true, createdAt: true,
expires: true, expires: true,
lastUsedAt: true,
}), }),
); );