summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/assets/javascripts/mx/extensions/mx.movements.js6
-rw-r--r--public/assets/javascripts/rectangles/util/mouse.js291
-rwxr-xr-xpublic/assets/stylesheets/app.css1
-rw-r--r--server/index.js9
-rw-r--r--server/lib/api/collaborator.js85
-rw-r--r--server/lib/api/index.js1
-rw-r--r--server/lib/auth/mail.js24
-rw-r--r--server/lib/middleware.js29
-rw-r--r--server/lib/schemas/Collaborator.js29
-rw-r--r--server/lib/views.js32
-rw-r--r--views/controls/editor/collaborators.ejs21
-rw-r--r--views/mail/collaborator.html.ejs20
-rw-r--r--views/mail/collaborator.text.ejs7
-rw-r--r--views/mail/welcome.text.ejs2
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