| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- 'use strict'
- require('dotenv').config()
- const crypto = require('crypto')
- const Util = require('util')
- const JWT = require('jsonwebtoken')
- const Schmervice = require('@hapipal/schmervice')
- const SecurePassword = require('secure-password')
-
- // Configuration for Brevo
- const Brevo = require('@getbrevo/brevo')
- const apiInstance = new Brevo.TransactionalEmailsApi()
- const apiKey = apiInstance.apiClient.authentications['api-key']
- apiKey.apiKey = process.env.BREVO_KEY
- const sendSmtpEmail = new Brevo.SendSmtpEmail()
-
- // TODO: Consider implementing, nice use of SecurePassword,
- // but currently not used anywhere...
- const hasher = async (pwd, steak) => {
- const hash = await pwd.hash(steak)
- const result = await pwd.verify(steak, hash)
- let squirtle = null
-
- switch (result) {
- case SecurePassword.INVALID_UNRECOGNIZED_HASH:
- return console.error(
- 'This hash was not made with secure-password. Attempt legacy algorithm',
- )
- case SecurePassword.INVALID:
- return console.log('Invalid password')
- case SecurePassword.VALID:
- return result
- case SecurePassword.VALID_NEEDS_REHASH:
- console.log('Yay you made it, wait for us to improve your safety')
- try {
- squirtle = await pwd.hash(steak)
- // console.log('improvedHash', squirtle)
- // const saveHash = Auth.insert({user_email:
- // matchingEmails}).into('token')
- return squirtle
- } catch (err) {
- console.error(
- 'You are authenticated, but we could not improve your safety this time around',
- )
- }
- break
- }
- }
-
- /** Class for methods used in the User plugin */
- module.exports = class UserService extends Schmervice.Service {
- /**
- * Unsure of what our constructor does
- * @param {...any} args
- */
- constructor(...args) {
- super(...args)
- const pwd = new SecurePassword()
- // TODO: Invalidate this application state somehow after a
- // certain time period has passed
- this.activeSessions = {}
-
- this.pwd = {
- hash: Util.promisify(pwd.hash.bind(pwd)),
- verify: Util.promisify(pwd.verify.bind(pwd)),
- }
- }
-
- /**
- * Use knex to find users with id column
- * @param {number} id
- * @param {*} txn
- * @returns
- */
- async findById(id, txn) {
- const { User } = this.server.models()
-
- return await User.query(txn)
- .throwIfNotFound()
- .first()
- .where({ user_id: id })
- }
-
- /**
- * Use knew to find first user with username
- * @param {*} username
- * @param {*} txn
- * @returns
- */
- async findByUsername(username, txn) {
- const { User } = this.server.models()
-
- return await User.query(txn)
- .throwIfNotFound()
- .first()
- .where({ user_name: username })
- }
-
- /**
- * Use to find first user with useremail
- * @param {*} username
- * @param {*} txn
- * @returns
- */
- async findByUserEmail(userEmail, txn) {
- const { User } = this.server.models()
- const user = await User.query(txn)
- .throwIfNotFound()
- .first()
- .where({ user_email: userEmail })
- return user
- }
-
- hashToken(token) {
- const salt = process.env.APP_SESSION_SALT
- try {
- return crypto.createHmac('sha256', salt).update(token).digest('hex')
- } catch (err) {
- throw new Error(err.message)
- }
- }
-
- /**
- * Signup function
- * @param {*} param0
- * @param {*} txn
- * @returns
- */
- async signup({ password, userInfo, created_at }, txn) {
- const { User, Auth } = this.server.models()
- const matchingEmails = await User.query().where(
- 'user_email',
- userInfo.user_email,
- )
- if (matchingEmails.length > 0) {
- throw `User ${userInfo.user_email} already exists: Cannot create a user without a unique email`
- }
- // Insert User Info to User table
- const insertUser = await User.query().insert(userInfo)
- // insert a row with blank password to be updated by changePassword()
- await Auth.query().insert({
- user_email: insertUser.user_email,
- created_at: created_at,
- token: null,
- })
- // update null token with hashed password
- await this.changePassword(insertUser.user_email, password, txn)
- return {
- user_id: insertUser.id,
- user_name: insertUser.user_name,
- user_email: insertUser.user_email,
- is_poster: insertUser.is_poster,
- is_admin: insertUser.is_admin,
- is_verified: insertUser.is_verified,
- }
- }
-
- /**
- * Updates user's info
- * @param {number} id
- * @param {*} param1
- * @param {*} txn
- * @returns
- */
- async update(id, { password, ...userInfo }, txn) {
- const { User } = this.server.models()
-
- if (Object.keys(userInfo).length > 0) {
- await User.query(txn)
- .throwIfNotFound()
- .where({ id })
- .patch(userInfo)
- }
-
- if (password) {
- await this.changePassword(id, password, txn)
- }
-
- return id
- }
-
- /**
- * Self explanatory
- * @param {*} param0
- * @param {*} txn
- * @returns
- */
- async login({ email, password }, txn) {
- const { User, Auth } = this.server.models()
-
- const user = await Auth.query(txn)
- .throwIfNotFound()
- .first()
- .where({ user_email: email })
- const bufferPepper = Buffer.from(process.env.PEPPER + password)
-
- /** Uncomment to run password check using SecurePassword */
- const passwordCheck = await this.pwd.verify(bufferPepper, user.token)
-
- if (passwordCheck === SecurePassword.VALID_NEEDS_REHASH) {
- await this.changePassword(user.user_email, password, txn)
- } else if (passwordCheck !== SecurePassword.VALID) {
- throw User.createNotFoundError()
- }
-
- return user
- }
-
- /**
- * Create a token to be sent in request headers
- * @param {data, expiration}
- * @returns {Token}
- */
- createToken(data, expiration = 600) {
- const key = process.env.APP_SECRET
- const obj = {}
- Object.assign(obj, { ...data })
- return JWT.sign(obj, key, { expiresIn: expiration })
- }
-
- async makeUserCredentials(email) {
- const user = await this.findByUserEmail(email)
- const userCredentials = {
- email: user.user_email,
- name: user.user_name,
- seeking: user.is_poster === 1 ? 'poster' : 'seeker',
- }
- const token = this.createToken({
- payload: userCredentials,
- })
- return {
- userCredentials,
- token,
- }
- }
-
- /**
- * Validates whether a token has expired or not
- * @param {User} user
- * @returns {Token}
- */
- validateToken(token) {
- const key = process.env.APP_SECRET
- try {
- return JWT.verify(token, key)
- } catch (err) {
- return { payload: null, message: err.message }
- }
- }
- removeSession(hashedSessionToken) {
- const userSession = this.activeSessions[hashedSessionToken]
- if (!userSession) {
- throw new Error(
- 'hashedSessionToken not in activeSessions registry!',
- )
- } else {
- delete this.activeSessions[hashedSessionToken]
- }
- }
- /**
- * Use knex to try to change password entry
- * @param {number} id
- * @param {string} password
- * @param {*} txn
- * @returns {number}
- */
- async changePassword(email, password, txn) {
- const { Auth } = this.server.models()
-
- const hashed = await this.pwd.hash(
- Buffer.from(process.env.PEPPER + password),
- )
-
- await Auth.query(txn)
- .throwIfNotFound()
- .where({ user_email: email })
- .patch({
- // user_email: email,
- token: hashed,
- })
- return email
- }
-
- async getPassword(email, txn) {
- const { Auth } = this.server.models()
-
- const passwordRow = await Auth.query(txn)
- .where('user_email', email)
- .first()
- return passwordRow ? passwordRow.token : null
- }
-
- /**
- * Sends a Transactional Email via Brevo
- * @ returns {Object}
- */
- async emailSent(userCredentials) {
- const hashedSessionToken = this.hashToken(userCredentials.sessionToken)
- if (Object.keys(this.activeSessions).includes(hashedSessionToken)) {
- return new Error('session already in cache!!')
- }
- // Set expiration time for ten minutes from now
- const duration = 600000
-
- this.activeSessions[hashedSessionToken] = {
- email: userCredentials.email,
- name: userCredentials.name,
- seeking: userCredentials.seeking,
- sessionToken: userCredentials.sessionToken,
- expiration: Date.now() + duration,
- emailWasRespondedTo: false,
- accessToken: null,
- }
- // NOTE: Although this looks messy, Brevo requries these
- // parameters be defined individually like this, attempts
- // to configure this in a singel object cause errors on their API
- sendSmtpEmail.sender = {
- name: 'My Test Company',
- email: 'mytestemail@email.com',
- }
- sendSmtpEmail.subject = 'My Test Company'
- sendSmtpEmail.to = [
- {
- email: userCredentials.email,
- },
- ]
- sendSmtpEmail.templateId = Number(process.env.BREVO_TEMPLATE_ID)
- sendSmtpEmail.params = {
- link: `${process.env.BREVO_LINK}/verify/${hashedSessionToken}`,
- }
- return await apiInstance.sendTransacEmail(sendSmtpEmail).then(
- data => {
- return { wasSuccessfull: true, data: data }
- },
- error => {
- return { wasSuccessfull: false, error: error }
- },
- )
- }
- }
|