HealthCheckController.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { db, ObjectId } from './mongodb.js'
  2. import settings from '@overleaf/settings'
  3. import logger from '@overleaf/logger'
  4. import {
  5. fetchJson,
  6. fetchNothing,
  7. RequestFailedError,
  8. } from '@overleaf/fetch-utils'
  9. import { expressify } from '@overleaf/promise-utils'
  10. import { z, zz } from '@overleaf/validation-tools'
  11. import type { Request, Response } from 'express'
  12. const { port } = settings.internal.notifications
  13. function makeUrl(userId: string, endPath?: string) {
  14. return new URL(
  15. `/user/${userId}${endPath ? `/${endPath}` : ''}`,
  16. `http://127.0.0.1:${port}`
  17. )
  18. }
  19. async function makeNotification(notificationKey: string, userId: string) {
  20. const postOpts = {
  21. method: 'POST',
  22. json: {
  23. key: notificationKey,
  24. messageOpts: '',
  25. templateKey: 'f4g5',
  26. user_id: userId,
  27. },
  28. signal: AbortSignal.timeout(5000),
  29. }
  30. const url = makeUrl(userId)
  31. await fetchNothing(url, postOpts)
  32. }
  33. const getUserNotificationsResponseSchema = z
  34. .object({
  35. _id: zz.objectId(),
  36. key: z.string(),
  37. messageOpts: z.string().optional(),
  38. templateKey: z.string().optional(),
  39. user_id: zz.objectId(),
  40. })
  41. .array()
  42. async function getUsersNotifications(userId: string) {
  43. const url = makeUrl(userId)
  44. try {
  45. const body = await fetchJson(url, {
  46. signal: AbortSignal.timeout(5000),
  47. })
  48. return getUserNotificationsResponseSchema.parse(body)
  49. } catch (err) {
  50. if (err instanceof RequestFailedError) {
  51. logger.err({ err }, 'Non-2xx status code received')
  52. throw err
  53. }
  54. logger.err({ err }, 'Health Check: error getting notification')
  55. throw err
  56. }
  57. }
  58. async function userHasNotification(userId: string, notificationKey: string) {
  59. const body = await getUsersNotifications(userId)
  60. const hasNotification = body.some(
  61. notification =>
  62. notification.key === notificationKey && notification.user_id === userId
  63. )
  64. if (hasNotification) {
  65. return body
  66. } else {
  67. logger.err(
  68. { body, notificationKey },
  69. 'Health Check: notification not in response'
  70. )
  71. throw new Error('notification not found in response')
  72. }
  73. }
  74. async function cleanupNotifications(userId: string) {
  75. await db.notifications.deleteOne({ user_id: userId })
  76. }
  77. async function deleteNotification(
  78. userId: string,
  79. notificationId: string,
  80. notificationKey: string
  81. ) {
  82. const deleteByIdUrl = makeUrl(userId, `notification/${notificationId}`)
  83. try {
  84. await fetchNothing(deleteByIdUrl, {
  85. signal: AbortSignal.timeout(5000),
  86. method: 'DELETE',
  87. })
  88. } catch (err) {
  89. logger.err(
  90. { err, url: deleteByIdUrl },
  91. 'Health Check: error cleaning up notification'
  92. )
  93. throw err
  94. }
  95. const deleteByKeyUrl = makeUrl(userId)
  96. try {
  97. await fetchNothing(deleteByKeyUrl, {
  98. signal: AbortSignal.timeout(5000),
  99. method: 'DELETE',
  100. json: {
  101. key: notificationKey,
  102. },
  103. })
  104. } catch (err) {
  105. logger.err(
  106. { err, url: deleteByKeyUrl },
  107. 'Health Check: error cleaning up notification'
  108. )
  109. throw err
  110. }
  111. }
  112. async function check(req: Request, res: Response) {
  113. const userId = new ObjectId().toString()
  114. let notificationKey = `smoke-test-notification-${new ObjectId()}`
  115. logger.debug({ userId, key: notificationKey }, 'Health Check: running')
  116. await makeNotification(notificationKey, userId)
  117. try {
  118. const body = await userHasNotification(userId, notificationKey)
  119. const notificationId = body[0]._id
  120. notificationKey = body[0].key
  121. logger.debug(
  122. { notificationId, notificationKey },
  123. 'Health Check: doing cleanup'
  124. )
  125. await deleteNotification(userId, notificationId, notificationKey)
  126. res.sendStatus(200)
  127. } catch (err) {
  128. logger.err({ err }, 'Health Check: error running health check')
  129. res.sendStatus(500)
  130. } finally {
  131. await cleanupNotifications(userId)
  132. }
  133. }
  134. export default {
  135. check: expressify(check),
  136. }