Files
Verso/services/web/scripts/oauth/backfill_suppress_expired_token_notifications.mjs
T
Liam O'BrienandClaude Opus 4.7 e53c6f2aea Notify users about expiring git PATs and expose PATs in admin panel (#33802)
* Allow admin access to user PATs

* Tests for new screen in admin panel

* Adding error for invalid token and way to parse error for OAuth 2

* Git bridge handles expired PAT

* Script for alerting on close to expiry and expired git tokens

* Refactoring and simplifying

* Updating email templates to match agreed docs

* tweak to email subject to include Overleaf

* Allowing dry run in scripts and general tidy up

* removing redundant tests and dry running script

* Fixing CI errors

* Adding new tab to admin test expectation

* Address PR feedback on oauth2-server changes

- Replace ad-hoc overleafErrorCode prop with a TokenExpiredError subclass
- Collapse listTokens/listTokensForAdmin into a single hook

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Adding cron definitions for alerting on expiring git pat

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: 69b9fd901a201592a580c69abe7bd7d603e85d3a
2026-06-04 08:05:26 +00:00

70 lines
1.8 KiB
JavaScript

// One-off backfill: mark every personal access token that was already expired
// at the moment the expiry-notification feature shipped, so the recurring
// notifier (notify_expiring_tokens.mjs) does not blast a "your token has
// expired" email to users about long-dead tokens.
//
// `notificationsSuppressedAt` is set ONLY by this script. It is intentionally
// distinct from `lastNotifiedAt.expired` (which records actual sends) so the
// two cases remain unambiguous in the data forever.
//
// Idempotent: re-running does nothing further once the flag is set.
//
// Pass --dry-run to count matching tokens without writing.
import logger from '@overleaf/logger'
import {
db,
READ_PREFERENCE_SECONDARY,
} from '../../app/src/infrastructure/mongodb.mjs'
import { scriptRunner } from '../lib/ScriptRunner.mjs'
async function main() {
const dryRun = process.argv.includes('--dry-run')
const now = new Date()
const cursor = db.oauthAccessTokens.find(
{
type: 'pat',
accessTokenExpiresAt: { $lt: now },
'lastNotifiedAt.expired': { $exists: false },
notificationsSuppressedAt: { $exists: false },
},
{
projection: { _id: 1 },
readPreference: READ_PREFERENCE_SECONDARY,
}
)
let matched = 0
for await (const doc of cursor) {
if (!dryRun) {
await db.oauthAccessTokens.updateOne(
{ _id: doc._id },
{ $set: { notificationsSuppressedAt: now } }
)
}
matched++
}
if (dryRun) {
logger.info(
{ matched },
'dry run: expired-token notifications would be suppressed'
)
} else {
logger.info(
{ suppressed: matched },
'expired-token notifications suppressed'
)
}
}
try {
await scriptRunner(main)
process.exit(0)
} catch (error) {
logger.error(
{ err: error },
'backfill_suppress_expired_token_notifications failed'
)
process.exit(1)
}