summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--backend/migrations/20200205194947_user_add_activated_email.js13
-rw-r--r--backend/migrations/20200205200212_add_activate_code_token_type.js17
-rw-r--r--backend/src/authn.js60
-rw-r--r--backend/src/email.js20
-rw-r--r--backend/src/routes.js3
-rw-r--r--frontend/src/Activate.js36
-rw-r--r--frontend/src/App.js2
-rw-r--r--frontend/src/Register.js4
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>
);
};