diff options
| -rw-r--r-- | public/assets/javascripts/mx/extensions/mx.movements.js | 6 | ||||
| -rw-r--r-- | public/assets/javascripts/rectangles/util/mouse.js | 291 | ||||
| -rwxr-xr-x | public/assets/stylesheets/app.css | 1 | ||||
| -rw-r--r-- | server/index.js | 9 | ||||
| -rw-r--r-- | server/lib/api/collaborator.js | 85 | ||||
| -rw-r--r-- | server/lib/api/index.js | 1 | ||||
| -rw-r--r-- | server/lib/auth/mail.js | 24 | ||||
| -rw-r--r-- | server/lib/middleware.js | 29 | ||||
| -rw-r--r-- | server/lib/schemas/Collaborator.js | 29 | ||||
| -rw-r--r-- | server/lib/views.js | 32 | ||||
| -rw-r--r-- | views/controls/editor/collaborators.ejs | 21 | ||||
| -rw-r--r-- | views/mail/collaborator.html.ejs | 20 | ||||
| -rw-r--r-- | views/mail/collaborator.text.ejs | 7 | ||||
| -rw-r--r-- | views/mail/welcome.text.ejs | 2 |
14 files changed, 386 insertions, 171 deletions
diff --git a/public/assets/javascripts/mx/extensions/mx.movements.js b/public/assets/javascripts/mx/extensions/mx.movements.js index 3b7d3e2..669a7f4 100644 --- a/public/assets/javascripts/mx/extensions/mx.movements.js +++ b/public/assets/javascripts/mx/extensions/mx.movements.js @@ -34,8 +34,10 @@ MX.Movements = function (cam) { init: function () { document.addEventListener('keydown', function (e) { - // console.log(e.keyCode) - if (locked) return; + // console.log(e.keyCode) + if (locked || e.altKey || e.metaKey || e.ctrlKey) { + return + } switch ( e.keyCode ) { case 16: // shift diff --git a/public/assets/javascripts/rectangles/util/mouse.js b/public/assets/javascripts/rectangles/util/mouse.js index 34d3f5e..cb36038 100644 --- a/public/assets/javascripts/rectangles/util/mouse.js +++ b/public/assets/javascripts/rectangles/util/mouse.js @@ -1,65 +1,66 @@ /* - usage: - - base.mouse = new mouse({ - el: document.querySelector("#map"), - down: function(e, cursor){ - // do something with val - // cursor.x.a - // cursor.y.a - }, - move: function(e, cursor){ - // delta.a (x) - // delta.b (y) - }, - up: function(e, cursor, new_cursor){ - // cursor.x.a - // cursor.y.a - }, - }) + usage: + + base.mouse = new mouse({ + el: document.querySelector("#map"), + down: function(e, cursor){ + // do something with val + // cursor.x.a + // cursor.y.a + }, + move: function(e, cursor){ + // var delta = cursor.delta() + // delta.a (x) + // delta.b (y) + }, + up: function(e, cursor, new_cursor){ + // cursor.x.a + // cursor.y.a + }, + }) */ function mouse (opt) { - var base = this + var base = this - opt = defaults(opt, { - el: null, - down: null, - move: null, - drag: null, - enter: null, - up: null, - rightclick: null, - propagate: false, - locked: false, - use_offset: true, - val: 0, - }) - - base.down = false + opt = defaults(opt, { + el: null, + down: null, + move: null, + drag: null, + enter: null, + up: null, + rightclick: null, + propagate: false, + locked: false, + use_offset: true, + val: 0, + }) + + base.down = false - base.creating = false - base.dragging = false + base.creating = false + base.dragging = false - base.cursor = new Rect(0,0,0,0) + base.cursor = new Rect(0,0,0,0) - base.tube = new Tube () - opt.down && base.tube.on("down", opt.down) - opt.move && base.tube.on("move", opt.move) - opt.drag && base.tube.on("drag", opt.drag) - opt.enter && base.tube.on("enter", opt.enter) - opt.leave && base.tube.on("leave", opt.leave) - opt.up && base.tube.on("up", opt.up) - opt.rightclick && base.tube.on("rightclick", opt.rightclick) - - var offset = (opt.use_offset && opt.el) ? opt.el.getBoundingClientRect() : null - - base.init = function (){ - base.bind() - } + base.tube = new Tube () + opt.down && base.tube.on("down", opt.down) + opt.move && base.tube.on("move", opt.move) + opt.drag && base.tube.on("drag", opt.drag) + opt.enter && base.tube.on("enter", opt.enter) + opt.leave && base.tube.on("leave", opt.leave) + opt.up && base.tube.on("up", opt.up) + opt.rightclick && base.tube.on("rightclick", opt.rightclick) + + var offset = (opt.use_offset && opt.el) ? opt.el.getBoundingClientRect() : null + + base.init = function (){ + base.bind() + } - base.on = function(){ + base.on = function(){ base.tube.on.apply(base.tube, arguments) } @@ -67,104 +68,104 @@ function mouse (opt) { base.tube.off.apply(base.tube, arguments) } - base.bind = function(){ - if (opt.el) { - opt.el.addEventListener("mousedown", base.mousedown) - opt.el.addEventListener("contextmenu", base.contextmenu) - } - window.addEventListener("mousemove", base.mousemove) - window.addEventListener("mouseup", base.mouseup) - } + base.bind = function(){ + if (opt.el) { + opt.el.addEventListener("mousedown", base.mousedown) + opt.el.addEventListener("contextmenu", base.contextmenu) + } + window.addEventListener("mousemove", base.mousemove) + window.addEventListener("mouseup", base.mouseup) + } - base.bind_el = function(el){ - el.addEventListener("mousedown", base.mousedown) - el.addEventListener("mousemove", base.mousemove) - } - base.unbind_el = function(el){ - el.removeEventListener("mousedown", base.mousedown) - el.removeEventListener("mousemove", base.mousemove) - } + base.bind_el = function(el){ + el.addEventListener("mousedown", base.mousedown) + el.addEventListener("mousemove", base.mousemove) + } + base.unbind_el = function(el){ + el.removeEventListener("mousedown", base.mousedown) + el.removeEventListener("mousemove", base.mousemove) + } - function positionFromMouse(e) { - if (offset) { - return new vec2(offset.left - e.pageX, e.pageY - offset.top) - } - else { - return new vec2(e.pageX, e.pageY) - } - } - - base.mousedown = function(e){ - if (opt.use_offset) { - offset = this.getBoundingClientRect() - } - - var pos = positionFromMouse(e) - - var x = pos.a, y = pos.b - base.cursor = new Rect (x,y, x,y) - base.down = true - e.clickAccepted = true - - base.tube("down", e, base.cursor) + function positionFromMouse(e) { + if (offset) { + return new vec2(offset.left - e.pageX, e.pageY - offset.top) + } + else { + return new vec2(e.pageX, e.pageY) + } + } + + base.mousedown = function(e){ + if (opt.use_offset) { + offset = this.getBoundingClientRect() + } + + var pos = positionFromMouse(e) + + var x = pos.a, y = pos.b + base.cursor = new Rect (x,y, x,y) + base.down = true + e.clickAccepted = true + + base.tube("down", e, base.cursor) - if (e.clickAccepted) { - e.stopPropagation() - } - else { - base.down = false - } - } - base.mousemove = function(e){ - if (opt.use_offset && ! offset) return - - var pos = positionFromMouse(e) + if (e.clickAccepted) { + e.stopPropagation() + } + else { + base.down = false + } + } + base.mousemove = function(e){ + if (opt.use_offset && ! offset) return + + var pos = positionFromMouse(e) - if (e.shiftKey) { - pos.quantize(10) - } + if (e.shiftKey) { + pos.quantize(10) + } - var x = pos.a, y = pos.b - - if (base.down) { - base.cursor.x.b = x - base.cursor.y.b = y - base.tube("drag", e, base.cursor) - e.stopPropagation() - } - else { - base.cursor.x.a = base.cursor.x.b = x - base.cursor.y.a = base.cursor.y.b = y - base.tube("move", e, base.cursor) - } - } - base.mouseenter = function(e, target, index){ - if (! base.down) return - if (opt.use_offset && ! offset) return - base.tube("enter", e, target, base.cursor) - } - base.mouseleave = function(e, target){ - if (! base.down) return - if (opt.use_offset && ! offset) return - base.tube("leave", e, target, base.cursor) - } - base.mouseup = function(e){ - var pos, new_cursor - - if (base.down) { - e.stopPropagation() - base.down = false - pos = positionFromMouse(e) - new_cursor = new Rect (pos.a, pos.b) - base.tube("up", e, base.cursor, new_cursor) - base.cursor = new_cursor - } - } - base.contextmenu = function(e){ - e.preventDefault() - base.tube("rightclick", e, base.cursor) - } + var x = pos.a, y = pos.b + + if (base.down) { + base.cursor.x.b = x + base.cursor.y.b = y + base.tube("drag", e, base.cursor) + e.stopPropagation() + } + else { + base.cursor.x.a = base.cursor.x.b = x + base.cursor.y.a = base.cursor.y.b = y + base.tube("move", e, base.cursor) + } + } + base.mouseenter = function(e, target, index){ + if (! base.down) return + if (opt.use_offset && ! offset) return + base.tube("enter", e, target, base.cursor) + } + base.mouseleave = function(e, target){ + if (! base.down) return + if (opt.use_offset && ! offset) return + base.tube("leave", e, target, base.cursor) + } + base.mouseup = function(e){ + var pos, new_cursor + + if (base.down) { + e.stopPropagation() + base.down = false + pos = positionFromMouse(e) + new_cursor = new Rect (pos.a, pos.b) + base.tube("up", e, base.cursor, new_cursor) + base.cursor = new_cursor + } + } + base.contextmenu = function(e){ + e.preventDefault() + base.tube("rightclick", e, base.cursor) + } - base.init() + base.init() } diff --git a/public/assets/stylesheets/app.css b/public/assets/stylesheets/app.css index 1c48eee..3b00cd2 100755 --- a/public/assets/stylesheets/app.css +++ b/public/assets/stylesheets/app.css @@ -1552,6 +1552,7 @@ form li textarea { cursor: pointer; position: fixed; right: 20px; + top: 20px; } .close:hover { diff --git a/server/index.js b/server/index.js index e6afdb8..212db01 100644 --- a/server/index.js +++ b/server/index.js @@ -117,12 +117,17 @@ site.route = function () { app.get('/layout', middleware.ensureAuthenticated, middleware.ensureIsStaff, views.modal) app.get('/layout/:name', middleware.ensureAuthenticated, middleware.ensureIsStaff, views.builder) + app.get('/join/:nonce', middleware.ensureAuthenticated, api.collaborator.join) + app.get('/api/collaborator/:slug/index', middleware.ensureAuthenticated, middleware.ensureProject, api.collaborator.index) + app.post('/api/collaborator/:slug/create', middleware.ensureAuthenticated, middleware.ensureProject, api.collaborator.create) + app.delete('/api/collaborator/:slug/destroy', middleware.ensureAuthenticated, middleware.ensureProject, api.collaborator.destroy) + app.get('/project', middleware.ensureAuthenticated, views.modal) app.get('/project/new', middleware.ensureAuthenticated, views.modal) - app.get('/project/new/:layout', middleware.ensureAuthenticated, views.editor) + app.get('/project/new/:layout', middleware.ensureAuthenticated, views.editor_new) app.get('/project/:slug', middleware.ensureProject, views.reader) app.get('/project/:slug/view', middleware.ensureProject, views.reader) - app.get('/project/:slug/edit', middleware.ensureProject, views.editor) + app.get('/project/:slug/edit', middleware.ensureProject, middleware.ensureIsCollaborator, views.editor) app.get('/api/layout', middleware.ensureAuthenticated, api.layouts.index) app.get('/api/layout/:slug', middleware.ensureAuthenticated, api.layouts.show) diff --git a/server/lib/api/collaborator.js b/server/lib/api/collaborator.js new file mode 100644 index 0000000..4b55f09 --- /dev/null +++ b/server/lib/api/collaborator.js @@ -0,0 +1,85 @@ +/* jshint node: true */ + +var _ = require('lodash'), + util = require('../util'), + upload = require('../upload'), + config = require('../../../config.json'), + Collaborator = require('../schemas/Collaborator'), + Project = require('../schemas/Project'); + +var collaborator = { + + join: function(req, res){ + var nonce = req.query.nonce + if (! nonce || ! nonce.length) { return res.json({ error: "invalid invite code" }) } + Collaborator.findOne({ nonce: nonce }, function(err, collaborator){ + if (err || ! collaborator) { return res.json({ error: "can't find collaborator" }) } + collaborator.user_id = req.user.user_id + collaborator.nonce = "" + collaborator.save(function(err, collaborator){ + Project.findOne({ _id: collaborator.project_id }, function(err, project){ + if (err || ! project) { return res.json({ error: err }) } + res.redirect("/project/" + project.slug + "/edit") + }) + }) + }) + }, + + // + + index: function(req, res){ + if (! req.project) { + return res.json({ error: "can't find project" }) + } + if (req.project.user_id !== req.user._id) { return res.json({ error: "insufficient permission" }) } + Collaborator.find({ project_id: req.project._id }, function(err, collaborators){ + var user_ids = _.pluck(collaborators, "user_id").filter(function(id){ return !! id }) + User.find({ _id: user_ids }, "username displayName photo", function(err, users){ + if (! user_ids) { + return res.json(collaborators) + } + var userIndex = _.indexBy(users, '_id') + collaborators = collaborators.map(function(collaborator){ + var obj = collaborator.toObject() + obj.user = userIndex[ obj.user_id ] + return obj + }) + res.json(collaborators) + }) + }) + }, + + create: function(req, res){ + if (! req.project) { + return res.json({ error: "can't find project" }) + } + var data = util.cleanQuery(req.body) + delete data.user_id + + data.project_id = req.project._id + + Collaborator.makeNonce(function(nonce){ + data.nonce = nonce + new Collaborator(data).save(function(err, collaborator){ + if (err || ! collaborator) { return res.json({ error: err }) } + auth.mail.forgotPassword(req.project, req.user, collaborator, function(){ + res.json(collaborator) + }) + }) + }) + }, + + destroy: function(req, res){ + if (! req.project) { + return res.json({ error: "can't find project" }) + } + if (req.project.user_id !== req.user._id) { + return res.json({ error: "insufficient permission" }) + } + Collaborator.remove({ _id: _id }, function(err){ + res.json({ status: "OK" }) + }) + } +} + +module.exports = collaborator diff --git a/server/lib/api/index.js b/server/lib/api/index.js index bfe3632..ad86daa 100644 --- a/server/lib/api/index.js +++ b/server/lib/api/index.js @@ -6,6 +6,7 @@ var api = { media: require('./media'), profile: require('./profile'), projects: require('./projects'), + collaborator: require('./collaborator'), } module.exports = api diff --git a/server/lib/auth/mail.js b/server/lib/auth/mail.js index a4abccd..0211325 100644 --- a/server/lib/auth/mail.js +++ b/server/lib/auth/mail.js @@ -10,7 +10,7 @@ var mail = { templates: {}, init: function(){ - var names = ["welcome","password"].forEach(function(name){ + var names = ["welcome","password","collaborator"].forEach(function(name){ mail.templates[name] = {}; var types = ["text","html"].forEach(function(type){ fs.readFile("views/mail/" + name + "." + type + ".ejs", function(err, data){ @@ -62,6 +62,28 @@ var mail = { mail.send(message, cb) console.log("sent password email to", user.email) }, + + collaborator: function(project, user, collaborator, cb){ + var data = { + projectSlug: project.slug, + projectName: project.name, + nonce: collaborator.nonce, + username: user.username, + } + + var message = { + text: mail.templates.collaborator.text(data), + from: mail.from, + to: collaborator.email, + subject: "Join " + data.username + " on " + data.projectName, + attachment: [ + { data: mail.templates.collaborator.html(data), alternative: true }, + ] + } + mail.send(message, cb) + console.log("sent collaborator email to", user.email) + }, + } module.exports = mail diff --git a/server/lib/middleware.js b/server/lib/middleware.js index 27b9c04..9d6236a 100644 --- a/server/lib/middleware.js +++ b/server/lib/middleware.js @@ -5,6 +5,7 @@ var passport = require('passport'), _ = require('lodash'), config = require('../../config.json'), User = require('./schemas/User'), + Collaborator = require('./schemas/Collaborator'), Project = require('./schemas/Project'); @@ -36,7 +37,7 @@ var middleware = { ensureLocals: function (req, res, next) { res.locals.token = req.csrfToken(); res.locals.logged_in = req.isAuthenticated() - res.locals.user = req.user || { id: undefined } + res.locals.user = req.user || { _id: undefined } res.locals.config = config res.locals.profile = null res.locals.opt = {} @@ -63,8 +64,32 @@ var middleware = { req.project = null next() } - } + }, + + ensureIsCollaborator: function(req, res, next) { + req.isCollaborator = false + req.isOwner = false + if (! req.user || ! req.project) { + next() + } + else if (String(req.user._id) === String(req.project.user_id)) { + req.isOwner = true + next() + } + else { + Collaborator.findOne({ user_id: req.user._id, project_id: req.project._id }, function(err, collab) { + if (err || ! collab) { + next() + } + else { + req.isCollaborator = true + next() + } + }) + } + }, + } module.exports = middleware diff --git a/server/lib/schemas/Collaborator.js b/server/lib/schemas/Collaborator.js new file mode 100644 index 0000000..79e3287 --- /dev/null +++ b/server/lib/schemas/Collaborator.js @@ -0,0 +1,29 @@ +/* jshint node: true */ + +var mongoose = require('mongoose'), + _ = require('lodash'), + config = require('../../../config.json'), + util = require('../util'); + +var CollaboratorSchema = new mongoose.Schema({ + email: { type: String, required: true}, + project_id: { type: mongoose.Schema.ObjectId, index: true }, + user_id: { type: mongoose.Schema.ObjectId, index: true }, + nonce: { + type: String, + default: "", + }, + created_at: { type: Date }, + updated_at: { type: Date }, +}) + +CollaboratorSchema.statics.makeNonce = function(cb){ + crypto.pseudoRandomBytes(256, function (err, buf){ + var shasum = crypto.createHash('sha1') + shasum.update(buf) + cb( shasum.digest('hex') ) + }) +} + +module.exports = exports = mongoose.model('collaborator', CollaboratorSchema); +exports.schema = CollaboratorSchema; diff --git a/server/lib/views.js b/server/lib/views.js index b776582..4faf80f 100644 --- a/server/lib/views.js +++ b/server/lib/views.js @@ -3,6 +3,7 @@ var User = require('./schemas/User'), Project = require('./schemas/Project'), Documentation = require('./schemas/Documentation'), + Collaborator = require('./schemas/Collaborator'), config = require('../../config'), marked = require('marked'), util = require('./util'), @@ -19,29 +20,24 @@ marked.setOptions({ var views = {} +views.editor_new = function (req, res) { + if (! req.user) { + res.redirect('/') + } + else { + res.render('editor') + } +} + views.editor = function (req, res) { - if (! req.user && ! req.project) { + if (! req.project) { res.redirect('/') } - else if (! req.user || (req.project && String(req.user._id) !== String(req.project.user_id))) { - User.findOne({ _id: req.project.user_id }, function(err, user) { - if (err || ! user) { - console.error(err) - res.redirect('/') - return - } - res.render('reader', { - name: util.sanitize(req.project.name), - description: util.sanitize(req.project.description), - date: moment(req.project.updated_at).format("M/DD/YYYY"), - author: user.displayName, - authorlink: "/profile/" + user.username, - noui: !! (req.query.noui === '1'), - }) - }) + else if (req.isCollaborator || req.isOwner) { + res.render('editor') } else { - res.render('editor') + views.reader(req, res) } } diff --git a/views/controls/editor/collaborators.ejs b/views/controls/editor/collaborators.ejs new file mode 100644 index 0000000..448b6d4 --- /dev/null +++ b/views/controls/editor/collaborators.ejs @@ -0,0 +1,21 @@ +<div class="collaborators fixed animate"> + <span class="close">X</span> + + <ul id="collaborator-list"> + <li> + <div class="avatar"></div> + <span class="username"></span> + <button class="remove-user">Remove</button> + </li> + </ul> + + <div> + To invite others to contribute to this project, submit their email address below. They'll receive an email with instructions to join this blog and register if they're not a Vvalls user yet. + </div> + + <form> + <input type="text" id="collaborator-email"> + <button id="collaborator-invite">Invite to this project</button> + </form> + +</div> diff --git a/views/mail/collaborator.html.ejs b/views/mail/collaborator.html.ejs new file mode 100644 index 0000000..5621c1e --- /dev/null +++ b/views/mail/collaborator.html.ejs @@ -0,0 +1,20 @@ +<html> +<body> + +<p> + <a href="http://vvalls.com/"><img src="http://vvalls.com/img/logo.svg"></a> +</p> + +<p> + <a href="http://vvalls.com/profile/[[- username ]]">[[- username ]]</a> has invited you to join the project + <a href="http://vvalls.com/project/[[- projectSlug ]]">[[- projectName ]]</a> on Vvalls. +</p> + +<p> + Accept the invitation below: +</p> + +<a href="http://vvalls.com/join/[[- nonce ]]">Join Project</a> + +</body> +</html> diff --git a/views/mail/collaborator.text.ejs b/views/mail/collaborator.text.ejs new file mode 100644 index 0000000..52d39b6 --- /dev/null +++ b/views/mail/collaborator.text.ejs @@ -0,0 +1,7 @@ + +[[- username ]] has invited you to join the project [[- projectName ]] on Vvalls. + +Accept the invitation below: + +http://vvalls.com/join/[[- nonce ]] + diff --git a/views/mail/welcome.text.ejs b/views/mail/welcome.text.ejs index cab9c15..02b449b 100644 --- a/views/mail/welcome.text.ejs +++ b/views/mail/welcome.text.ejs @@ -1,4 +1,4 @@ Welcome to Vvalls, [[- username ]] -http://www.posthang.com +http://www.vvalls.com |
