diff --git a/README.md b/README.md index b5fe36890ac97d64fc55681dcc380131404275e3..16c6594469748a623eeea243e307e90f6dba1c51 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,12 @@ * `mongoose`: Mongoose MongoDB ODM * `body-parser`: Node.js body parsing middleware * `morgan`: HTTP request logger middleware for node.js +* `connect-mongodb-session`: MongoDB session store for connect/express built by MongoDB ### To use * `joi`: Object schema description language and validator for JavaScript objects. ## Release -### Next: v0.1.0 -* [ ] Authentication and session +### TODO * [ ] Validate mongoose models -* [ ] Make mongoose unique index work - -### Next: v0.2.0 -* [ ] Message handler diff --git a/index.js b/index.js index d307bc5d409eec01a6c3049a08337f6eff3090c6..b6be466f1a06db7e2de8c36bc97fee93ea0e504c 100644 --- a/index.js +++ b/index.js @@ -1,28 +1,65 @@ const express = require('express'); const session = require('express-session'); const bodyParser = require('body-parser'); -const User = require('./models/user'); const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const morgan = require('morgan'); +const mongoDBStore = require('connect-mongodb-session')(session); const config = require('./config.json'); +const User = require('./models/user'); +const Notification = require('./models/notification'); + // Utils -const render = (req, res, view, options) => res.render(view, { - ...options, - user: req.session.user, - nextUrl: req.url, -}); +const render = (req, res, view, options) => { + // Load notifications + if (req.session.user) { + res.render(view, { + ...options, + user: req.session.user, + nextUrl: req.url, + notifications: req.session.user.notifications + }); + req.session.user.notifications + .filter(notification => !notification.persistant) + .forEach(notification => notification.remove()); + req.session.user.save(); + } else { + return res.render(view, { + ...options, + user: req.session.user, + nextUrl: req.url, + notifications: [] + }); + } +}; +const warn = (req, res, title, content) => { + req.session.user.notifications.push({ title, content, color: "warning" }); + req.session.user.save(); +}; +const error = (req, res, title, content) => { + req.session.user.notifications.push({ title, content, color: "error" }); + req.user.session.save(); + return res.redirect('/'); // TODO redirect to error route or previous +} // Configuration const app = express(); +const store = new mongoDBStore({ + uri: 'mongodb://localhost/rolegame', + collection: 'sessions' +}); +store.on('error', function(error) { + console.error(error); +}); app.set('view engine', 'pug'); app.use(morgan('tiny')); app.use(session({ secret: config.secret, resave: false, - saveUninitialized: false + saveUninitialized: false, + store })); // Middlewares @@ -36,6 +73,17 @@ app.use((req, res, next) => { return res.redirect('/signup'); } }); +app.use((req, res, next) => { + if (req.session.user) { + User.findById(req.session.user._id, (err, user) => { + err ? error(req, res, 'Error fetching user', err) : null; + req.session.user = user; + next(); + }); + } else { + next(); + } +}) app.get('/', (req, res) => { return render(req, res, 'home'); @@ -46,7 +94,7 @@ app.get('/signup', (req, res) => { app.post('/signup', (req, res) => { const passwordHash = bcrypt.hashSync(req.body.password, config.cryptRounds); User.create(req.body, (err, user) => { - err ? console.error(err) : null; + err ? error(req, res, 'Error creating user', err) : null; user.passwordHash = passwordHash; user.save(); req.session.user = user; @@ -54,16 +102,22 @@ app.post('/signup', (req, res) => { }); }); app.post('/login', (req, res) => { - User.findOne({ username: req.body.username }, (err, user) => { - err ? console.error(err) : null; - if (bcrypt.compareSync(req.body.password, user.passwordHash)) { - req.session.user = user; - return res.redirect(req.query.nextUrl || '/'); - } else { - console.error("Bad authentication"); - return res.redirect('/signup'); - } - }); + if (!req.body.username || !req.body.password) { + return res.redirect('/signup'); + } else if (req.session.user) { + error(req, res, 'User already logged in', 'You must logout before log in.') + } else { + User.findOne({ username: req.body.username }, (err, user) => { + err ? error(req, res, 'Error fetching user', err) : null; + if (bcrypt.compareSync(req.body.password, user.passwordHash)) { + req.session.user = user; + return res.redirect(req.query.nextUrl || '/'); + } else { + error(req, res, 'Bad credentials') + return res.redirect('/signup'); + } + }); + } }); app.post('/logout', (req, res) => { req.session.destroy(); diff --git a/models/notification.js b/models/notification.js new file mode 100644 index 0000000000000000000000000000000000000000..f411cff6edcfea0b65353c5916ded00bd8c93aee --- /dev/null +++ b/models/notification.js @@ -0,0 +1,21 @@ +const mongoose = require('mongoose'); + +const Notification = new mongoose.Schema({ + title: { + type: String, + required: true, + }, + content: { + type: String, + }, + persistant: { + type: Boolean, + default: false, + }, + color: { + type: String, + default: "" + } +}); + +module.exports = Notification; \ No newline at end of file diff --git a/models/user.js b/models/user.js index fdb9dc2587a5453ea56b85e9a19e301f93f3c045..993d4d7ea91843c639a3ceb21a4680ca712b2280 100644 --- a/models/user.js +++ b/models/user.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const Notification = require('./notification'); const User = new mongoose.Schema({ firstName: { @@ -7,11 +8,12 @@ const User = new mongoose.Schema({ }, lastName: { type: String, + required: true, }, username: { unique: true, type: String, - lowercase: true + lowercase: true, }, email: { type: String, @@ -20,6 +22,7 @@ const User = new mongoose.Schema({ passwordHash: { type: String, }, + notifications: [Notification], }); module.exports = mongoose.model('User', User); \ No newline at end of file diff --git a/package.json b/package.json index e3b0f5265bcfe6b94893fabefb7e5623309b5744..e5449afe88dc0e2936b2eee22d25720e724c4002 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "bcrypt": "^3.0.4", "body-parser": "^1.18.3", + "connect-mongodb-session": "^2.1.1", "eslint": "^5.13.0", "express": "^4.16.4", "express-session": "^1.15.6", diff --git a/views/base.pug b/views/base.pug index 38855d3c86dc03a07458dd4ec5681663723f9595..6be478781b22b3d1c969eb199770446e16f4af0d 100644 --- a/views/base.pug +++ b/views/base.pug @@ -7,6 +7,8 @@ html(lang="en") link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" integrity="sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=" crossorigin="anonymous") title RoleGame body + script(src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E=" crossorigin="anonymous") + script(src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js" integrity="sha256-t8GepnyPmw9t+foMh3mKNvcorqNHamSKtKRxxpUEgFI=" crossorigin="anonymous") block navbar if !user form.ui.form(action="/login", method="post") @@ -29,8 +31,8 @@ html(lang="en") .item button.ui.icon.button.basic(type="submit") i.power.off.icon - .ui.container - block main - block script - script(src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E=" crossorigin="anonymous") - script(src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js" integrity="sha256-t8GepnyPmw9t+foMh3mKNvcorqNHamSKtKRxxpUEgFI=" crossorigin="anonymous") \ No newline at end of file + .ui.grid + .thirteen.wide.column + block main + .three.wide.column + include notifications.pug diff --git a/views/notifications.pug b/views/notifications.pug new file mode 100644 index 0000000000000000000000000000000000000000..bf6c0d7b23c3ce6a5ec99b73d9d0361ab1932480 --- /dev/null +++ b/views/notifications.pug @@ -0,0 +1,10 @@ +each notification in notifications + .ui.message.tiny(class=notification.color) + i.close.icon + .header= notification.title + p= notification.content + +script + | $('.message .close').on('click', function() { + | $(this).closest('.message').transition('fade'); + | }); \ No newline at end of file