From d480043b5ae51fedae110653125ae804ec2b2287 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Mon, 26 Apr 2021 15:30:14 -0700 Subject: [PATCH] Create send forgot email, route for reset on UI and reset APIs, resetting password. --- api/src/accounts/reset.js | 77 +++++++++++++++++++++++++++++++++++++++ api/src/index.js | 43 ++++++++++++++++++++++ ui/src/index.js | 2 + 3 files changed, 122 insertions(+) create mode 100644 api/src/accounts/reset.js diff --git a/api/src/accounts/reset.js b/api/src/accounts/reset.js new file mode 100644 index 0000000..39b3cf0 --- /dev/null +++ b/api/src/accounts/reset.js @@ -0,0 +1,77 @@ +import crypto from 'crypto' +const { ROOT_DOMAIN, JWT_SIGNATURE } = process.env + +function createResetToken(email, expTimestamp) { + try { + // Auth String, JWT Signature, email, expTimestamp + const authString = `${JWT_SIGNATURE}:${email}:${expTimestamp}` + return crypto.createHash('sha256').update(authString).digest('hex'); + } catch (e) { + console.log('e', e); + } +} + +function validateExpTimestamp(expTimestamp) { + // One day milliseconds + const expTime = 24 * 60 * 60 * 1000 + // Difference between now and expired time + const dateDiff = Number(expTimestamp) - Date.now() + // We're expired if not past OR difference in time is less than allowed + const isValid = dateDiff > 0 && dateDiff < expTime + return isValid +} + +export async function createResetEmailLink(email) { + try { + // Encode url string + const URIencodedEmail = encodeURIComponent(email); + + // Create timestamp + const expTimestamp = Date.now() + 24 * 60 * 60 * 1000 + + // Create token + const token = createResetToken(email, expTimestamp) + + // Link email contains user email, token, expiration date + return `https://${ROOT_DOMAIN}/reset/${URIencodedEmail}/${expTimestamp}/${token}` + } catch (e) { + console.log('e', e); + } +} + +export async function createResetLink(email) { + try { + const { user } = await import ("../user/user.js") + // Check to see if a user exists with that email + const foundUser = await user.findOne({ + 'email.address': email, + }) + // If user exists + if (foundUser) { + // Create email link + const link = await createResetEmailLink(email) + return link + } + return '' + } catch (e) { + console.log('e', e); + return false + } +} + +export async function validateResetEmail(token, email, expTimestamp) { + try { + // Create a hash aka token + const resetToken = createResetToken(email, expTimestamp) + + // Compare hash with token + const isValid = resetToken === token + + // Time is not expired + const isTimestampValid = validateExpTimestamp(expTimestamp) + return isValid && isTimestampValid + } catch (e) { + console.log('e', e) + return false + } +} \ No newline at end of file diff --git a/api/src/index.js b/api/src/index.js index 5921bb4..87633b8 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -13,6 +13,7 @@ import { logUserOut } from './accounts/logUserOut.js' import { getUserFromCookies, changePassword } from './accounts/user.js' import { sendEmail, mailInit } from './mail/index.js' import { createVerifyEmailLink, validateVerifyEmail } from './accounts/verify.js' +import { createResetLink, validateResetEmail } from './accounts/reset.js' // ESM specific "features" const __filename = fileURLToPath(import.meta.url) @@ -115,6 +116,48 @@ async function startApp() { } }) + app.post('/api/forgot-password', {}, async (request, reply) => { + try { + const { email } = request.body + const link = await createResetLink(email) + // Send email with link + if (link) { + await sendEmail({ + to: email, + subject: "Reset your password", + html: /*html*/ `Reset`, + }) + } + return reply.code(200).send() + } catch (e) { + console.error('e', e); + return reply.code(401).send() + } + }) + + app.post('/api/reset', {}, async (request, reply) => { + try { + const { email, password, token, time } = request.body + const isValid = await validateResetEmail(token, email, time) + if (isValid) { + // Find User + const { user } = await import('./user/user.js') + const foundUser = await user.findOne({ + "email.address": email, + }) + // Change password + if (foundUser._id) { + await changePassword(foundUser._id, password) + return reply.code(200).send('Password Updated') + } + } + return reply.code(401).send('Reset failed') + } catch (e) { + console.error('e', e); + return reply.code(401).send() + } + }) + app.post('/api/verify', {}, async (request, reply) => { try { const { token, email } = request.body diff --git a/ui/src/index.js b/ui/src/index.js index abdeb8d..1044620 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -17,6 +17,8 @@ async function startApp() { root: path.join(__dirname, "public"), }) + app.get('/reset/:email/:exp/:token', {}, async (request, reply) => reply.sendFile('reset.html')) + app.get('/verify/:email/:token', {}, async ( request, reply ) => { try { const { email, token } = request.params