model.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import type { ClientSession } from 'mongodb'
  2. import type { Context } from '../types'
  3. import { ObjectId } from 'mongodb'
  4. import crypto from 'crypto'
  5. import type { Account, AccountCreate, AccountUpdate } from './types'
  6. /** Model for accessing and managing accounts. */
  7. export type AccountModel = Awaited<ReturnType<typeof createAccountModel>>
  8. /** Create an account model. */
  9. async function createAccountModel(ctx: Context) {
  10. const collection = ctx.db.collection<Account>('account')
  11. /**
  12. * Create an account.
  13. * The password is automatically hashed.
  14. */
  15. async function create(input: AccountCreate) {
  16. const passwordSalt = generateSalt()
  17. const password = hashPassword(input.password, passwordSalt)
  18. const result = await collection.insertOne({
  19. email: input.email,
  20. password,
  21. passwordSalt,
  22. })
  23. return await collection.findOne({ _id: result.insertedId })
  24. }
  25. /**
  26. * Delete an account.
  27. * This function also deletes any related data, including herds and tasks.
  28. */
  29. async function _delete(id: ObjectId | string, session?: ClientSession) {
  30. let deletedHerds = 0
  31. let deletedTasks = 0
  32. // Delete herds
  33. const herds = ctx.ctx().model.herd.collection.find({ _account: new ObjectId(id) }, { projection: { _id: 1 }, session })
  34. for await (const herd of herds) {
  35. const result = await ctx.ctx().model.herd.delete(herd._id)
  36. deletedHerds++
  37. deletedTasks += result.deletedCount
  38. }
  39. const account = await collection.findOneAndDelete({ _id: new ObjectId(id) }, { session })
  40. return { account, deletedHerds, deletedTasks }
  41. }
  42. /** Generate a salt for use in password hashing. */
  43. function generateSalt() {
  44. return crypto.randomBytes(32).toString('hex')
  45. }
  46. /** Hash a password. */
  47. function hashPassword(password: string, salt: string) {
  48. return crypto.createHmac('sha256', salt).update(password).digest('hex')
  49. }
  50. /** Initialize the account collection. */
  51. async function init() {
  52. await ctx.db.createCollection('account')
  53. const exists = await collection.indexExists('account_unq_email')
  54. if (!exists) {
  55. await collection.createIndex({ email: 1 }, { name: 'account_unq_email', unique: true })
  56. }
  57. }
  58. /**
  59. * Update an account.
  60. * If a password is given, it is automatically hashed with a new salt.
  61. */
  62. async function update(id: ObjectId | string, input: AccountUpdate) {
  63. const changes = <Partial<Account>>{}
  64. if (input.email) changes.email = input.email
  65. if (input.password) {
  66. changes.passwordSalt = crypto.randomBytes(32).toString('hex')
  67. changes.password = crypto.createHmac('sha256', changes.passwordSalt).update(input.password).digest('hex')
  68. }
  69. return collection.findOneAndUpdate(
  70. { _id: new ObjectId(id) },
  71. { $set: changes },
  72. { returnDocument: 'after' },
  73. )
  74. }
  75. // Initialize on startup
  76. await init()
  77. return {
  78. collection,
  79. create,
  80. delete: _delete,
  81. generateSalt,
  82. hashPassword,
  83. init,
  84. update,
  85. }
  86. }
  87. export default createAccountModel