api.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import type { AuthRequestHandler } from '../auth'
  2. import type { Context } from '../types'
  3. import type { RequestHandler } from 'express'
  4. import type { WithId } from 'mongodb'
  5. import type { Account, AccountCreate, AccountUpdate } from './types'
  6. import { http, validate as v } from '@edge/misc-utils'
  7. /** Create an account. */
  8. export function createAccount({ model }: Context): RequestHandler {
  9. interface RequestData {
  10. account: AccountCreate
  11. }
  12. interface ResponseData {
  13. account: WithId<Account>
  14. }
  15. const readRequestData = v.validate<RequestData>({
  16. account: {
  17. email: v.email,
  18. password: v.seq(v.str, v.minLength(8)),
  19. },
  20. })
  21. return async function (req, res, next) {
  22. try {
  23. const input = readRequestData(req.body)
  24. const account = await model.account.create(input.account)
  25. if (!account) return http.notFound(res, next, { reason: 'unexpectedly failed to get new account' })
  26. const output: ResponseData = { account }
  27. res.send(output)
  28. next()
  29. } catch (err) {
  30. const name = (err as Error).name
  31. if (name === 'ValidateError') {
  32. const ve = err as v.ValidateError
  33. return http.badRequest(res, next, { param: ve.param, reason: ve.message })
  34. }
  35. return next(err)
  36. }
  37. }
  38. }
  39. /** Delete an account. */
  40. export function deleteAccount({ model }: Context): AuthRequestHandler {
  41. interface ResponseData {
  42. account: WithId<Account>
  43. herds: {
  44. deletedCount: number
  45. }
  46. tasks: {
  47. deletedCount: number
  48. }
  49. }
  50. return async function (req, res, next) {
  51. if (!req.account) return http.unauthorized(res, next)
  52. // Get account ID and assert access
  53. const id = req.params.id || req.account._id
  54. if (!id) return http.badRequest(res, next)
  55. if (!req.account._id.equals(id)) return http.forbidden(res, next)
  56. try {
  57. // Delete account
  58. const { account, deletedHerds, deletedTasks } = await model.account.delete(id)
  59. if (!account) return http.notFound(res, next)
  60. // Send output
  61. const output: ResponseData = {
  62. account,
  63. herds: {
  64. deletedCount: deletedHerds,
  65. },
  66. tasks: {
  67. deletedCount: deletedTasks,
  68. },
  69. }
  70. res.send(output)
  71. next()
  72. } catch (err) {
  73. next(err)
  74. }
  75. }
  76. }
  77. /** Get an account. */
  78. export function getAccount(): AuthRequestHandler {
  79. interface ResponseData {
  80. account: WithId<Account>
  81. }
  82. return async function (req, res, next) {
  83. if (!req.account) return http.unauthorized(res, next)
  84. // Get account ID and assert access
  85. const id = req.params.id || req.account._id
  86. if (!req.account._id.equals(id)) return http.forbidden(res, next)
  87. try {
  88. // Send output
  89. const output: ResponseData = { account: req.account }
  90. res.send(output)
  91. next()
  92. } catch (err) {
  93. return next(err)
  94. }
  95. }
  96. }
  97. /**
  98. * Log in to an account.
  99. * The token returned should be added to the authorization header of subsequent requests.
  100. */
  101. export function loginAccount({ auth, model }: Context): RequestHandler {
  102. interface RequestData {
  103. account: Pick<Account, 'email' | 'password'>
  104. }
  105. interface ResponseData {
  106. token: string
  107. account: WithId<Account>
  108. }
  109. const readRequestData = v.validate<RequestData>({
  110. account: {
  111. email: v.email,
  112. password: v.seq(v.minLength(8)),
  113. },
  114. })
  115. return async function (req, res, next) {
  116. try {
  117. // Read input
  118. const input = readRequestData(req.body)
  119. // Get account
  120. const account = await model.account.collection.findOne({ email: input.account.email })
  121. if (!account) return http.notFound(res, next)
  122. // Validate password
  123. const password = model.account.hashPassword(input.account.password, account.passwordSalt)
  124. if (password !== account.password) return http.badRequest(res, next, { reason: 'invalid password' })
  125. // Create JWT
  126. const token = await auth.sign(account._id)
  127. // Send output
  128. const output: ResponseData = { token, account }
  129. res.send(output)
  130. next()
  131. } catch (err) {
  132. const name = (err as Error).name
  133. if (name === 'ValidateError') {
  134. const ve = err as v.ValidateError
  135. return http.badRequest(res, next, { param: ve.param, reason: ve.message })
  136. }
  137. return next(err)
  138. }
  139. }
  140. }
  141. /** Update an account. */
  142. export function updateAccount({ model }: Context): AuthRequestHandler {
  143. interface RequestData {
  144. account: AccountUpdate
  145. }
  146. interface ResponseData {
  147. account: WithId<Account>
  148. }
  149. const readRequestData = v.validate<RequestData>({
  150. account: {
  151. email: v.seq(v.optional, v.email),
  152. password: v.seq(v.optional, v.str, v.minLength(8)),
  153. },
  154. })
  155. return async function (req, res, next) {
  156. if (!req.account) return http.unauthorized(res, next)
  157. // Get account ID and assert access
  158. const id = req.params.id || req.account._id
  159. if (!req.account._id.equals(id)) return http.forbidden(res, next)
  160. try {
  161. // Read input
  162. const input = readRequestData(req.body)
  163. if (!input.account.email && !input.account.password) {
  164. return http.badRequest(res, next, { reason: 'no changes' })
  165. }
  166. // Update account
  167. const account = await model.account.update(id, input.account)
  168. if (!account) return http.notFound(res, next)
  169. // Send output
  170. const output: ResponseData = { account }
  171. res.send(output)
  172. next()
  173. } catch (err) {
  174. const name = (err as Error).name
  175. if (name === 'ValidateError') {
  176. const ve = err as v.ValidateError
  177. return http.badRequest(res, next, { param: ve.param, reason: ve.message })
  178. }
  179. return next(err)
  180. }
  181. }
  182. }