From 1165ef5440e643252635aeea73a14cba0bb2e461 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 9 Jun 2014 16:14:49 -0400 Subject: documentation system --- package.json | 8 +-- public/assets/javascripts/ui/DocumentModal.js | 41 ++++++++++++ public/assets/javascripts/ui/Router.js | 53 +++++++++++++--- public/assets/javascripts/util.js | 4 ++ public/assets/javascripts/vendor/ModalFormView.js | 2 +- public/assets/stylesheets/app.css | 22 ++++++- server/index.js | 13 +++- server/lib/api.js | 54 ++++++++++++++-- server/lib/auth.js | 9 ++- server/lib/middleware.js | 6 +- server/lib/schemas/Documentation.js | 33 ++++++++++ server/lib/schemas/Project.js | 8 +-- server/lib/schemas/User.js | 13 ++-- server/lib/util.js | 51 ++++++++++++--- server/lib/views.js | 51 +++++++++++++++ views/docs.ejs | 49 +++++++++++++++ views/home.ejs | 2 +- views/modal.ejs | 2 +- views/partials/edit-profile.ejs | 6 +- views/partials/footer.ejs | 2 +- views/partials/meta.ejs | 4 +- views/partials/scripts.ejs | 1 + views/partials/sign-in.ejs | 77 +++++++++++++++++++++++ views/partials/signin.ejs | 77 ----------------------- views/staff.ejs | 19 ++++++ views/staff/edit-docs.ejs | 42 +++++++++++++ 26 files changed, 519 insertions(+), 130 deletions(-) create mode 100644 public/assets/javascripts/ui/DocumentModal.js create mode 100644 server/lib/schemas/Documentation.js create mode 100644 views/docs.ejs create mode 100644 views/partials/sign-in.ejs delete mode 100644 views/partials/signin.ejs create mode 100644 views/staff.ejs create mode 100644 views/staff/edit-docs.ejs diff --git a/package.json b/package.json index b0feaf0..bcac5cf 100644 --- a/package.json +++ b/package.json @@ -21,19 +21,17 @@ "ejs": "^0.8.8", "useful-string": "0.0.1", "express-subdomain-handler": "~0.1.0", - "lodash": "~2.4.1", "express-subdomains": "0.0.5", - "mers": "~0.7.2", + "lodash": "~2.4.1", "mongoose": "~3.8.8", - "baucis": "~0.15.1", - "baucis-swagger": "~0.2.5", "mongoose-unique-validator": "~0.3.0", "mongoose-lifecycle": "~1.0.0", "knox": "~0.8.10", "moment": "~2.6.0", "html-entities": "~1.0.10", "multer": "~0.1.0", - "body-parser": "1.3.0" + "body-parser": "1.3.0", + "marked": "~0.3.2" }, "devDependencies": { "grunt": "~0.4.1", diff --git a/public/assets/javascripts/ui/DocumentModal.js b/public/assets/javascripts/ui/DocumentModal.js new file mode 100644 index 0000000..f821d07 --- /dev/null +++ b/public/assets/javascripts/ui/DocumentModal.js @@ -0,0 +1,41 @@ + + +var DocumentModal = ModalFormView.extend({ + el: ".mediaDrawer.editDocument", + createAction: "/api/docs/new", + updateAction: "/api/docs/edit", + + load: function(name, isNew){ + this.reset() + + if (isNew || name === "new") { + name = sanitize(name) + if (name !== "new") { + this.$("[name='new_name']").val( name.replace(/\s+/g,"-") ) + this.$("[name='displayName']").val( capitalize(name.replace(/-/g," ")) ) + } + this.action = this.createAction + return this.show() + } + + this.action = this.updateAction + + $.get("/api/docs", { name: name }, $.proxy(function(data){ + if (data.isNew) { + this.action = this.createAction + } + + for (var i in data) { + this.$("[name='" + i + "']").val(data[i]) + } + this.$("[name='new_name']").val(name) + + this.show() + }, this)) + }, + + success: function(res){ + window.location.pathname = "/about/" + res.name + } + +}) diff --git a/public/assets/javascripts/ui/Router.js b/public/assets/javascripts/ui/Router.js index a518e27..fea3698 100644 --- a/public/assets/javascripts/ui/Router.js +++ b/public/assets/javascripts/ui/Router.js @@ -8,14 +8,18 @@ var Router = View.extend({ "click [data-role='new-project-modal']": 'newProject', "click [data-role='edit-project-modal']": 'editProject', "click [data-role='edit-profile-modal']": 'editProfile', + "click [data-role='new-document-modal']": 'newDocument', + "click [data-role='edit-document-modal']": 'editDocument', }, routes: { - "/login": 'signin', - "/signup": 'signup', - "/project/new": 'newProject', - "/profile/edit": 'editProfile', - "/app": 'launch', + "/login": 'signin', + "/signup": 'signup', + "/project/new": 'newProject', + "/profile/edit": 'editProfile', + "/about/:name/edit": 'editDocument', + "/about/new": 'newDocument', + "/app": 'launch', }, initialize: function(){ @@ -24,13 +28,29 @@ var Router = View.extend({ this.newProjectModal = new NewProjectModal() this.editProjectModal = new EditProjectModal() this.editProfileModal = new EditProfileModal() + this.documentModal = new DocumentModal() this.originalPath = window.location.pathname + var path = window.location.pathname.split("/") + console.log(path) for (var route in this.routes) { - if (window.location.pathname.indexOf(route) === 0) { - this[this.routes[route]]() - break; + var routePath = route.split("/") + if (routePath[1] == path[1]) { + if (routePath[2] && routePath[2].indexOf(":") !== -1 && path[2] && (path[3] === routePath[3]) ) { + console.log("GOT :") + console.log(routePath) + this[this.routes[route]](null, path[2]) + break; + } + else if (routePath[2] == path[2]) { + this[this.routes[route]](null) + break; + } + else if (! routePath[2] && (! path[2].length || ! path[2])) { + this[this.routes[route]](null) + break; + } } } @@ -73,5 +93,22 @@ var Router = View.extend({ this.editProfileModal.load() }, + newDocument: function(e){ + e && e.preventDefault() + + var name = e ? $(e.currentTarget).data("name") : "new" + + window.history.pushState(null, document.title, "/about/new") + this.documentModal.load(name, true) + }, + + editDocument: function(e, name){ + e && e.preventDefault() + + var name = e ? $(e.currentTarget).data("name") : name + window.history.pushState(null, document.title, "/about/" + name + "/edit") + this.documentModal.load(name, false) + }, + }) diff --git a/public/assets/javascripts/util.js b/public/assets/javascripts/util.js index 64bda4d..ec2760c 100644 --- a/public/assets/javascripts/util.js +++ b/public/assets/javascripts/util.js @@ -7,6 +7,10 @@ if (window.$) { } function trim(s){ return s.replace(/^\s+/,"").replace(/\s+$/,"") } +function sanitize (s){ return (s || "").replace(new RegExp("[<>&\"\']", 'g'), "") } +function capitalize (s){ return s.split(" ").map(capitalizeWord).join(" ") } +function capitalizeWord (s){ return s.charAt(0).toUpperCase() + s.slice(1) } + var E = Math.E var PI = Math.PI diff --git a/public/assets/javascripts/vendor/ModalFormView.js b/public/assets/javascripts/vendor/ModalFormView.js index bb926d1..3ef7810 100644 --- a/public/assets/javascripts/vendor/ModalFormView.js +++ b/public/assets/javascripts/vendor/ModalFormView.js @@ -14,7 +14,7 @@ var ModalFormView = ModalView.extend({ }, reset: function(){ - this.$("input").not("[type='submit']").not("[type='hidden']").val("") + this.$("input,textarea").not("[type='submit']").not("[type='hidden']").val("") }, load: function(){ diff --git a/public/assets/stylesheets/app.css b/public/assets/stylesheets/app.css index 4a17214..e3f4de5 100755 --- a/public/assets/stylesheets/app.css +++ b/public/assets/stylesheets/app.css @@ -318,6 +318,18 @@ h5{ padding-top: 25px; } +/* DOCUMENTATION / ABOUT SECTION / FAQ PAGES */ + +.docs .content { + width: 600px; + margin: 0 auto; + text-align: left; +} + +.docs .content p { + margin: 1em 0; +} + .footer { width: 100%; margin: 80px 0; @@ -427,7 +439,6 @@ h5{ .profilepage .bio span:after { content: ' \00b7 ' } .profilepage .bio span:last-of-type:after { display: none; } - .templates { padding-top: 7vh; } @@ -1279,7 +1290,14 @@ form li div div { text-align: left; margin: 0 10px 10px 0; } - +form li img#load_avatar { + max-width: 200px; +} +form li textarea { + width: 100%; + height: 300px; + margin-top: 20px; +} .video { height:80vh; diff --git a/server/index.js b/server/index.js index a1efaf2..f418c42 100644 --- a/server/index.js +++ b/server/index.js @@ -63,6 +63,9 @@ app.all('*', middleware.ensureLocals); // Initialize views app.get('/', views.home); +app.get('/about', views.docs); +app.get('/about/:name/edit', views.docs); +app.get('/about/:name', views.docs); app.get('/login', views.modal); app.get('/signup', views.modal); app.post('/auth/signin', auth.loggedInLocal); @@ -74,10 +77,18 @@ app.get('/auth/facebook', auth.login('facebook')); app.get('/auth/facebook/callback', auth.loggedIn('facebook')); app.get('/profile', views.profile) app.get('/profile/edit', views.profile) + app.get('/api/profile', middleware.ensureAuthenticated, api.profile.show) app.put('/api/profile', middleware.ensureAuthenticated, api.profile.update) -app.get('/project/new', views.modal); +app.get('/project/new', middleware.ensureAuthenticated, views.modal); + +app.get('/staff', middleware.ensureAuthenticated, middleware.ensureIsStaff, views.staff.index); +app.get('/staff/bless', middleware.ensureAuthenticated, views.staff.bless); + +app.get('/api/docs', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.show) +app.post('/api/docs/new', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.create) +app.post('/api/docs/edit', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.update) app.get(/^\/([-_a-zA-Z0-9]+)\/?$/, views.profile) diff --git a/server/lib/api.js b/server/lib/api.js index cf2a911..9a8a1fc 100644 --- a/server/lib/api.js +++ b/server/lib/api.js @@ -8,8 +8,8 @@ var passport = require('passport'), util = require('./util'), upload = require('./upload'), config = require('../../config.json'), - User = require('./schemas/User'); - + User = require('./schemas/User'), + Documentation = require('./schemas/Documentation'); var api = { @@ -35,11 +35,10 @@ var api = { } delete data.old_password delete data.new_password - delete data.isAdmin + delete data.isStaff + data.updated_at = new Date () if (req.files.avatar) { - // handle the upload here - console.log("GOT SOME FILES") upload.put("avatars", req.files.avatar, { acceptable: function(){ console.log("acceptable") @@ -66,6 +65,51 @@ var api = { }) } } + }, + + + docs: { + show: function(req, res){ + Documentation.findOne({ name: req.query.name }, function(err, doc){ + if (doc) { + res.json(doc) + } + else { + var name = util.sanitize(req.query.name) + if (name == "new") { + name = "" + } + res.json({ name: name, isNew: true }) + } + }) + }, + + create: function(req, res){ + var data = util.cleanQuery(req.body) + data.name = data.new_name + delete data.new_name + new Documentation(data).save(function(err, doc){ + if (err || ! doc) { return res.json({ error: err }) } + res.json(doc) + }) + }, + + update: function(req, res){ + var data = util.cleanQuery(req.body) + if (data.name == "new") { + return api.docs.create(req, res) + } + Documentation.findOne({ name: data.name }, function(err, doc){ + if (err || ! doc) { return res.json({ error: err }) } + data.name = data.new_name + delete data.new_name + _.extend(doc, data) + doc.save(function(err, doc){ + if (err || ! doc) { return res.json({ error: err }) } + res.json(doc) + }) + }) + }, } } diff --git a/server/lib/auth.js b/server/lib/auth.js index 5a952f5..47c1c7c 100644 --- a/server/lib/auth.js +++ b/server/lib/auth.js @@ -98,7 +98,7 @@ var auth = { }, deserializeUser: function (id, done) { - User.findOne({ _id: id }, "_id displayName username photo", function (err, user) { + User.findOne({ _id: id }, "_id displayName username photo isStaff", function (err, user) { done(err, user); }); }, @@ -112,7 +112,7 @@ var auth = { shasum.update(password) password = shasum.digest('hex'); - User.findOne({ username: username }, function (err, user) { + User.findOne({ username: username }, "_id username", function (err, user) { if (user) { res.json({ error: { errors: { username: { message: "Username has been taken" } } } }) return @@ -121,7 +121,10 @@ var auth = { username: username, displayName: username, password: password, - email: email + email: email, + created_ip: util.ipToNum(req.connection.remoteAddress), + last_ip: util.ipToNum(req.connection.remoteAddress), + created_at: new Date () } new User(data).save(function(err, user){ if (err || ! data) { return res.json({ error: err }) } diff --git a/server/lib/middleware.js b/server/lib/middleware.js index fb19e68..dbe0b26 100644 --- a/server/lib/middleware.js +++ b/server/lib/middleware.js @@ -25,10 +25,10 @@ var middleware = { next(); }, - ensureIsAdmin: function (req, res, next) { + ensureIsStaff: function (req, res, next) { User.findOne({ _id: req.user._id }, function (err, user) { - if (! user.isAdmin) { - return res.redirect('http://' + config.host + '/' + req.user.username); + if (! user.isStaff) { + return res.redirect('http://' + config.host + '/'); } req.user = user next(); diff --git a/server/lib/schemas/Documentation.js b/server/lib/schemas/Documentation.js new file mode 100644 index 0000000..35cf34f --- /dev/null +++ b/server/lib/schemas/Documentation.js @@ -0,0 +1,33 @@ +/* jshint node: true */ + + +var mongoose = require('mongoose'), + _ = require('lodash'), + util = require('../util'); + +var DocumentationSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + unique: true, + validate: [function (val){ + val = util.slugify(val || this.displayName || "") + if (! val.length) return false + if (val == "new") return false + return true + },"{PATH} name is required"] + }, + displayName: { + type: String, + }, + body: { + type: String, + default: "" + }, + created_at: { type: Date }, + updated_at: { type: Date }, +}); + + +module.exports = exports = mongoose.model('documentation', DocumentationSchema); +exports.schema = DocumentationSchema; diff --git a/server/lib/schemas/Project.js b/server/lib/schemas/Project.js index a0382b3..5176e06 100644 --- a/server/lib/schemas/Project.js +++ b/server/lib/schemas/Project.js @@ -1,10 +1,9 @@ /* jshint node: true */ -var NONALPHANUMERICS_REGEX = new RegExp('[^-_a-zA-Z0-9]', 'g') - var mongoose = require('mongoose'), _ = require('lodash'), - config = require('../../../config.json'); + config = require('../../../config.json'), + util = require('../util'); var ProjectSchema = new mongoose.Schema({ name: { type: String, required: true }, @@ -12,7 +11,7 @@ var ProjectSchema = new mongoose.Schema({ type: String, required: true, validate: [function (val){ - val = (val || this.displayName || "").replace(/\s/g,"-").replace(NONALPHANUMERICS_REGEX, '-').replace(/-+/g,"-") + val = util.sanitize(val || this.displayName || "") if (! val.length) return false return true },"{PATH} name is required"] @@ -33,6 +32,5 @@ var ProjectSchema = new mongoose.Schema({ updated_at: { type: Date }, }); - module.exports = exports = mongoose.model('project', ProjectSchema); exports.schema = ProjectSchema; diff --git a/server/lib/schemas/User.js b/server/lib/schemas/User.js index 24b0adf..5a93df2 100644 --- a/server/lib/schemas/User.js +++ b/server/lib/schemas/User.js @@ -1,7 +1,5 @@ /* jshint node: true */ -var NONALPHANUMERICS_REGEX = new RegExp('[^-_a-zA-Z0-9]', 'g') - var mongoose = require('mongoose'), _ = require('lodash'), crypto = require('crypto'), @@ -16,7 +14,7 @@ var UserSchema = new mongoose.Schema({ type: String, required: true, validate: [function (val) { - val = val.replace(NONALPHANUMERICS_REGEX, "") + val = util.slugify(val) this.username = val.toLowerCase() switch (val) { case 'login': @@ -27,6 +25,7 @@ var UserSchema = new mongoose.Schema({ case 'about': case 'settings': case 'assets': + case 'staff': case 'admin': case 'terms': case 'api': @@ -39,7 +38,7 @@ var UserSchema = new mongoose.Schema({ return true }, "{PATH} is not an acceptable name"] }, - email: { type: String, efault: "" }, + email: { type: String, default: "" }, emailVerified: { type: Boolean, default: false, @@ -57,7 +56,11 @@ var UserSchema = new mongoose.Schema({ website: { type: String, default: "" }, twitterName: { type: String, default: "" }, facebookUrl: { type: String, default: "" }, - isAdmin: { type: Boolean, default: false } + isStaff: { type: Boolean, default: false }, + created_at: { type: Date }, + updated_at: { type: Date }, + created_ip: { type: Number }, + last_ip: { type: Number }, }); UserSchema.methods.validPassword = function (pw) { diff --git a/server/lib/util.js b/server/lib/util.js index 88d16cb..228563a 100644 --- a/server/lib/util.js +++ b/server/lib/util.js @@ -1,21 +1,56 @@ var _ = require('lodash'); +var whitespace = new RegExp('\s', 'g') var whitespaceHead = /^\s+/ var whitespaceTail = /\s+$/ +var nonAlphanumerics = new RegExp('[^-_a-zA-Z0-9]', 'g') +var consecutiveDashes = new RegExp("-+", 'g') +var entities = new RegExp("[<>&]", 'g') var util = {} -util.trim = function (s){ return s.replace(whitespaceHead,"").replace(whitespaceTail,"") } + +util.trim = function (s){ return (s || "").replace(whitespaceHead,"").replace(whitespaceTail,"") } + +util.slugify = function (s){ + return (s || "").replace(whitespace,"-").replace(nonAlphanumerics, '-').replace(consecutiveDashes,"-") +} + +util.sanitize = function (s){ + return (s || "").replace(entities, "") +} + +util.capitalize = function (s) { + return (s || "").split(" ").map(util.capitalizeWord).join(" "); +} + +util.capitalizeWord = function (s) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + util.cleanQuery = function (query) { - var update = _.extend({}, query); - delete update._id; - delete update.created_at; - delete update.modified_at; - delete update.modified_by; - delete update.created_by; - return update; + var update = _.extend({}, query); + delete update._id; + delete update.created_at; + delete update.modified_at; + delete update.modified_by; + delete update.created_by; + return update; } +util.ip2num = function(dot) { + var d = dot.split('.'); + return ((((((+d[0])*256)+(+d[1]))*256)+(+d[2]))*256)+(+d[3]); +} + +util.num2ip = function(num) { + var d = num % 256; + for (var i = 3; i > 0; i--) { + num = Math.floor(num/256); + d = num % 256 + '.' + d; + } + return d; +} module.exports = util diff --git a/server/lib/views.js b/server/lib/views.js index 224dd3f..94774cb 100644 --- a/server/lib/views.js +++ b/server/lib/views.js @@ -2,9 +2,20 @@ var User = require('./schemas/User'), Project = require('./schemas/Project'), + Documentation = require('./schemas/Documentation'), config = require('../../config'), + marked = require('marked'), + util = require('./util'), _ = require('lodash'); +marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + sanitize: true, + smartLists: true, + smartypants: true, +}); + var views = {} views.modal = function (req, res) { @@ -17,6 +28,34 @@ views.home = function (req, res) { }) } +views.docs = function (req, res){ + var name = req.params.name || "index" + + if (name === "new") { + res.render('docs', { + doc: { name: "new" }, + content: null, + isNew: true + }) + return + } + + Documentation.findOne({ name: name }, function(err, doc) { + if (err || ! doc) { + return res.render('docs', { + doc: { name: util.sanitize(name) }, + content: null, + isNew: true + }) + } + res.render('docs', { + doc: doc, + content: marked(doc.body), + isNew: false + }) + }) +} + views.profile = function (req, res) { var username = req.params[0] || req.user.username if (username) { @@ -43,4 +82,16 @@ views.profile = function (req, res) { } } +views.staff = { + index: function(req, res){ + res.render('staff') + }, + bless: function(req, res){ + req.user.isStaff = true + req.user.save(function(){ + res.redirect("/staff") + }) + }, +} + module.exports = views diff --git a/views/docs.ejs b/views/docs.ejs new file mode 100644 index 0000000..601f40f --- /dev/null +++ b/views/docs.ejs @@ -0,0 +1,49 @@ + + + + vvalls + [[ include partials/meta ]] + + +
+ [[ include partials/header ]] + +
+ + [[ if (! isNew) { ]] +

[[- doc.displayName ]]

+ + [[ if (user.isStaff) { ]] + Edit this document + [[ include staff/edit-docs ]] + [[ } ]] + +
+ [[- content ]] +
+ + [[ } else { ]] + [[ if (doc.name !== "new") { ]] +

404!

+

+ [[- doc.name ]] not found! +

+ [[ } ]] + + [[ if (user.isStaff) { ]] +

+ Create this document +

+ [[ include staff/edit-docs ]] + [[ } ]] + + [[ } ]] + + [[ include partials/sign-in ]] + [[ include partials/footer ]] + +
+ + +[[ include partials/scripts ]] + diff --git a/views/home.ejs b/views/home.ejs index 670dcbe..3ed56ef 100755 --- a/views/home.ejs +++ b/views/home.ejs @@ -45,7 +45,7 @@ View More - [[ include partials/signin ]] + [[ include partials/sign-in ]] [[ include partials/footer ]] diff --git a/views/modal.ejs b/views/modal.ejs index b47f6aa..aa75f72 100644 --- a/views/modal.ejs +++ b/views/modal.ejs @@ -10,7 +10,7 @@
- [[ include partials/signin ]] + [[ include partials/sign-in ]] [[ include projects/new-project ]] [[ include projects/edit-project ]] [[ include partials/footer ]] diff --git a/views/partials/edit-profile.ejs b/views/partials/edit-profile.ejs index f5807ee..96e9da4 100644 --- a/views/partials/edit-profile.ejs +++ b/views/partials/edit-profile.ejs @@ -49,13 +49,15 @@
- +
  • -

    please choose a picture at least 500px wide

    +

    please choose a picture at least 500px wide

    +
    +
  • diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index df48cf3..a40a873 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -1,7 +1,7 @@