From 040b96772518d309a4a6a253a356b43a488b50a5 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Thu, 3 Oct 2019 13:04:17 -0600 Subject: [PATCH] Add create and confirm account emails in backend --- server/config.example.json | 3 + server/controllers/account.js | 46 +++++++ server/index.js | 2 +- server/routes/account.js | 112 +++++++++++++++++- server/templates/email.confirm_account.txt | 10 ++ .../email.confirm_account_thanks.txt | 10 ++ 6 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 server/templates/email.confirm_account.txt create mode 100644 server/templates/email.confirm_account_thanks.txt diff --git a/server/config.example.json b/server/config.example.json index 2376ae7..05a7c5d 100644 --- a/server/config.example.json +++ b/server/config.example.json @@ -1,4 +1,5 @@ { + "domain": "localhost", "port": 3000, "db_engine": "postgres", "sqlite_location": "./database.sqlite", @@ -11,6 +12,8 @@ "email_port": 465, "email_username": null, "email_password": "password", + "email_from_name": "Readlebee Admin", + "email_from_address": null, "jwtSecretKey": "SomethingAtLeast32CharactersLong!", "tokenExpireDays": 7, "inventaireDomain": "https://inventaire.io" diff --git a/server/controllers/account.js b/server/controllers/account.js index 76e5946..11b51b0 100644 --- a/server/controllers/account.js +++ b/server/controllers/account.js @@ -58,6 +58,19 @@ class Account { } } + static confirmAccountDataIsValid(createAccountData) { + if (typeof createAccountData.id === 'undefined' + || typeof createAccountData.confirm === 'undefined' + || !createAccountData.confirm) { + return { + error: true, + message: 'api.account_confirm_required_data_missing', + }; + } + + return true; + } + async emailExists (email) { const existingUser = await this.model.find({ attributes: ['id'], @@ -109,6 +122,39 @@ class Account { accountConfirm: needsConfirmation ? crypto.randomBytes(32).toString('hex') : null, }); } + + async confirmUser (id, accountConfirm) { + const userToConfirm = await this.model.findOne({ + where: { + id, + accountConfirm, + }, + }); + + if (!userToConfirm) { + return { + error: true, + message: 'api.account_confirm_invalid_code', + } + } + + return await this.model.update({ + accountConfirm: null, + }, { + where: { + id, + accountConfirm, + }, + }).then(success => { + if (success[0] < 1) { + return { + error: true, + message: 'api.account_confirm_update_fail', + } + } + return userToConfirm; + }); + } } diff --git a/server/index.js b/server/index.js index 4b9a058..c0f7a34 100644 --- a/server/index.js +++ b/server/index.js @@ -33,7 +33,7 @@ const sequelizeConfig = { switch (fastify.siteConfig.db_engine) { case 'sqlite': { sequelizeConfig.storage = typeof fastify.siteConfig.sqlite_location !== 'undefined' - ? path.resolve(fastify.siteConfig.sqlite_location) + ? path.resolve(__dirname, fastify.siteConfig.sqlite_location) : path.resolve(__dirname, './database.sqlite'); break; } diff --git a/server/routes/account.js b/server/routes/account.js index 8336665..51fcd93 100644 --- a/server/routes/account.js +++ b/server/routes/account.js @@ -1,3 +1,5 @@ +const fs = require('fs'); +const path = require('path'); const Account = require('../controllers/account'); async function routes(fastify, options) { @@ -27,20 +29,56 @@ async function routes(fastify, options) { return reply.code(400).send(canCreateUser); } - const result = await account.createUser(formData.email, formData.username, formData.displayName, formData.password, fastify.canEmail); + const newUser = await account.createUser(formData.email, formData.username, formData.displayName, formData.password, fastify.canEmail); - if (typeof result.error !== 'undefined') { - return reply.code(400).send(result); + if (typeof newUser.error !== 'undefined') { + return reply.code(400).send(newUser); } if (fastify.canEmail) { - // fastify.nodemailer.sendMail(); - return reply.send({ + try { + const file = fs.readFileSync(path.resolve(__dirname, '../templates/email.confirm_account.txt')); + console.log(file.toString()); + const text = file.toString() + .replace(/\{display_name\}/g, newUser.displayName) + .replace(/\{username\}/g, newUser.username) + .replace(/\{domain\}/g, fastify.siteConfig.domain) + .replace(/\{id\}/g, newUser.id) + .replace(/\{code\}/g, newUser.accountConfirm) + .replace(/\{sender\}/g, fastify.siteConfig.email_from_name); + + return fastify.nodemailer.sendMail({ + // Default to email_username if email_from_address is null/falsy + from: `"${fastify.siteConfig.email_from_name}" ${!fastify.siteConfig.email_from_address ? fastify.siteConfig.email_username : fastify.siteConfig.email_from_address}`, + to: `"${newUser.displayName}" ${newUser.email}`, + subject: 'Please Confirm your Account', + text, + // Definitely gonna have to wait to design the HTML version of the email! + // html: '

HTML version of the message

', + }).then(email => { + if (email.err) { + console.error(email.err); + return reply.send({ + error: true, + message: 'api.account_email_send_fail', + newUser, + }); + } + + return reply.send({ + error: false, + message: 'api.account_confirm_email', + }); + }) + } catch (ex) { + console.error(ex); + return reply.send({ error: false, message: 'api.account_create_success', }); + } } else { - const token = fastify.jwt.sign({ id: result.id }); + const token = fastify.jwt.sign({ id: newUser.id }); const expireTime = fastify.siteConfig.tokenExpireDays * (24 * 60 * 60e3); // The section in parentheses is milliseconds in a day return reply @@ -58,6 +96,68 @@ async function routes(fastify, options) { } }); + fastify.post('/api/account/confirm', async (request, reply) => { + if (request.isLoggedInUser) { + return reply.code(400).send({ + error: true, + message: 'api.account_already_logged_in', + }); + } + + const formDataIsValid = Account.confirmAccountDataIsValid(request.body); + if (formDataIsValid !== true) { + return reply.code(400).send(formDataIsValid); + } + + const account = new Account(fastify.models.User); + + const confirmed = await account.confirmUser(request.body.id, request.body.confirm); + + if (typeof confirmed.error !== 'undefined') { + return reply.code(400).send(confirmed); + } + + // Expects email to be working, and indeed it should be working because that's how the confirmation code was sent. + try { + const file = fs.readFileSync(path.resolve(__dirname, '../templates/email.confirm_account_thanks.txt')); + console.log(file.toString()); + const text = file.toString() + .replace(/\{display_name\}/g, confirmed.displayName) + .replace(/\{username\}/g, confirmed.username) + .replace(/\{domain\}/g, fastify.siteConfig.domain) + .replace(/\{sender\}/g, fastify.siteConfig.email_from_name); + + return fastify.nodemailer.sendMail({ + // Default to email_username if email_from_address is null/falsy + from: `"${fastify.siteConfig.email_from_name}" ${!fastify.siteConfig.email_from_address ? fastify.siteConfig.email_username : fastify.siteConfig.email_from_address}`, + to: `"${confirmed.displayName}" ${confirmed.email}`, + subject: 'Account Confirmed Successfully', + text, + // Definitely gonna have to wait to design the HTML version of the email! + // html: '

HTML version of the message

', + }).then(email => { + if (email.err) { + console.error(email.err); + return reply.send({ + error: true, + message: 'api.account_confirm_email_send_fail', + }); + } + + return reply.send({ + error: false, + message: 'api.account_confirm_success_email', + }); + }) + } catch (ex) { + console.error(ex); + return reply.send({ + error: false, + message: 'api.account_confirm_success', + }); + } + }); + fastify.get('/api/login', async (request, reply) => { reply.view('login.hbs', { text: request.isLoggedInUser ? JSON.stringify(fastify.jwt.decode(request.cookies.token)) : 'you are NOT logged in' }); }); diff --git a/server/templates/email.confirm_account.txt b/server/templates/email.confirm_account.txt new file mode 100644 index 0000000..2d58898 --- /dev/null +++ b/server/templates/email.confirm_account.txt @@ -0,0 +1,10 @@ +Hello {display_name} (username: {username}), + +Someone used this email address to create an account at {domain}. + +If this was you, please confirm your account and log in using the link below: + +{domain}/login?id={id}&confirm={code} + +Thanks! +{sender} \ No newline at end of file diff --git a/server/templates/email.confirm_account_thanks.txt b/server/templates/email.confirm_account_thanks.txt new file mode 100644 index 0000000..eca7e18 --- /dev/null +++ b/server/templates/email.confirm_account_thanks.txt @@ -0,0 +1,10 @@ +Hello {display_name} (username: {username}), + +You have successfully confirmed your account on {domain}. + +You may now log in using your email address and password at the following address: + +{domain}/login + +Thanks! +{sender} \ No newline at end of file