From 72b5fc9357f0100f4cc560c1430f7ea2d849eb40 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 5 Feb 2020 22:31:16 +0200 Subject: Implement account activation --- .../20200205194947_user_add_activated_email.js | 13 +++++ .../20200205200212_add_activate_code_token_type.js | 17 ++++++ backend/src/authn.js | 60 +++++++++++++++++++--- backend/src/email.js | 20 ++++++++ backend/src/routes.js | 3 +- frontend/src/Activate.js | 36 +++++++++++++ frontend/src/App.js | 2 + frontend/src/Register.js | 4 +- 8 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 backend/migrations/20200205194947_user_add_activated_email.js create mode 100644 backend/migrations/20200205200212_add_activate_code_token_type.js create mode 100644 backend/src/email.js create mode 100644 frontend/src/Activate.js diff --git a/backend/migrations/20200205194947_user_add_activated_email.js b/backend/migrations/20200205194947_user_add_activated_email.js new file mode 100644 index 0000000..cf58c35 --- /dev/null +++ b/backend/migrations/20200205194947_user_add_activated_email.js @@ -0,0 +1,13 @@ +exports.up = function(knex) { + return knex.schema.table('users', function(table) { + table.string('email').notNullable().default(''); + table.boolean('activated').notNullable().default(false); + }); +}; + +exports.down = function(knex) { + return knex.schema.table('users', function(table) { + table.dropColumn('email'); + table.dropColumn('activated'); + }); +}; diff --git a/backend/migrations/20200205200212_add_activate_code_token_type.js b/backend/migrations/20200205200212_add_activate_code_token_type.js new file mode 100644 index 0000000..b1c264c --- /dev/null +++ b/backend/migrations/20200205200212_add_activate_code_token_type.js @@ -0,0 +1,17 @@ +exports.up = function(knex) { + return knex.schema.raw(` + ALTER TABLE "tokens" + DROP CONSTRAINT "tokens_type_check", + ADD CONSTRAINT "tokens_type_check" + CHECK (type IN ('session', 'activation')) + `); +}; + +exports.down = function(knex) { + return knex.schema.raw(` + ALTER TABLE "tokens" + DROP CONSTRAINT "tokens_type_check", + ADD CONSTRAINT "tokens_type_check" + CHECK (type IN ('session')) + `); +}; diff --git a/backend/src/authn.js b/backend/src/authn.js index 1980aed..52ba862 100644 --- a/backend/src/authn.js +++ b/backend/src/authn.js @@ -64,7 +64,7 @@ const authMiddleware = async (req, res, next) => { }; const registerRoute = async (req, res) => { - const { username, password } = req.body; + const { username, password, email } = req.body; if (!username || !password) { res.status(400); return res.json({ @@ -110,16 +110,21 @@ const registerRoute = async (req, res) => { const sessionToken = shortid.generate(); const sessionTokenHash = sha512(salt + sessionToken); - const user = (await db('users').insert({ username, password: pwHash, salt }).returning('*'))[0]; - await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); + const insertedUsers = await db('users').insert({ username, password: pwHash, email, salt }).returning('*'); + const user = insertedUsers[0]; + + const activationToken = shortid.generate(); + await db('tokens').insert({ user_id: user.id, type: 'activation', value: activationToken }); - res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember: false, isLogout: false })); + // await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); + // res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember: false, isLogout: false })); await consoleBackend.send({ recipients: [ username ], subject: `Welcome to the service, ${username}!`, - body: `Welcome.\n\nLog in at http://localhost:3000.\n\nBR,\nTeam`, + body: `Welcome.\n\nActivate your account at http://localhost:3000/activate/${activationToken}.\n\nBR,\nTeam`, }); + return res.json({ username, }); @@ -135,10 +140,17 @@ const loginRoute = async (req, res) => { } const user = await db('users').where({ username }).first(); + if (!user) { + res.status(400); + res.json({ + error: 'Wrong username or password', + }); + } + const salt = user.salt; const pwHash = sha512(salt + password); - if (!user || pwHash !== user.password) { + if (pwHash !== user.password) { res.status(400); res.json({ error: 'Wrong username or password', @@ -150,7 +162,7 @@ const loginRoute = async (req, res) => { await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); - res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember, isLogout: false })); + res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, isLogout: false })); return res.json({ username, @@ -164,10 +176,44 @@ const logoutRoute = async (req, res) => { res.send(); }; +const activateRoute = async (req, res) => { + const { activationToken } = req.body; + if (!activationToken) { + res.status(400); + return res.json({ + error: 'Missing or invalid activation token.', + }); + } + + const tokenObj = await db('tokens').where({ type: 'activation', value: activationToken }).first(); + if (!tokenObj) { + res.status(400); + return res.json({ + error: 'Missing or invalid activation token.', + }); + } + + const updatedUsers = await db('users').where({ id: tokenObj.user_id }).update({ activated: true }).returning('*'); + const user = updatedUsers[0]; + await db('tokens').where({ type: 'activation', value: activationToken }).delete(); + + const { username } = user; + const sessionToken = shortid.generate(); + const sessionTokenHash = sha512(user.salt + sessionToken); + + await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); + + res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, isLogout: false })); + return res.json({ + username, + }); +}; + module.exports = { buildSessionCookie, authMiddleware, registerRoute, loginRoute, logoutRoute, + activateRoute, }; diff --git a/backend/src/email.js b/backend/src/email.js new file mode 100644 index 0000000..b41bae5 --- /dev/null +++ b/backend/src/email.js @@ -0,0 +1,20 @@ +const consoleBackend = { + send: ({ recipients, subject, body, cc, bcc }) => { + if (!recipients) throw new Exception('Recipients missing'); + if (!subject) throw new Exception('Subject missing'); + + console.log('Email sent to console.'); + console.log('=== To:', recipients.join(', ')); + if (cc) console.log('=== CC:', cc.join(', ')); + if (bcc) console.log('=== BCC:', bcc.join(', ')); + console.log('=== Subject:', subject); + console.log(); + console.log('=== Body:'); + console.log(body); + console.log('==='); + }, +}; + +module.exports = { + consoleBackend, +}; diff --git a/backend/src/routes.js b/backend/src/routes.js index 9d6775e..9424f75 100644 --- a/backend/src/routes.js +++ b/backend/src/routes.js @@ -4,7 +4,7 @@ const db = require('./db'); const { permissionMiddleware } = require('./authz'); const PERMISSIONS = require('./permissions'); -const { authMiddleware, registerRoute, loginRoute, logoutRoute } = require('./authn'); +const { authMiddleware, registerRoute, loginRoute, logoutRoute, activateRoute } = require('./authn'); const normalRouter = express.Router(); const authedRouter = express.Router(); @@ -20,6 +20,7 @@ normalRouter.get('/', (req, res) => { /* Authentication flow */ normalRouter.post('/register', registerRoute); normalRouter.post('/login', loginRoute); +normalRouter.post('/activate', activateRoute); authedRouter.post('/logout', logoutRoute); authedRouter.get('/user', (req, res) => { diff --git a/frontend/src/Activate.js b/frontend/src/Activate.js new file mode 100644 index 0000000..8a420f4 --- /dev/null +++ b/frontend/src/Activate.js @@ -0,0 +1,36 @@ +import React, { useEffect, useState } from 'react'; +import axios from './axios'; +import { Link } from '@reach/router'; + +const Activate = ({ activationToken }) => { + const [ data, setData ] = useState({}); + useEffect(() => { + (async () => { + try { + await axios.post('/activate', { activationToken }); + setData({ + message: 'Account succesfully activated. You can now log in.', + }); + } catch (err) { + setData({ + error: err.response.data.error, + }); + } + })(); + }, []); + + return ( +
+

Activating account

+
+
+ {data.error} +
+
{data.message}
+ {data.message && To login page} +
+
+ ); +}; + +export default Activate; diff --git a/frontend/src/App.js b/frontend/src/App.js index 89f3e1c..38d8aef 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -7,6 +7,7 @@ import Home from './Home'; import userState from './UserState'; import './App.css'; import axios from './axios'; +import Activate from './Activate'; const App = () => { useEffect(() => { @@ -28,6 +29,7 @@ const App = () => { + diff --git a/frontend/src/Register.js b/frontend/src/Register.js index 6b04816..97a5992 100644 --- a/frontend/src/Register.js +++ b/frontend/src/Register.js @@ -37,6 +37,9 @@ const Register = () => { + @@ -47,7 +50,6 @@ const Register = () => { Register - {/*
{errors}
*/} ); }; -- cgit v1.3