From a9df3b9ad073c3ca91892a13832cda3306b12938 Mon Sep 17 00:00:00 2001 From: Alex Johansson Date: Mon, 25 Oct 2021 17:16:42 +0200 Subject: [PATCH] Move cron jobs into GitHub actions (#1006) --- .github/workflows/cron-bookingReminder.yml | 20 ++++ .github/workflows/cron-downgradeUsers.yml | 20 ++++ pages/api/cron/bookingReminder.ts | 125 ++++++++++++--------- pages/api/cron/downgradeUsers.ts | 9 +- 4 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/cron-bookingReminder.yml create mode 100644 .github/workflows/cron-downgradeUsers.yml diff --git a/.github/workflows/cron-bookingReminder.yml b/.github/workflows/cron-bookingReminder.yml new file mode 100644 index 00000000..e3f3b4b2 --- /dev/null +++ b/.github/workflows/cron-bookingReminder.yml @@ -0,0 +1,20 @@ +name: Cron - bookingReminder + +on: + # "Scheduled workflows run on the latest commit on the default or base branch." + # — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule + schedule: + # Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru) + - cron: "0,15,30,45 * * * *" +jobs: + cron: + runs-on: ubuntu-latest + steps: + - name: cURL request + if: ${{ secrets.APP_URL && secrets.CRON_API_KEY }} + run: | + curl ${{ secrets.APP_URL }}/api/cron/bookingReminder \ + -X POST + -H 'content-type: application/json' \ + -H 'authorization: ${{ secrets.CRON_API_KEY }}' \ + --fail diff --git a/.github/workflows/cron-downgradeUsers.yml b/.github/workflows/cron-downgradeUsers.yml new file mode 100644 index 00000000..f464b518 --- /dev/null +++ b/.github/workflows/cron-downgradeUsers.yml @@ -0,0 +1,20 @@ +name: Cron - downgradeUsers + +on: + # "Scheduled workflows run on the latest commit on the default or base branch." + # — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule + schedule: + # Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru) + - cron: "0,15,30,45 * * * *" +jobs: + cron: + runs-on: ubuntu-latest + steps: + - name: cURL request + if: ${{ secrets.APP_URL && secrets.CRON_API_KEY }} + run: | + curl ${{ secrets.APP_URL }}/api/cron/downgradeUsers \ + -X POST + -H 'content-type: application/json' \ + -H 'authorization: ${{ secrets.CRON_API_KEY }}' \ + --fail diff --git a/pages/api/cron/bookingReminder.ts b/pages/api/cron/bookingReminder.ts index 8564ec6d..3b5bab41 100644 --- a/pages/api/cron/bookingReminder.ts +++ b/pages/api/cron/bookingReminder.ts @@ -7,69 +7,82 @@ import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequest import prisma from "@lib/prisma"; export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { - const apiKey = req.query.apiKey; - if (process.env.CRON_API_KEY != apiKey) { - return res.status(401).json({ message: "Not authenticated" }); + const apiKey = req.headers.authorization || req.query.apiKey; + if (process.env.CRON_API_KEY !== apiKey) { + res.status(401).json({ message: "Not authenticated" }); + return; + } + if (req.method !== "POST") { + res.status(405).json({ message: "Invalid method" }); + return; } - if (req.method == "POST") { - const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60]; - let notificationsSent = 0; - for (const interval of reminderIntervalMinutes) { - const bookings = await prisma.booking.findMany({ - where: { - confirmed: false, - rejected: false, - createdAt: { - lte: dayjs().add(-interval, "minutes").toDate(), - }, + const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60]; + let notificationsSent = 0; + for (const interval of reminderIntervalMinutes) { + const bookings = await prisma.booking.findMany({ + where: { + confirmed: false, + rejected: false, + createdAt: { + lte: dayjs().add(-interval, "minutes").toDate(), }, - select: { - title: true, - description: true, - startTime: true, - endTime: true, - attendees: true, - user: true, - id: true, - uid: true, + }, + select: { + title: true, + description: true, + startTime: true, + endTime: true, + attendees: true, + user: true, + id: true, + uid: true, + }, + }); + + const reminders = await prisma.reminderMail.findMany({ + where: { + reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, + referenceId: { + in: bookings.map((b) => b.id), }, - }); - - const reminders = await prisma.reminderMail.findMany({ - where: { - reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, - referenceId: { - in: bookings.map((b) => b.id), - }, - elapsedMinutes: { - gte: interval, - }, + elapsedMinutes: { + gte: interval, }, - }); + }, + }); - for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) { - const evt: CalendarEvent = { - type: booking.title, - title: booking.title, - description: booking.description, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - organizer: { email: booking.user.email, name: booking.user.name, timeZone: booking.user.timeZone }, - attendees: booking.attendees, - }; - - await new EventOrganizerRequestReminderMail(evt, booking.uid).sendEmail(); - await prisma.reminderMail.create({ - data: { - referenceId: booking.id, - reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, - elapsedMinutes: interval, - }, - }); - notificationsSent++; + for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) { + const { user } = booking; + const name = user?.name || user?.username; + if (!user || !name || !user.timeZone) { + console.error(`Booking ${booking.id} is missing required properties for booking reminder`, { user }); + continue; } + const evt: CalendarEvent = { + type: booking.title, + title: booking.title, + description: booking.description || undefined, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + organizer: { + email: user.email, + name, + timeZone: user.timeZone, + }, + attendees: booking.attendees, + }; + + await new EventOrganizerRequestReminderMail(evt, booking.uid).sendEmail(); + await prisma.reminderMail.create({ + data: { + referenceId: booking.id, + reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, + elapsedMinutes: interval, + }, + }); + notificationsSent++; } - res.status(200).json({ notificationsSent }); } + res.status(200).json({ notificationsSent }); } diff --git a/pages/api/cron/downgradeUsers.ts b/pages/api/cron/downgradeUsers.ts index 12b6ea91..24ad89f1 100644 --- a/pages/api/cron/downgradeUsers.ts +++ b/pages/api/cron/downgradeUsers.ts @@ -6,9 +6,14 @@ import prisma from "@lib/prisma"; const TRIAL_LIMIT_DAYS = 14; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const apiKey = req.query.apiKey; + const apiKey = req.headers.authorization || req.query.apiKey; if (process.env.CRON_API_KEY !== apiKey) { - return res.status(401).json({ message: "Not authenticated" }); + res.status(401).json({ message: "Not authenticated" }); + return; + } + if (req.method !== "POST") { + res.status(405).json({ message: "Invalid method" }); + return; } await prisma.user.updateMany({