Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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 SibApiV3Sdk = require('sib-api-v3-sdk')
  10. const { access, accessSync } = require('fs')
  11. const defaultClient = SibApiV3Sdk.ApiClient.instance
  12. const apiKey = defaultClient.authentications['api-key']
  13. apiKey.apiKey = process.env.BREVO_KEY
  14. const apiInstance = new SibApiV3Sdk.TransactionalEmailsApi()
  15. const hashToken = async token => {
  16. // Give it a .env file phrase, NOT RANDOM
  17. const salt = crypto.randomBytes(16).toString('base64')
  18. // const salt = process.env.salt
  19. try {
  20. return crypto.createHmac('sha256', salt).update(token).digest('hex')
  21. } catch (err) {
  22. // console.error('ERROR :=>', err)
  23. throw new Error(err.message)
  24. }
  25. }
  26. const hasher = async (pwd, steak) => {
  27. const hash = await pwd.hash(steak)
  28. const result = await pwd.verify(steak, hash)
  29. let squirtle = null
  30. switch (result) {
  31. case SecurePassword.INVALID_UNRECOGNIZED_HASH:
  32. return console.error(
  33. 'This hash was not made with secure-password. Attempt legacy algorithm',
  34. )
  35. case SecurePassword.INVALID:
  36. return console.log('Invalid password')
  37. case SecurePassword.VALID:
  38. return result
  39. case SecurePassword.VALID_NEEDS_REHASH:
  40. console.log('Yay you made it, wait for us to improve your safety')
  41. try {
  42. squirtle = await pwd.hash(steak)
  43. // console.log('improvedHash', squirtle)
  44. // const saveHash = Auth.insert({user_email:
  45. // matchingEmails}).into('token')
  46. return squirtle
  47. } catch (err) {
  48. console.error(
  49. 'You are authenticated, but we could not improve your safety this time around',
  50. )
  51. }
  52. break
  53. }
  54. }
  55. /** Class for methods used in the User plugin */
  56. module.exports = class UserService extends Schmervice.Service {
  57. /**
  58. * Unsure of what our constructor does
  59. * @param {...any} args
  60. */
  61. constructor(...args) {
  62. super(...args)
  63. const pwd = new SecurePassword()
  64. // TODO: Invalidate this application state somehow after a
  65. // certain time period has passed
  66. this.activeSessions = {
  67. // abc123456: '123456689',
  68. // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
  69. // email: rawEmailString,
  70. // name: 'Joe Doe',
  71. // seeking: 'candidate'
  72. // sessionToken: rawSessionToken, // use for expires instead of expires?
  73. // expires: expirationTime in seconds
  74. // }
  75. }
  76. // Check the hashedCookie which is our hashedSessionToken string
  77. // validate whether or not the rawAccessToken is still valid, if valid good to go.
  78. // if NOT valid, then we need to reassign accessToken to a newAccessToken
  79. // this.activeSessions = {
  80. // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: {
  81. // accessToken: 'as;dflkja;;dlfkja;sldkf... rawAccessToken'
  82. // }
  83. // }
  84. this.pwd = {
  85. hash: Util.promisify(pwd.hash.bind(pwd)),
  86. verify: Util.promisify(pwd.verify.bind(pwd)),
  87. }
  88. }
  89. /**
  90. * Use knex to find users with id column
  91. * @param {number} id
  92. * @param {*} txn
  93. * @returns
  94. */
  95. async findById(id, txn) {
  96. const { User } = this.server.models()
  97. return await User.query(txn)
  98. .throwIfNotFound()
  99. .first()
  100. .where({ user_id: id })
  101. }
  102. /**
  103. * Use knew to find first user with username
  104. * @param {*} username
  105. * @param {*} txn
  106. * @returns
  107. */
  108. async findByUsername(username, txn) {
  109. const { User } = this.server.models()
  110. return await User.query(txn)
  111. .throwIfNotFound()
  112. .first()
  113. .where({ user_name: username })
  114. }
  115. /**
  116. * Use to find first user with useremail
  117. * @param {*} username
  118. * @param {*} txn
  119. * @returns
  120. */
  121. async findByUserEmail(userEmail, txn) {
  122. const { User } = this.server.models()
  123. const user = await User.query(txn)
  124. .throwIfNotFound()
  125. .first()
  126. .where({ user_email: userEmail })
  127. return user
  128. }
  129. /**
  130. * Signup function
  131. * @param {*} param0
  132. * @param {*} txn
  133. * @returns
  134. */
  135. async signup({ password, userInfo, created_at }, txn) {
  136. const { User, Auth } = this.server.models()
  137. const matchingEmails = await User.query().where(
  138. 'user_email',
  139. userInfo.user_email,
  140. )
  141. if (matchingEmails.length > 0) {
  142. throw `User ${userInfo.user_email} already exists: Cannot create a user without a unique email`
  143. }
  144. // Insert User Info to User table
  145. const insertUser = await User.query().insert(userInfo)
  146. // insert a row with blank password to be updated by changePassword()
  147. await Auth.query().insert({
  148. user_email: insertUser.user_email,
  149. created_at: created_at,
  150. token: null,
  151. })
  152. // update null token with hashed password
  153. await this.changePassword(insertUser.user_email, password, txn)
  154. return {
  155. user_id: insertUser.id,
  156. user_name: insertUser.user_name,
  157. user_email: insertUser.user_email,
  158. is_poster: insertUser.is_poster,
  159. is_admin: insertUser.is_admin,
  160. is_verified: insertUser.is_verified,
  161. }
  162. }
  163. /**
  164. * Updates user's info
  165. * @param {number} id
  166. * @param {*} param1
  167. * @param {*} txn
  168. * @returns
  169. */
  170. async update(id, { password, ...userInfo }, txn) {
  171. const { User } = this.server.models()
  172. if (Object.keys(userInfo).length > 0) {
  173. await User.query(txn)
  174. .throwIfNotFound()
  175. .where({ id })
  176. .patch(userInfo)
  177. }
  178. if (password) {
  179. await this.changePassword(id, password, txn)
  180. }
  181. return id
  182. }
  183. /**
  184. * Self explanatory
  185. * @param {*} param0
  186. * @param {*} txn
  187. * @returns
  188. */
  189. async login({ email, password }, txn) {
  190. const { User, Auth } = this.server.models()
  191. const user = await Auth.query(txn)
  192. .throwIfNotFound()
  193. .first()
  194. .where({ user_email: email })
  195. const bufferPepper = Buffer.from(process.env.PEPPER + password)
  196. /** Uncomment to run password check using SecurePassword */
  197. const passwordCheck = await this.pwd.verify(bufferPepper, user.token)
  198. if (passwordCheck === SecurePassword.VALID_NEEDS_REHASH) {
  199. await this.changePassword(user.user_email, password, txn)
  200. } else if (passwordCheck !== SecurePassword.VALID) {
  201. throw User.createNotFoundError()
  202. }
  203. return user
  204. }
  205. /**
  206. * Create a token to be sent in request headers
  207. * @param {data, expiration}
  208. * @returns {Token}
  209. */
  210. createToken(data, expiration = 600) {
  211. const key = this.server.registrations['main-app-plugin'].options.jwtKey
  212. const obj = {}
  213. Object.assign(obj, { ...data })
  214. return JWT.sign(obj, key, { expiresIn: expiration })
  215. }
  216. /**
  217. * Validates whether a token has expired or not
  218. * @param {User} user
  219. * @returns {Token}
  220. */
  221. validateToken(token) {
  222. const key = this.server.registrations['main-app-plugin'].options.jwtKey
  223. try {
  224. return JWT.verify(token, key)
  225. } catch (err) {
  226. return { payload: null, message: err.message }
  227. }
  228. }
  229. /**
  230. * Uses this.validateToken() to verify hashedSessionToken's
  231. * existence, expiry, and also valdiates accessToken
  232. * @param {User} user
  233. * @returns {Token}
  234. */
  235. // TODO: remove testing console.log() messages once onboarding auth is working
  236. // REFACTOR: Have this function only do one thing (UNIX philsophy)
  237. validateSession(hashedSessionToken) {
  238. console.log('this.activeSessions :=>', this.activeSessions)
  239. if (!this.activeSessions[hashedSessionToken]) {
  240. throw new Error(
  241. 'hashedSessionToken not in activeSessions registry!',
  242. )
  243. }
  244. // BREAK OUT INTO ANOTHER FUNC
  245. const rawSessionToken =
  246. this.activeSessions[hashedSessionToken].sessionToken
  247. const accessToken = this.activeSessions[hashedSessionToken].accessToken
  248. // Weird Edge case...
  249. if (!rawSessionToken) {
  250. throw new Error(
  251. 'hashedSessionToken is in activeSessions registry, but rawSessionToken does not exist',
  252. )
  253. }
  254. // ANOTHER FUNC HERE
  255. const sessionTokenIsValid = this.validateToken(rawSessionToken)
  256. console.log('sessionTokenIsValid :=>', sessionTokenIsValid)
  257. const accessTokenIsValid = this.validateToken(accessToken)
  258. console.log('accessTokenIsValid :=>', accessTokenIsValid)
  259. // Both sessionToken and accessToken are expired
  260. // createAccessToken()
  261. //
  262. if (!accessTokenIsValid.payload) {
  263. console.log(
  264. 'sessionToken is valid, but accessToken is null or is expired :=>',
  265. )
  266. const accessToken = this.createToken({
  267. payload: sessionTokenIsValid.payload,
  268. })
  269. this.activeSessions[hashedSessionToken].accessToken = accessToken
  270. }
  271. return {
  272. ...sessionTokenIsValid.payload,
  273. sessionToken: this.activeSessions[hashedSessionToken].sessionToken,
  274. }
  275. }
  276. /**
  277. * Use knex to try to change password entry
  278. * @param {number} id
  279. * @param {string} password
  280. * @param {*} txn
  281. * @returns {number}
  282. */
  283. async changePassword(email, password, txn) {
  284. const { Auth } = this.server.models()
  285. const hashed = await this.pwd.hash(
  286. Buffer.from(process.env.PEPPER + password),
  287. )
  288. await Auth.query(txn)
  289. .throwIfNotFound()
  290. .where({ user_email: email })
  291. .patch({
  292. // user_email: email,
  293. token: hashed,
  294. })
  295. return email
  296. }
  297. async getPassword(email, txn) {
  298. const { Auth } = this.server.models()
  299. const passwordRow = await Auth.query(txn)
  300. .where('user_email', email)
  301. .first()
  302. return passwordRow ? passwordRow.token : null
  303. }
  304. /**
  305. * Sends a Transactional Email via Brevo
  306. * @ returns {Object}
  307. */
  308. async emailSent(userCredentials) {
  309. const hashedSessionToken = await hashToken(userCredentials.sessionToken)
  310. if (Object.keys(this.activeSessions).includes(hashedSessionToken)) {
  311. return new Error('session already in cache!!')
  312. }
  313. // Set expiration time for ten minutes from now
  314. const duration = 600000
  315. this.activeSessions[hashedSessionToken] = {
  316. email: userCredentials.email,
  317. name: userCredentials.name,
  318. seeking: userCredentials.seeking,
  319. sessionToken: userCredentials.sessionToken,
  320. expiration: Date.now() + duration,
  321. accessToken: null,
  322. }
  323. const sendSmtpEmail = {
  324. to: [
  325. {
  326. email: userCredentials.email,
  327. },
  328. ],
  329. templateId: 1,
  330. params: {
  331. // TODO: Change this in production...
  332. link: `localhost:3000/verify/${hashedSessionToken}`,
  333. },
  334. }
  335. return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
  336. data => {
  337. return { wasSuccessfull: true, data: data }
  338. },
  339. error => {
  340. return { wasSuccessfull: false, error: error }
  341. },
  342. )
  343. }
  344. }