OneTimeTokenHandler.mjs 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import crypto from 'node:crypto'
  2. import { db } from '../../infrastructure/mongodb.js'
  3. import Errors from '../Errors/Errors.js'
  4. import { callbackify } from 'node:util'
  5. const ONE_HOUR_IN_S = 60 * 60
  6. async function peekValueFromToken(use, token) {
  7. const tokenDoc = await db.tokens.findOneAndUpdate(
  8. {
  9. use,
  10. token,
  11. expiresAt: { $gt: new Date() },
  12. usedAt: { $exists: false },
  13. peekCount: { $not: { $gte: OneTimeTokenHandler.MAX_PEEKS } },
  14. },
  15. {
  16. $inc: { peekCount: 1 },
  17. },
  18. {
  19. returnDocument: 'after',
  20. }
  21. )
  22. if (!tokenDoc) {
  23. throw new Errors.NotFoundError('no token found')
  24. }
  25. // The allowed number of peaks will be 1 less than OneTimeTokenHandler.MAX_PEEKS
  26. // since the updated doc is returned after findOneAndUpdate above
  27. const remainingPeeks = OneTimeTokenHandler.MAX_PEEKS - tokenDoc.peekCount
  28. return { data: tokenDoc.data, remainingPeeks }
  29. }
  30. async function getNewToken(use, data, options = {}) {
  31. const expiresIn = options.expiresIn || ONE_HOUR_IN_S
  32. const createdAt = new Date()
  33. const expiresAt = new Date(createdAt.getTime() + expiresIn * 1000)
  34. const token = crypto.randomBytes(32).toString('hex')
  35. await db.tokens.insertOne({
  36. use,
  37. token,
  38. data,
  39. createdAt,
  40. expiresAt,
  41. })
  42. return token
  43. }
  44. async function getValueFromTokenAndExpire(use, token) {
  45. const now = new Date()
  46. const tokenDoc = await db.tokens.findOneAndUpdate(
  47. {
  48. use,
  49. token,
  50. expiresAt: { $gt: now },
  51. usedAt: { $exists: false },
  52. peekCount: { $not: { $gte: OneTimeTokenHandler.MAX_PEEKS } },
  53. },
  54. {
  55. $set: {
  56. usedAt: now,
  57. },
  58. }
  59. )
  60. if (!tokenDoc) {
  61. throw new Errors.NotFoundError('no token found')
  62. }
  63. return tokenDoc.data
  64. }
  65. async function expireToken(use, token) {
  66. const now = new Date()
  67. await db.tokens.updateOne(
  68. {
  69. use,
  70. token,
  71. },
  72. {
  73. $set: {
  74. usedAt: now,
  75. },
  76. }
  77. )
  78. }
  79. async function expireAllTokensForUser(userId, use) {
  80. const now = new Date()
  81. await db.tokens.updateMany(
  82. {
  83. use,
  84. 'data.user_id': userId.toString(),
  85. usedAt: { $exists: false },
  86. },
  87. {
  88. $set: {
  89. usedAt: now,
  90. },
  91. }
  92. )
  93. }
  94. const OneTimeTokenHandler = {
  95. MAX_PEEKS: 4,
  96. getNewToken: callbackify(getNewToken),
  97. getValueFromTokenAndExpire: callbackify(getValueFromTokenAndExpire),
  98. peekValueFromToken: callbackify(peekValueFromToken),
  99. expireToken: callbackify(expireToken),
  100. expireAllTokensForUser: callbackify(expireAllTokensForUser),
  101. promises: {
  102. getNewToken,
  103. getValueFromTokenAndExpire,
  104. peekValueFromToken,
  105. expireToken,
  106. expireAllTokensForUser,
  107. },
  108. }
  109. export default OneTimeTokenHandler