| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- // Recurring job. Sends two kinds of email about personal access token expiry:
- //
- // - "expiring soon": token has not expired yet and falls within the
- // configured warning window
- // (Settings.personalAccessTokens.expiry.warningWindowDays, default 2 days).
- // Sent at most once per token.
- //
- // - "expired": token has expired and we have not yet emailed the owner.
- // Sent at most once per token. Tokens that were already expired before
- // the feature shipped have `notificationsSuppressedAt` set by the
- // backfill_suppress_expired_token_notifications.mjs script and are
- // deliberately excluded.
- //
- // Pass --dry-run to log who would be emailed without sending or marking
- // `lastNotifiedAt`.
- import settings from '@overleaf/settings'
- import logger from '@overleaf/logger'
- import {
- db,
- READ_PREFERENCE_SECONDARY,
- } from '../../app/src/infrastructure/mongodb.mjs'
- import { User } from '../../app/src/models/User.mjs'
- import EmailHandler from '../../app/src/Features/Email/EmailHandler.mjs'
- import { scriptRunner } from '../lib/ScriptRunner.mjs'
- const MS_PER_DAY = 24 * 60 * 60 * 1000
- export async function main({ dryRun = false } = {}) {
- const now = new Date()
- const warningWindowDays =
- settings.personalAccessTokens.expiry.warningWindowDays
- const warningHorizon = new Date(
- now.getTime() + warningWindowDays * MS_PER_DAY
- )
- logger.info(
- { warningWindowDays, warningHorizon, dryRun },
- 'starting notify_expiring_tokens'
- )
- const warningCount = await processBucket({
- kind: 'warning',
- template: 'gitTokenExpiringSoon',
- dryRun,
- query: {
- type: 'pat',
- accessTokenExpiresAt: { $gt: now, $lte: warningHorizon },
- 'lastNotifiedAt.warning': { $exists: false },
- },
- })
- const expiredCount = await processBucket({
- kind: 'expired',
- template: 'gitTokenExpired',
- dryRun,
- query: {
- type: 'pat',
- accessTokenExpiresAt: { $lt: now },
- 'lastNotifiedAt.expired': { $exists: false },
- notificationsSuppressedAt: { $exists: false },
- },
- })
- logger.info(
- { warningCount, expiredCount, dryRun },
- 'finished notify_expiring_tokens'
- )
- }
- export async function processBucket({ kind, template, query, dryRun = false }) {
- const cursor = db.oauthAccessTokens.find(query, {
- projection: {
- _id: 1,
- user_id: 1,
- },
- readPreference: READ_PREFERENCE_SECONDARY,
- })
- let sent = 0
- for await (const token of cursor) {
- const ok = await notifyOwner({ token, kind, template, dryRun })
- if (ok) sent++
- }
- return sent
- }
- export async function notifyOwner({ token, kind, template, dryRun = false }) {
- const user = await User.findOne(
- { _id: token.user_id },
- { email: 1, first_name: 1 }
- ).exec()
- if (!user?.email) {
- logger.warn(
- { tokenId: token._id, userId: token.user_id },
- 'skipping token notification: user not found or has no email'
- )
- return false
- }
- if (dryRun) {
- logger.info(
- { tokenId: token._id, userId: token.user_id, kind, template },
- 'dry run: would send git token expiry notification'
- )
- return true
- }
- try {
- await EmailHandler.promises.sendEmail(template, {
- to: user.email,
- firstName: user.first_name,
- })
- } catch (err) {
- logger.error(
- { err, tokenId: token._id, userId: token.user_id, kind },
- 'failed to send git token expiry notification; will retry next run'
- )
- return false
- }
- await db.oauthAccessTokens.updateOne(
- { _id: token._id },
- { $set: { [`lastNotifiedAt.${kind}`]: new Date() } }
- )
- return true
- }
- if (import.meta.url === `file://${process.argv[1]}`) {
- const dryRun = process.argv.includes('--dry-run')
- try {
- await scriptRunner(() => main({ dryRun }))
- process.exit(0)
- } catch (error) {
- logger.error({ err: error }, 'notify_expiring_tokens failed')
- process.exit(1)
- }
- }
|