diff options
| author | Jan Tuomi <jan.tuomi@eficode.com> | 2020-02-05 22:31:16 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan.tuomi@eficode.com> | 2020-02-05 22:31:16 +0200 |
| commit | 72b5fc9357f0100f4cc560c1430f7ea2d849eb40 (patch) | |
| tree | 81acb78eae4f69e7b24b2c2d3359d63ba44a82c2 | |
| parent | 6327535653f5ff1abc9f5bbf666268d41abb4d74 (diff) | |
Implement account activation
| -rw-r--r-- | backend/migrations/20200205194947_user_add_activated_email.js | 13 | ||||
| -rw-r--r-- | backend/migrations/20200205200212_add_activate_code_token_type.js | 17 | ||||
| -rw-r--r-- | backend/src/authn.js | 60 | ||||
| -rw-r--r-- | backend/src/email.js | 20 | ||||
| -rw-r--r-- | backend/src/routes.js | 3 | ||||
| -rw-r--r-- | frontend/src/Activate.js | 36 | ||||
| -rw-r--r-- | frontend/src/App.js | 2 | ||||
| -rw-r--r-- | frontend/src/Register.js | 4 |
8 files changed, 146 insertions, 9 deletions
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 ( + <div> + <h2 className="subtitle">Activating account</h2> + <div> + <div className="error" key="error"> + {data.error} + </div> + <div>{data.message}</div> + {data.message && <Link to="/login">To login page</Link>} + </div> + </div> + ); +}; + +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 = () => { <Router> <Login path="/login" /> <Register path="/register" /> + <Activate path="/activate/:activationToken" /> <Home path="/" exact /> </Router> </div> 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 @@ -38,6 +38,9 @@ const Register = () => { Username <input className="input" name="username" type="text" ref={register} required /> </label> <label className="label"> + Email <input className="input" name="email" type="email" ref={register} required /> + </label> + <label className="label"> Password <input className="input" name="password" type="password" ref={register} required /> </label> <label className="label"> @@ -47,7 +50,6 @@ const Register = () => { Register </button> <ul className="error">{data.errors ? data.errors.map(e => <li>{e}</li>) : null}</ul> - {/* <div className="error">{errors}</div> */} </form> ); }; |
