You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

user.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. 'use strict'
  2. require('dotenv').config()
  3. const crypto = require('crypto')
  4. const Util = require('util')
  5. const JWT = require('jsonwebtoken')
  6. const Schmervice = require('@hapipal/schmervice')
  7. const SecurePassword = require('secure-password')
  8. // Configuration for Brevo
  9. const Brevo = require('@getbrevo/brevo')
  10. const apiInstance = new Brevo.TransactionalEmailsApi()
  11. const apiKey = apiInstance.apiClient.authentications['api-key']
  12. apiKey.apiKey = process.env.BREVO_KEY
  13. const sendSmtpEmail = new Brevo.SendSmtpEmail()
  14. // TODO: Consider implementing, nice use of SecurePassword,
  15. // but currently not used anywhere...
  16. const hasher = async (pwd, steak) => {
  17. const hash = await pwd.hash(steak)
  18. const result = await pwd.verify(steak, hash)
  19. let squirtle = null
  20. switch (result) {
  21. case SecurePassword.INVALID_UNRECOGNIZED_HASH:
  22. return console.error(
  23. 'This hash was not made with secure-password. Attempt legacy algorithm',
  24. )
  25. case SecurePassword.INVALID:
  26. return console.log('Invalid password')
  27. case SecurePassword.VALID:
  28. return result
  29. case SecurePassword.VALID_NEEDS_REHASH:
  30. console.log('Yay you made it, wait for us to improve your safety')
  31. try {
  32. squirtle = await pwd.hash(steak)
  33. // console.log('improvedHash', squirtle)
  34. // const saveHash = Auth.insert({user_email:
  35. // matchingEmails}).into('token')
  36. return squirtle
  37. } catch (err) {
  38. console.error(
  39. 'You are authenticated, but we could not improve your safety this time around',
  40. )
  41. }
  42. break
  43. }
  44. }
  45. /** Class for methods used in the User plugin */
  46. module.exports = class UserService extends Schmervice.Service {
  47. /**
  48. * Unsure of what our constructor does
  49. * @param {...any} args
  50. */
  51. constructor(...args) {
  52. super(...args)
  53. const pwd = new SecurePassword()
  54. // TODO: Invalidate this application state somehow after a
  55. // certain time period has passed
  56. this.activeSessions = {}
  57. this.pwd = {
  58. hash: Util.promisify(pwd.hash.bind(pwd)),
  59. verify: Util.promisify(pwd.verify.bind(pwd)),
  60. }
  61. }
  62. /**
  63. * Use knex to find users with id column
  64. * @param {number} id
  65. * @param {*} txn
  66. * @returns
  67. */
  68. async findById(id, txn) {
  69. const { User } = this.server.models()
  70. return await User.query(txn)
  71. .throwIfNotFound()
  72. .first()
  73. .where({ user_id: id })
  74. }
  75. /**
  76. * Use knew to find first user with username
  77. * @param {*} username
  78. * @param {*} txn
  79. * @returns
  80. */
  81. async findByUsername(username, txn) {
  82. const { User } = this.server.models()
  83. return await User.query(txn)
  84. .throwIfNotFound()
  85. .first()
  86. .where({ user_name: username })
  87. }
  88. /**
  89. * Use to find first user with useremail
  90. * @param {*} username
  91. * @param {*} txn
  92. * @returns
  93. */
  94. async findByUserEmail(userEmail, txn) {
  95. const { User } = this.server.models()
  96. const user = await User.query(txn)
  97. .throwIfNotFound()
  98. .first()
  99. .where({ user_email: userEmail })
  100. return user
  101. }
  102. hashToken(token) {
  103. const salt = process.env.APP_SESSION_SALT
  104. try {
  105. return crypto.createHmac('sha256', salt).update(token).digest('hex')
  106. } catch (err) {
  107. throw new Error(err.message)
  108. }
  109. }
  110. /**
  111. * Signup function
  112. * @param {*} param0
  113. * @param {*} txn
  114. * @returns
  115. */
  116. async signup({ password, userInfo, created_at }, txn) {
  117. const { User, Auth } = this.server.models()
  118. const matchingEmails = await User.query().where(
  119. 'user_email',
  120. userInfo.user_email,
  121. )
  122. if (matchingEmails.length > 0) {
  123. throw `User ${userInfo.user_email} already exists: Cannot create a user without a unique email`
  124. }
  125. // Insert User Info to User table
  126. const insertUser = await User.query().insert(userInfo)
  127. // insert a row with blank password to be updated by changePassword()
  128. await Auth.query().insert({
  129. user_email: insertUser.user_email,
  130. created_at: created_at,
  131. token: null,
  132. })
  133. // update null token with hashed password
  134. await this.changePassword(insertUser.user_email, password, txn)
  135. return {
  136. user_id: insertUser.id,
  137. user_name: insertUser.user_name,
  138. user_email: insertUser.user_email,
  139. is_poster: insertUser.is_poster,
  140. is_admin: insertUser.is_admin,
  141. is_verified: insertUser.is_verified,
  142. }
  143. }
  144. /**
  145. * Updates user's info
  146. * @param {number} id
  147. * @param {*} param1
  148. * @param {*} txn
  149. * @returns
  150. */
  151. async update(id, { password, ...userInfo }, txn) {
  152. const { User } = this.server.models()
  153. if (Object.keys(userInfo).length > 0) {
  154. await User.query(txn)
  155. .throwIfNotFound()
  156. .where({ id })
  157. .patch(userInfo)
  158. }
  159. if (password) {
  160. await this.changePassword(id, password, txn)
  161. }
  162. return id
  163. }
  164. /**
  165. * Self explanatory
  166. * @param {*} param0
  167. * @param {*} txn
  168. * @returns
  169. */
  170. async login({ email, password }, txn) {
  171. const { User, Auth } = this.server.models()
  172. const user = await Auth.query(txn)
  173. .throwIfNotFound()
  174. .first()
  175. .where({ user_email: email })
  176. const bufferPepper = Buffer.from(process.env.PEPPER + password)
  177. /** Uncomment to run password check using SecurePassword */
  178. const passwordCheck = await this.pwd.verify(bufferPepper, user.token)
  179. if (passwordCheck === SecurePassword.VALID_NEEDS_REHASH) {
  180. await this.changePassword(user.user_email, password, txn)
  181. } else if (passwordCheck !== SecurePassword.VALID) {
  182. throw User.createNotFoundError()
  183. }
  184. return user
  185. }
  186. /**
  187. * Create a token to be sent in request headers
  188. * @param {data, expiration}
  189. * @returns {Token}
  190. */
  191. createToken(data, expiration = 600) {
  192. const key = this.server.registrations['main-app-plugin'].options.jwtKey
  193. const obj = {}
  194. Object.assign(obj, { ...data })
  195. return JWT.sign(obj, key, { expiresIn: expiration })
  196. }
  197. async makeUserCredentials(email) {
  198. const user = await this.findByUserEmail(email)
  199. const userCredentials = {
  200. email: user.user_email,
  201. name: user.user_name,
  202. seeking: user.is_poster === 1 ? 'poster' : 'seeker',
  203. }
  204. const token = this.createToken({
  205. payload: userCredentials,
  206. })
  207. return {
  208. userCredentials,
  209. token,
  210. }
  211. }
  212. /**
  213. * Validates whether a token has expired or not
  214. * @param {User} user
  215. * @returns {Token}
  216. */
  217. validateToken(token) {
  218. const key = this.server.registrations['main-app-plugin'].options.jwtKey
  219. try {
  220. return JWT.verify(token, key)
  221. } catch (err) {
  222. return { payload: null, message: err.message }
  223. }
  224. }
  225. /**
  226. * Uses this.validateToken() to verify hashedSessionToken's
  227. * existence, expiry, and also valdiates accessToken
  228. * @param {HashedSessionToken} hashedSessionToken
  229. * @returns {PayloadFromActiveSessions}
  230. */
  231. validateSession(hashedSessionToken) {
  232. if (!hashedSessionToken) {
  233. throw new Error('hashedSessionToken not passed!')
  234. }
  235. const userSession = this.activeSessions[hashedSessionToken]
  236. if (!userSession) {
  237. throw new Error(
  238. 'hashedSessionToken not in activeSessions registry!',
  239. )
  240. }
  241. if (!userSession.emailWasRespondedTo) {
  242. throw new Error('email was never responded to!')
  243. }
  244. const sessionToken = userSession.sessionToken
  245. if (!sessionToken) {
  246. throw new Error('No session token in userSession')
  247. }
  248. const sessionTokenIsValid = this.validateToken(sessionToken)
  249. return {
  250. ...sessionTokenIsValid.payload,
  251. sessionToken,
  252. email: this.activeSessions[hashedSessionToken].email,
  253. }
  254. }
  255. removeSession(hashedSessionToken) {
  256. const userSession = this.activeSessions[hashedSessionToken]
  257. if (!userSession) {
  258. throw new Error(
  259. 'hashedSessionToken not in activeSessions registry!',
  260. )
  261. } else {
  262. delete this.activeSessions[hashedSessionToken]
  263. }
  264. }
  265. /**
  266. * Use knex to try to change password entry
  267. * @param {number} id
  268. * @param {string} password
  269. * @param {*} txn
  270. * @returns {number}
  271. */
  272. async changePassword(email, password, txn) {
  273. const { Auth } = this.server.models()
  274. const hashed = await this.pwd.hash(
  275. Buffer.from(process.env.PEPPER + password),
  276. )
  277. await Auth.query(txn)
  278. .throwIfNotFound()
  279. .where({ user_email: email })
  280. .patch({
  281. // user_email: email,
  282. token: hashed,
  283. })
  284. return email
  285. }
  286. async getPassword(email, txn) {
  287. const { Auth } = this.server.models()
  288. const passwordRow = await Auth.query(txn)
  289. .where('user_email', email)
  290. .first()
  291. return passwordRow ? passwordRow.token : null
  292. }
  293. /**
  294. * Sends a Transactional Email via Brevo
  295. * @ returns {Object}
  296. */
  297. async emailSent(userCredentials) {
  298. const hashedSessionToken = this.hashToken(userCredentials.sessionToken)
  299. if (Object.keys(this.activeSessions).includes(hashedSessionToken)) {
  300. return new Error('session already in cache!!')
  301. }
  302. // Set expiration time for ten minutes from now
  303. const duration = 600000
  304. this.activeSessions[hashedSessionToken] = {
  305. email: userCredentials.email,
  306. name: userCredentials.name,
  307. seeking: userCredentials.seeking,
  308. sessionToken: userCredentials.sessionToken,
  309. expiration: Date.now() + duration,
  310. emailWasRespondedTo: false,
  311. accessToken: null,
  312. }
  313. // NOTE: Although this looks messy, Brevo requries these
  314. // parameters be defined individually like this, attempts
  315. // to configure this in a singel object cause errors on their API
  316. sendSmtpEmail.sender = {
  317. name: 'My Test Company',
  318. email: 'mytestemail@email.com',
  319. }
  320. sendSmtpEmail.subject = 'My Test Company'
  321. sendSmtpEmail.to = [
  322. {
  323. email: userCredentials.email,
  324. },
  325. ]
  326. sendSmtpEmail.templateId = Number(process.env.BREVO_TEMPLATE_ID)
  327. sendSmtpEmail.params = {
  328. link: `${process.env.BREVO_LINK}/verify/${hashedSessionToken}`,
  329. }
  330. return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
  331. data => {
  332. return { wasSuccessfull: true, data: data }
  333. },
  334. error => {
  335. return { wasSuccessfull: false, error: error }
  336. },
  337. )
  338. }
  339. }