'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 SibApiV3Sdk = require('sib-api-v3-sdk') const { access, accessSync } = require('fs') const defaultClient = SibApiV3Sdk.ApiClient.instance const apiKey = defaultClient.authentications['api-key'] apiKey.apiKey = process.env.BREVO_KEY const apiInstance = new SibApiV3Sdk.TransactionalEmailsApi() const hashToken = async token => { const salt = process.env.APP_SESSION_SALT try { return crypto.createHmac('sha256', salt).update(token).digest('hex') } catch (err) { // console.error('ERROR :=>', err) throw new Error(err.message) } } 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 = { // abc123456: '123456689', // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: { // email: rawEmailString, // name: 'Joe Doe', // seeking: 'candidate' // sessionToken: rawSessionToken, // use for expires instead of expires? // expires: expirationTime in seconds // } } // Check the hashedCookie which is our hashedSessionToken string // validate whether or not the rawAccessToken is still valid, if valid good to go. // if NOT valid, then we need to reassign accessToken to a newAccessToken // this.activeSessions = { // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...hashedSessionToken: { // accessToken: 'as;dflkja;;dlfkja;sldkf... rawAccessToken' // } // } 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 } /** * 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 = this.server.registrations['main-app-plugin'].options.jwtKey const obj = {} Object.assign(obj, { ...data }) return JWT.sign(obj, key, { expiresIn: expiration }) } /** * Validates whether a token has expired or not * @param {User} user * @returns {Token} */ validateToken(token) { const key = this.server.registrations['main-app-plugin'].options.jwtKey try { return JWT.verify(token, key) } catch (err) { return { payload: null, message: err.message } } } /* * Grabs the sessionToken and accessToken from the * this.activeSessions object based off of provided hashedToken * @params {UserSession} * @returns {grabTokensFromActiveSession} */ _grabTokensFromActiveSessions(userSession) { const rawSessionToken = userSession.sessionToken const accessToken = userSession.accessToken return { rawSessionToken: rawSessionToken, accessToken: accessToken } } /** * Helper function to validate both tokens grabbed from this.activeSessions * @params {Tokens} * @returns {ValidatedTokens} */ _validateTokens(tokens) { const sessionTokenIsValid = this.validateToken(tokens.rawSessionToken) const accessTokenIsValid = this.validateToken(tokens.accessToken) return { sessionTokenIsValid: sessionTokenIsValid, accessTokenIsValid: accessTokenIsValid, } } /** * Checks to see if the activeSession accessToken is expired * If it is, it creates a new one and stores it in activeSession * @ params {UserSession} {ValidatedTokens} * @returns Void */ _createAccessTokenIfExpired(userSession, validatedTokens) { if (!validatedTokens.accessTokenIsValid.payload) { const accessToken = this.createToken({ payload: validatedTokens.sessionTokenIsValid.payload, }) userSession.accessToken = accessToken } } /** * Uses this.validateToken() to verify hashedSessionToken's * existence, expiry, and also valdiates accessToken * @param {HashedSessionToken} hashedSessionToken * @returns {PayloadFromActiveSessions} */ validateSession(hashedSessionToken) { const userSession = this.activeSessions[hashedSessionToken] if (!userSession) { throw new Error( 'hashedSessionToken not in activeSessions registry!', ) } const tokens = this._grabTokensFromActiveSessions(userSession) const validatedTokens = this._validateTokens(tokens) this._createAccessTokenIfExpired(userSession, validatedTokens) return { ...validatedTokens.sessionTokenIsValid.payload, // sessionToken: this.activeSessions[hashedSessionToken].sessionToken, // NOTE: this won't work as the jwt auth strategy needs a raw JWT string sessionToken: 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 = await 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, accessToken: null, } const sendSmtpEmail = { to: [ { email: userCredentials.email, }, ], templateId: 1, params: { // TODO: Change this in production... link: `localhost:3000/verify/${hashedSessionToken}`, }, } return await apiInstance.sendTransacEmail(sendSmtpEmail).then( data => { return { wasSuccessfull: true, data: data } }, error => { return { wasSuccessfull: false, error: error } }, ) } }