├── .gitignore ├── README.md ├── app.js ├── auth.js ├── config.js ├── package.json └── public ├── app.js ├── index.html └── main.styl /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/main.css 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rethinkdb-pubnub-liveblog 2 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | var r = require("rethinkdb"); 3 | var pubnub = require("pubnub"); 4 | var express = require("express"); 5 | var bodyParser = require("body-parser"); 6 | var stylus = require("stylus"); 7 | var jwt = require("express-jwt"); 8 | 9 | require("rethinkdb-init")(r); 10 | 11 | var auth = require("./auth"); 12 | var config = require("./config"); 13 | 14 | var pn = pubnub(config.pubnub); 15 | pn.grant({ 16 | write: true, read: false, 17 | callback: function(c) { console.log("Permission set:", c); } 18 | }); 19 | 20 | var app = express(); 21 | app.use(bodyParser.json()); 22 | app.use(stylus.middleware(__dirname + "/public")); 23 | app.use(express.static(__dirname + "/public")); 24 | 25 | app.listen(config.port, function() { 26 | console.log("Server started on port " + config.port); 27 | }); 28 | 29 | function validStr(s) { 30 | return typeof s === "string" && s.trim(); 31 | } 32 | 33 | r.init(config.database, [ 34 | {name: "users", indexes: ["username"]}, 35 | "updates" 36 | ]) 37 | .then(function(conn) { 38 | return r.table("updates").changes()("new_val").run(conn); 39 | }) 40 | .then(function(changes) { 41 | changes.each(function(err, item) { 42 | console.log("Received:", item); 43 | pn.publish({channel: "updates", message: item, 44 | error: function(err) { console.log("Failure:" , err); }}); 45 | }); 46 | }); 47 | 48 | function authHandler(authfn) { 49 | return function(req, res) { 50 | if (!(validStr(req.body.username) && validStr(req.body.password))) 51 | return res.status(400).json({success: false, 52 | error: "Must provide username and password"}); 53 | 54 | authfn(req.body.username, req.body.password).then(function(acct) { 55 | pn.grant({ 56 | channel: "updates", auth_key: acct.token, 57 | read: true, write: acct.user.admin, 58 | callback: function(c) { console.log("Set permissions:", c); } 59 | }); 60 | res.json({success: true, token: acct.token, user: acct.user}); 61 | }) 62 | .catch(function(err) { 63 | console.log(err); 64 | res.status(400).json({success: false, error: err}); 65 | }); 66 | }; 67 | } 68 | 69 | app.post("/api/user/create", authHandler(auth.create)); 70 | app.post("/api/user/login", authHandler(auth.login)); 71 | 72 | app.use(jwt({secret: config.jwt.secret, credentialsRequired: false})); 73 | 74 | app.post("/api/send", function(req, res) { 75 | if (!req.user.admin) 76 | return res.status(401).json({success: false, error: "Unauthorized User"}); 77 | 78 | if (!validStr(req.body.message)) 79 | return res.status(400).json({success: false, 80 | error: "Must include a message to send"}); 81 | 82 | r.connect(config.database).then(function(conn) { 83 | return r.table("updates").insert({ 84 | text: req.body.message, 85 | sender: req.user.username, 86 | time: r.now() 87 | }).run(conn).finally(function() { conn.close(); }); 88 | }) 89 | .then(function() { res.json({success: true}); }); 90 | }); 91 | 92 | app.get("/api/history", function(req, res) { 93 | if (!req.user) 94 | return res.status(401).json({success: false, error: "Unauthorized User"}); 95 | 96 | r.connect(config.database).then(function(conn) { 97 | return r.table("updates").orderBy(r.desc("time")).run(conn) 98 | .finally(function() { conn.close(); }); 99 | }) 100 | .then(function(stream) { return stream.toArray(); }) 101 | .then(function(output) { res.json(output); }); 102 | }); 103 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | var r = require("rethinkdb"); 2 | var jwt = require("jsonwebtoken"); 3 | var bluebird = require("bluebird"); 4 | var bcrypt = bluebird.promisifyAll(require("bcrypt")); 5 | 6 | var config = require("./config"); 7 | 8 | function userFind(username) { 9 | return r.connect(config.database).then(function(conn) { 10 | return r.table("users").getAll(username, {index: "username"})(0) 11 | .default(null).run(conn) 12 | .finally(function() { conn.close(); }); 13 | }); 14 | } 15 | 16 | function userMake(username, hash) { 17 | var user = { 18 | username: username, password: hash, 19 | admin: config.admins.indexOf(username) >= 0 20 | }; 21 | 22 | return r.connect(config.database).then(function(conn) { 23 | return r.table("users").insert(user, {returnChanges: true}) 24 | ("changes")(0)("new_val").run(conn) 25 | .finally(function() { conn.close(); }); 26 | }); 27 | } 28 | 29 | module.exports = { 30 | create: function(username, password) { 31 | return userFind(username).then(function(user) { 32 | if (user) throw "User already exists"; 33 | }) 34 | .then(function() { 35 | return bcrypt.hashAsync(password, 10); 36 | }) 37 | .then(function(hash) { 38 | return userMake(username, hash); 39 | }) 40 | .then(function(user) { 41 | return {user: user, token: jwt.sign(user, config.jwt.secret)}; 42 | }); 43 | }, 44 | 45 | login: function(username, password) { 46 | var user; 47 | return userFind(username).then(function(u) { 48 | if (!(user = u)) throw "User doesn't exist"; 49 | return bcrypt.compareAsync(password, u.password); 50 | }) 51 | .then(function(auth) { 52 | if (!auth) throw "Authentication failed"; 53 | }) 54 | .then(function() { 55 | return {user: user, token: jwt.sign(user, config.jwt.secret)}; 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | database: { 3 | db: "pubnub", 4 | host: "localhost", 5 | port: 28015 6 | }, 7 | jwt: { 8 | secret: "xxxxxxxxxxxxxxx" 9 | }, 10 | pubnub: { 11 | subscribe_key: "xxxxxxxxxxxxxxx", 12 | publish_key: "xxxxxxxxxxxxxxx", 13 | secret_key: "xxxxxxxxxxxxxxx" 14 | }, 15 | port: 8091, 16 | admins: [ "admin" ] 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bcrypt": "^5.0.0", 4 | "bluebird": "^2.9.25", 5 | "body-parser": "^1.12.3", 6 | "express": "^4.12.3", 7 | "express-jwt": "^6.0.0", 8 | "jsonwebtoken": "^5.0.0", 9 | "pubnub": "^3.7.10", 10 | "rethinkdb": "^2.0.0", 11 | "rethinkdb-init": "0.0.3", 12 | "stylus": "^0.51.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | Vue.filter("moment", function(value, fmt) { 2 | return moment(value).format(fmt).replace(/'/g, ""); 3 | }); 4 | 5 | var app = new Vue({ 6 | el: "body", 7 | data: { 8 | user: {}, 9 | token: null, 10 | message: null, 11 | messages: [], 12 | authError: null, 13 | authShow: "login" 14 | }, 15 | 16 | ready: function() { 17 | var user = window.sessionStorage.getItem("user"); 18 | var token = window.sessionStorage.getItem("token"); 19 | 20 | if (user && token) 21 | this.connect({user: JSON.parse(user), token: token}); 22 | }, 23 | 24 | methods: { 25 | connect: function(response) { 26 | this.user = response.user; 27 | this.token = response.token; 28 | 29 | window.sessionStorage.setItem("user", JSON.stringify(response.user)); 30 | window.sessionStorage.setItem("token", response.token); 31 | 32 | var pn = PUBNUB.init({ 33 | subscribe_key: "sub-c-751bd564-f6c1-11e4-b945-0619f8945a4f", 34 | auth_key: response.token 35 | }); 36 | 37 | var that = this; 38 | pn.subscribe({ 39 | channel: "updates", 40 | message: function(message, env, channel) { 41 | console.log("Message:", message); 42 | that.messages.unshift(message); 43 | } 44 | }); 45 | 46 | fetch("/api/history", { 47 | headers: { 48 | "Authorization": "Bearer " + this.token 49 | } 50 | }) 51 | .then(function(output) { return output.json(); }) 52 | .then(function(messages) { 53 | if (messages.success !== false) 54 | that.messages = messages; 55 | }); 56 | }, 57 | 58 | send: function(ev) { 59 | fetch("/api/send", { 60 | method: "post", 61 | headers: { 62 | "Content-Type": "application/json", 63 | "Authorization": "Bearer " + this.token 64 | }, 65 | body: JSON.stringify({message: this.message}) 66 | }) 67 | .then(function(output) { return output.json(); }) 68 | .then(function(response) { 69 | console.log("Sent:", response); 70 | app.message = null; 71 | }); 72 | }, 73 | 74 | login: function(path, ev) { 75 | this.authError = null; 76 | 77 | fetch("/api/user/" + path, { 78 | method: "post", 79 | headers: { "Content-Type": "application/json" }, 80 | body: JSON.stringify({ 81 | username: this.username, 82 | password: this.password 83 | }) 84 | }) 85 | .then(function(output) { return output.json(); }) 86 | .then(function(response) { 87 | if (!response.success) 88 | app.authError = response.error; 89 | else { 90 | app.authShow = null; 91 | app.connect(response); 92 | } 93 | }); 94 | } 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |