diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
index 239137d0b..548933f01 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx
@@ -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 (
+
+ Never
+
+ );
+ }
+
+ return {i18n.date(row.original.lastUsedAt)};
+ },
+ },
{
header: t`Actions`,
cell: ({ row }) => (
@@ -146,6 +160,9 @@ export default function ApiTokensPage() {
+
+
+
diff --git a/packages/lib/server-only/public-api/get-api-token-by-token.ts b/packages/lib/server-only/public-api/get-api-token-by-token.ts
index d25b9b902..abfab5ec3 100644
--- a/packages/lib/server-only/public-api/get-api-token-by-token.ts
+++ b/packages/lib/server-only/public-api/get-api-token-by-token.ts
@@ -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,
diff --git a/packages/lib/server-only/public-api/get-api-tokens.ts b/packages/lib/server-only/public-api/get-api-tokens.ts
index 06e014a10..a97ba809d 100644
--- a/packages/lib/server-only/public-api/get-api-tokens.ts
+++ b/packages/lib/server-only/public-api/get-api-tokens.ts
@@ -22,6 +22,7 @@ export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
name: true,
createdAt: true,
expires: true,
+ lastUsedAt: true,
},
orderBy: {
createdAt: 'desc',
diff --git a/packages/prisma/migrations/20260818081941_add_api_token_last_used_property/migration.sql b/packages/prisma/migrations/20260818081941_add_api_token_last_used_property/migration.sql
new file mode 100644
index 000000000..9b9f88927
--- /dev/null
+++ b/packages/prisma/migrations/20260818081941_add_api_token_last_used_property/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "ApiToken" ADD COLUMN "lastUsedAt" TIMESTAMP(3);
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 06be2eed9..13768e34f 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -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 {
diff --git a/packages/trpc/server/api-token-router/get-api-tokens.types.ts b/packages/trpc/server/api-token-router/get-api-tokens.types.ts
index 6a690066d..c3f06d916 100644
--- a/packages/trpc/server/api-token-router/get-api-tokens.types.ts
+++ b/packages/trpc/server/api-token-router/get-api-tokens.types.ts
@@ -9,6 +9,7 @@ export const ZGetApiTokensResponseSchema = z.array(
name: true,
createdAt: true,
expires: true,
+ lastUsedAt: true,
}),
);