From 7df4644f122389a04268ff25416a2d1b9829660f Mon Sep 17 00:00:00 2001 From: Tom Boutell Date: Mon, 31 Dec 2012 13:31:08 -0500 Subject: [PATCH] Permissions implemented and tested, documentation completed, getArea method added, road map migrated into the README.md file --- .gitignore | 2 - README.md | 245 +++++++++++++++++++++++++++++++++++++-- jot.js | 161 +++++++++++++++++-------- todo.txt | 7 -- views/area.html | 10 +- views/editArea.html | 1 + wiki/.gitignore | 1 + wiki/public/css/wiki.css | 13 +++ wiki/views/base.html | 20 ++-- wiki/views/layout.html | 11 +- wiki/views/page.html | 19 ++- wiki/wiki.js | 85 +++++++++----- 12 files changed, 450 insertions(+), 125 deletions(-) delete mode 100644 todo.txt diff --git a/.gitignore b/.gitignore index 56e8950..4fa9d14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ *.DS_Store node_modules -public/uploads -temp/uploadfs diff --git a/README.md b/README.md index f1a06ee..6866145 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,261 @@ # Jot -Jot is a rich content editor. In addition to rich text, jot allows you to add rich media to documents. +Jot is a rich content and rich text editor. In addition to rich text, jot allows you to add rich media to documents. Jot also includes simple facilities for storing your rich content areas in MongoDB and fetching them back again. + +[You can try a live demo of the Jot Wiki sample app here.](http://jotwiki.boutell.com/) (Note: the demo site resets at the top of the hour.) Jot introduces "widgets," separate editors for rich media items like photos, videos, pullquotes and code samples. Jot's widgets handle these items much better than a rich text editor on its own. -Jot also supports floating content properly, including wrapping text around images and video. Unlike other rich text editors, Jot addresses the usability problems that go with floating content. Jot users can see exactly where to add text above the floated element and where to add text after it so that it wraps around. Jot users can also easily select, cut, copy and paste rich content widgets exactly as if they were part of the text, without breaking them. You can even copy a video widget from one page of a site to another. +Jot also supports floating content properly, including wrapping text around images and video. Unlike other rich text editors, Jot addresses the usability problems that go with floating content. Jot users can see exactly where to add text above the floated element and where to add text after it so that it wraps around. When editing, Jot displays positioning arrows before and after rich media elements that make it clear where they connect to the text and ensure it is always possible to add content above and below them. Jot users can also easily select, cut, copy and paste rich content widgets exactly as if they were part of the text, without breaking them. You can even copy a video widget from one page of a site to another. + +In summary, Jot's rich media widgets are independently edited, but they are also part of the flow of a rich text document, with robust support for floating them if desired and displaying them at various well-chosen sizes rather than arbitrary sizes that may not suit your design. This is the major advantage of Jot over other rich text editors. + +Jot also provides server-side node.js code providing a back end for all of the above: storing uploaded files, validating rich content, and storing rich content areas in MongoDB. + +## Acknowledgements + +Jot was inspired by the [Apostrophe](http://apostrophenow.org) content management system. The lead developer of Jot works on Apostrophe at [P'unk Avenue](http://punkave.com). Jot is under consideration as a component of Apostrophe 2, a Node-based next-generation version of Apostrophe. -To sum up: Jot's rich media widgets are independently edited, but they are also part of the flow of a rich text document, with robust support for floating them if desired and displaying them at various well-chosen sizes. This is the major advantage of Jot over other rich text editors. +Jot wouldn't be nearly so awesome without [nunjucks](http://nunjucks.jlongster.com/), [Express](http://expressjs.com/) and [Rangy](http://code.google.com/p/rangy/). Please don't go anywhere near HTML's `contentEditable` attribute without Rangy. And a hip flask. ## Requirements Jot is intended to work in all major browsers from IE7 up, with best results in modern browsers such as recent versions of Firefox and Chrome. Of course the content you create with Jot could work with any browser. -Jot's server-side components are built in Node. Although in principle browser-side components of Jot could talk to other languages, right now a close partnership with Node code on the server is driving the flow of development (in other words, I like it). +Jot's server-side components are built in Node and require Express 3.0. Although in principle browser-side components of Jot could talk to other languages, right now a close partnership with Node code on the server is driving the flow of development. + +Jot's server-side code uses uploadfs to store media files. uploadfs allows you to decide whether to keep them in a local filesystem, Amazon S3 or a custom backend. + +Jot does not require any external CSS framework. Jot's internal templates are processed with Nunjucks, which is awesome, but your Node application does not have to use Nunjucks. + +## Adding Editable Areas With Jot: A Simple Example + +### Configuring Jot + +You'll need to `npm install` jot in your project, as well as `uploadfs`, `mongodb` and `express`. You might consider using [http://github.com/boutell/appy](appy), which eases the burden of setting up a typical Express app that supports all the usual stuff. But it's not a requirement. + +Here's the `initJot` function of the provided sample application `wiki.js`. Notice this function invokes a callback when it's done. `wiki.js` makes good use of the `async` module to carry out its initialization tasks elegantly. + + function initJot(callback) { + return jot.init({ + files: appy.files, + areas: appy.areas, + app: app, + uploadfs: uploadfs, + permissions: jotPermissions, + }, callback); + } + +"What are `appy.files` and `appy.areas`?" MongoDB collections. You are responsible for connecting to MongoDB and creating these two collection objects, then providing them to Jot. (Hint: it's pretty convenient with Appy.) For best results, `areas` should have a unique index on the `slug` property. + +"What is `app`?" `app` is your Express 3.0 app object. See the Express documentation for how to create an application. Again, Appy helps here. + +"What is `uploadfs`?" [http://github.com/boutell/uploadfs](uploadfs) is a module that conveniently stores uploaded files in either the local filesystem or S3, whichever you like. See `wiki.js` for an example of configuration. You'll create an `uploadfs` instance, initialize it and then pass it in here. + +"What is `jotPermissions`?" A function you define to decide who is allowed to edit content. If you skip this parameter, Jot allows everyone to edit everything - not safe in production of course, but convenient in the early development stages. + +To understand configuration in detail, you should really check out `wiki.js`. + +### Making Sure Jot Is In The Browser + +Before we can add rich content areas to a webpage with Jot, we need to make sure Jot's CSS, JavaScript and widget editor templates are present in the page. Jot adds convenience functions to your template language to accomplish that without a fuss. + +You will also need to make appropriate browser-side JavaScript calls to enable the "edit" buttons of areas and to enable video players in Jot content. + +Here's a simple `layout.html` Nunjucks template that includes everything Jot needs: + + + + + {{ jotStylesheets() }} + + {{ jotScripts() }} + + + {% block body %} + {% endblock %} + {{ jotTemplates() }} + + + + +Note the `body` block, which can be overridden in any template that `extend`s this template. Jade has an identical feature. + +"What if I hate the way you're loading CSS and JavaScript? What if I hate the version of jQuery you're loading?" Don't use the convenience functions. Instead examine Jot's `scripts.html` and `stylesheets.html` templates and make sure you are loading the same functionality. + +"Do I have to load all this stuff if I am certain the user has no editing privileges?" No. To make your pages lighter, if you know the user won't be editing, you can get by with just `content.css`, jQuery and `content.js`. We haven't spent much time testing this scenario yet. Pull requests to make it more convenient are welcome. + +### Adding Editable Areas To Your Templates + +The easiest way to add Jot-powered editable rich content areas to your Node Express 3.0 project is to use Jot's `jotArea` function, which is made available to your Express templates when you configure Jot. Here's a simple example taken from the `wiki` sample site provided with Jot: + + {{ jotArea({ slug: 'main', content: main, edit: true }) }} + +This is from a Nunjucks template. If you're using Twig, you'll write: + +!= jotArea({ slug: 'main', content: content, edit: true }) + +"What does `slug` mean?" Each area needs a unique "slug" to distinguish it from other editable content areas on your site. Many sites have slugs named `header`, `footer`, `sidebar` and the like. + +"Where does `content` come from?" Good question. You are responsible for fetching the content as part of the Express route code that renders your template. You do this with Jot's `getArea` method. + +Naturally `getArea` is asynchronous: -Jot's server-side code uses uploadfs to store media files. uploadfs allows you to decide whether to keep them in a local filesystem, Amazon S3 or a custom backend. + app.get('/', function(req, res) { + jot.getArea('main', function(err, area) { + return res.render('home', { content: area ? area.content : '' }); + }); + }); -Jot currently requires Twitter Bootstrap and FontAwesome. That will probably change, because Bootstrap is a big requirement for a rich content editor to force on an entire project. Jot definitely requires Underscore, jQuery and Rangy. Rangy makes working with contentEditable something a sane person would even consider. +Note the code that checks whether `area` is actually set before attempting to access its content. If no area with that slug has ever been saved, the `area` callback parameter will be null. -## Coding With Jot +Also note that there is an `err` parameter to the callback. Real-world applications should check for errors (and the provided `wiki.js` sample does). +## Grouping Areas Into "Pages" + +"What if I'm building a site with lots of pages? Each page might have two or three areas. Is there an efficient way to get them all at once?" + +Sure! Jot provides a `getAreasForPage` method for this purpose. + +If you pass the slug `/about` to `getAreasForPage`, and areas exist with the following slugs: + + /about:main + /about:sidebar + /about:footer + +Then `getAreasForPage` will fetch all of them and deliver an object like this: + + { + main: { + content: "rich content markup for main" + }, + sidebar: { + content: "rich content markup for sidebar" + }, + footer: { + content: "rich content markup for footer" + }, + } + +The provided `wiki.js` sample site uses this method to deliver complete Wiki pages: + + app.get('*', function(req, res) { + var slug = req.params[0]; + jot.getAreasForPage(slug, function(e, info) { + return res.render('page.html', { + slug: info.slug, + main: info.main ? info.main.content : '', + sidebar: info.sidebar ? info.sidebar.content : '' + }); + }); + }); + +This is a simplified example. The actual code in `wiki.js` uses middleware to summon the page and footer information into the `req` object. There are two middleware functions, one for the page contents and one for a globally shared footer. It's a good strategy to consider. Perhaps we'll add some standard middleware functions like this soon: + + app.get('*', + function(req, res, next) { + // Get content for this page + req.slug = req.params[0]; + jot.getAreasForPage(req.slug, function(e, info) { + if (e) { + console.log(e); + return fail(req, res); + } + req.page = info; + return next(); + }); + }, + function(req, res, next) { + // Get the shared footer + jot.getArea('footer', function(e, info) { + if (e) { + console.log(e); + return fail(req, res); + } + req.footer = info; + return next(); + }); + }, + function (req, res) { + return res.render('page.html', { + slug: req.slug, + main: req.page.main ? req.page.main.content : '', + sidebar: req.page.sidebar ? req.page.sidebar.content : '', + user: req.user, + edit: req.user && req.user.username === 'admin', + footer: req.footer ? req.footer.content : '' + }); + } + ); + +Now `page.jade` can call `jotArea` to render the areas: + + {{ jotArea({ slug: slug + ':main', content: main, edit: true }) }} + {{ jotArea({ slug: slug + ':sidebar', content: sidebar, edit: true }) }} + +## Enforcing Permissions + +You can hide edit buttons by passing `edit: false` to the `jotArea` function, and you should if the user doesn't have that privilege. But that doesn't actually prevent clever users from making form submissions that update areas. By default, everyone can edit everything if they know the URL. + +Of course this is not what you want. Fortunately it is very easy to pass your own custom permissions callback to Jot. + +When calling init(), just set the `permissions` option to a function that looks like this: + + function permissions(req, action, fileOrSlug, callback) { ... } + +Once you've decided whether `req.user` should be allowed to carry out `action`, invoke `callback` with `null` to let the user complete the action, or with an error to forbid the action. + +Currently the possible actions are `edit-area` and `edit-media`. `edit-area` calls will include the slug of the area as the third parameter. `edit-media` calls for existing files may include a `file` object retrieved from Jot's database, with an "owner" property set to the _id, id or username property of `req.user` at the time the file was last edited. `edit-media` calls with no existing file parameter also occur, for new file uploads. + +A common case is to restrict editing to a single user: + + function permissions(req, action, fileOrSlug, callback) { + if (req.user && (req.user.username === 'admin')) { + // OK + return callback(null); + } else { + return callback('Forbidden'); + } + } + +You can see an example of this pattern in `wiki.js`. + +## Roadmap + +Jot is a work in progress. Certainly the following things need to improve: + +* Developers should be able to leverage everything else in Jot without actually storing areas via the provided API. In particular, if I'm creating a blog post editor, I want an area to be part of it, but I don't want to store it separately or be forced to have a separate "save" button for it. +* Developers should be able to inject their own storage layer that satisfies an API. I think. Maybe. (Requiring MongoDB has its benefits.) +* The built-in oembed proxy should cache thumbnails and markup. +* The built-in oembed proxy should have a whitelist of sites whose oembed codes are not XSS attack vectors. +* Server-side content validation should be much smarter. +* It should be possible to fetch summaries of areas conveniently and quickly. +* It should be possible to fetch just certain rich media from areas conveniently and quickly. +* "Dynamic" widgets that require server-side interaction before the page is sent should be supported. +* There should be a server-side render method, in place of the existing client-side JavaScript to turn video placeholders into videos. +* Server-side renders should be cached, for a minimum lifetime equal to that of the widget with the shortest cache lifetime. +* A separate module that complements Jot by managing "pages" in a traditional page tree should be developed. ## Conclusion and Contact Information -That's it! That should be all you need. If not, open an issue on github and we'll talk. +That's it! You should have everything you need to enable rich content editing on your sites. If not, open an issue on github and we'll talk. See also the above roadmap. Tom Boutell +[tom@punkave.com](mailto:tom@punkave.com) + +[P'unk Avenue](http://punkave.com) + [http://github.com/boutell/jot](http://github.com/boutell/jot) +[@boutell](http://twitter.com/boutell) + [justjs.com](http://justjs.com) -[@boutell](http://twitter.com/boutell) -[tom@punkave.com](mailto:tom@punkave.com) diff --git a/jot.js b/jot.js index e6f8e5c..2643edb 100644 --- a/jot.js +++ b/jot.js @@ -17,13 +17,28 @@ module.exports = function() { function jot() { var self = this; - var app, files, areas, uploadfs, nunjucksEnv; + var app, files, areas, uploadfs, nunjucksEnv, permissions; self.init = function(options, callback) { app = options.app; files = options.files; areas = options.areas; uploadfs = options.uploadfs; + permissions = options.permissions; + + // Default is to allow anyone to do anything. + // You will probably want to at least check for req.user. + // Possible actions are edit-area and edit-media. + // edit-area calls will include a slug as the third parameter. + // edit-media calls for existing files may include a file, with an + // "owner" property set to the id or username property of req.user + // at the time the file was last edited. edit-media calls with + // no existing file parameter also occur, for new file uploads. + if (!permissions) { + permissions = function(req, action, fileOrSlug, callback) { + return callback(null); + }; + } nunjucksEnv = new nunjucks.Environment(new nunjucks.FileSystemLoader(__dirname + '/views')); nunjucksEnv.addFilter('json', function(data) { @@ -58,7 +73,7 @@ function jot() { app.get('/jot/file-iframe/:id', validId, function(req, res) { var id = req.params.id; - render(res, 'fileIframe.html', { id: id, error: false, uploaded: false }); + return render(res, 'fileIframe.html', { id: id, error: false, uploaded: false }); }); // Deliver details about a previously uploaded file as a JSON response @@ -71,8 +86,13 @@ function jot() { res.send("Not Found"); return; } - file.url = uploadfs.getUrl() + '/images/' + id; - res.send(file); + permissions(req, 'edit-media', file, function(err) { + if (err) { + return forbid(res); + } + file.url = uploadfs.getUrl() + '/images/' + id; + return res.send(file); + }); } }); @@ -92,10 +112,13 @@ function jot() { var info; function gotExisting(err, existing) { - // This is a good place to add permissions checks - - // Let uploadfs do the heavy lifting of scaling and storage to fs or s3 - uploadfs.copyImageIn(src, '/images/' + id, update); + permissions(req, 'edit-media', existing, function(err) { + if (err) { + return forbid(res); + } + // Let uploadfs do the heavy lifting of scaling and storage to fs or s3 + return uploadfs.copyImageIn(src, '/images/' + id, update); + }); } function update(err, infoArg) { @@ -107,6 +130,13 @@ function jot() { info.name = slugify(file.name); info.createdAt = new Date(); + // Do our best to record who owns this file to allow permissions + // checks later. If req.user exists and has an _id, id or username property, + // record that + if (req.user) { + info.owner = req.user._id || req.user.id || req.user.username; + } + files.update({ _id: info._id }, info, { upsert: true, safe: true }, inserted); } @@ -126,50 +156,60 @@ function jot() { app.get('/jot/edit-area', function(req, res) { var slug = req.query.slug; - var isNew = false; - if (!slug) { - return notfound(req, res); - } else { - areas.findOne({ slug: slug }, function(err, area) { - if (!area) { - var area = { - slug: slug, - _id: generateId(), - content: null, - isNew: true - }; - area.wid = 'w-' + area._id; - return render(res, 'editArea.html', area); - } - else - { - area.wid = 'w-' + area._id; - area.isNew = false; - return render(res, 'editArea.html', area); - } - }); - } + permissions(req, 'edit-area', slug, function(err) { + if (err) { + return forbid(res); + } + var isNew = false; + if (!slug) { + return notfound(req, res); + } else { + areas.findOne({ slug: slug }, function(err, area) { + if (!area) { + var area = { + slug: slug, + _id: generateId(), + content: null, + isNew: true + }; + area.wid = 'w-' + area._id; + return render(res, 'editArea.html', area); + } + else + { + area.wid = 'w-' + area._id; + area.isNew = false; + return render(res, 'editArea.html', area); + } + }); + } + }); }); app.post('/jot/edit-area', function(req, res) { var slug = req.body.slug; - var area = { - slug: req.body.slug, - content: validateContent(req.body.content) - }; + permissions(req, 'edit-area', slug, function(err) { + if (err) { + return forbid(res); + } + var area = { + slug: req.body.slug, + content: validateContent(req.body.content) + }; - // TODO: validate content. XSS, tag balancing, allowed tags and attributes, - // sensible use of widgets. All that stuff A1.5 does well + // TODO: validate content. XSS, tag balancing, allowed tags and attributes, + // sensible use of widgets. All that stuff A1.5 does well - areas.update({ slug: area.slug }, area, { upsert: true, safe: true }, updated); + areas.update({ slug: area.slug }, area, { upsert: true, safe: true }, updated); - function updated(err) { - if (err) { - console.log(err); - return notfound(req, res); + function updated(err) { + if (err) { + console.log(err); + return notfound(req, res); + } + res.send(area.content); } - res.send(area.content); - } + }); }); // A simple oembed proxy to avoid cross-site scripting restrictions. @@ -214,7 +254,23 @@ function jot() { return callback(null); }; - // Returns an object with a property for each named area + // Invokes the callback with an error if any, and if no error, + // the area object requested if it exists. The area object is + // guaranteed to have `slug` and `content` properties. The + // `content` property contains rich content markup ready to + // display in the browser. + + self.getArea = function(slug, callback) { + areas.findOne({ slug: slug }, function(err, area) { + if (err) { + return callback(err); + } + return callback(null, area); + }); + }; + + // Invokes the callback with an error if any, and if no error, + // an object with a property for each named area // matching the given page slug, plus a slug property. // Very handy for rendering pages and page-like collections // of areas. A simple convention is used to group areas into @@ -225,10 +281,10 @@ function jot() { // string at the beginning can use indexes). self.getAreasForPage = function(slug, callback) { - var pattern = new RegExp('^' + RegExp.quote(slug) + ':', 'i'); + var pattern = new RegExp('^' + RegExp.quote(slug) + ':'); areas.find({ slug: pattern }).toArray(function(err, areaDocs) { if (err) { - return callback('Not found'); + return callback(err); } var data = {}; // Organize the areas by name @@ -238,7 +294,6 @@ function jot() { data[results[1]] = area; } }); - data.slug = slug; return callback(null, data); }); }; @@ -256,6 +311,11 @@ function jot() { res.send('500 error, URL was ' + req.url); } + function forbid(res) { + res.statusCode = 403; + res.send('Forbidden'); + } + function notfound(req, res) { res.statusCode = 404; res.send('404 not found error, URL was ' + req.url); @@ -334,10 +394,9 @@ function jot() { // what we can do with jQuery on the server side. Note that // browser side validation is not enough because browsers are // inherently not trusted - var $content = jQuery(content); - $content.find('.jot-edit-widget').remove(); var wrapper = jQuery('
'); - wrapper.append($content); + wrapper.html(content); + wrapper.find('.jot-edit-widget').remove(); return wrapper.html(); } } diff --git a/todo.txt b/todo.txt deleted file mode 100644 index dcf993f..0000000 --- a/todo.txt +++ /dev/null @@ -1,7 +0,0 @@ -Isolate the truly wiki-sample-app-specific stuff in wiki.js -Move the rest to jot.js -Move the wiki-specific views to wiki/views -Leave the jot views in views -Etc. -Reintroduce permissions hooks, implement an extremely simple example - diff --git a/views/area.html b/views/area.html index e81cc5c..de07336 100644 --- a/views/area.html +++ b/views/area.html @@ -1,9 +1,11 @@
-
- Edit -
-
+ {% if edit %} +
+ Edit +
+
+ {% endif %}
{{ content }}
diff --git a/views/editArea.html b/views/editArea.html index b710418..f39ca6c 100644 --- a/views/editArea.html +++ b/views/editArea.html @@ -29,6 +29,7 @@ Save Cancel
+
- {% endblock %} + + + {# Must be present in the page in order to use jot's widget editors #} + {{ jotTemplates() }} diff --git a/wiki/views/layout.html b/wiki/views/layout.html index 101df2b..405d2b9 100644 --- a/wiki/views/layout.html +++ b/wiki/views/layout.html @@ -3,6 +3,14 @@ {# Document with typical page structure #} {% block body %} +

Jot Wiki

@@ -28,6 +36,3 @@

Jot Wiki

{% endblock %} -{% block domready %} -{{ super() }} -{% endblock %} diff --git a/wiki/views/page.html b/wiki/views/page.html index 934deaa..9be8f39 100644 --- a/wiki/views/page.html +++ b/wiki/views/page.html @@ -1,23 +1,18 @@ {% extends "layout.html" %} -{% block before %} -{{ super() }} -{# Must be present in the page where we plan to use Jot #} -{# See also jotStylesheets and jotScripts loaded in base.jade #} -{{ jotTemplates() }} -{% endblock %} - {# Output the current content of the page, with buttons to edit it #} {% block main %} - {{ jotArea({ slug: slug + ':main', name: 'main', content: main }) }} +

Hint: to make more Wiki pages, just visit them by editing the URL in the address bar and pressing Enter.

+ {{ jotArea({ slug: slug + ':main', content: main, edit: edit }) }} {% endblock %} {% block sidebar %} - {{ jotArea({ slug: slug + ':sidebar', name: 'sidebar', content: sidebar }) }} + {{ jotArea({ slug: slug + ':sidebar', content: sidebar, edit: edit }) }} {% endblock %} -{% block domready %} - jot.enableAreas(); - jot.enablePlayers(); +{% block footer %} + {# Shared footer, no page URL in the slug #} + {{ jotArea({ slug: 'footer', content: footer, edit: edit }) }} + {{ super() }} {% endblock %} diff --git a/wiki/wiki.js b/wiki/wiki.js index e2f3190..7dc6489 100644 --- a/wiki/wiki.js +++ b/wiki/wiki.js @@ -15,21 +15,18 @@ var options = { env.express(app); }, - // auth: { - // strategy: 'local', - // options: { - // users: { - // admin: { - // username: 'admin', - // password: 'demo', - // id: 'admin' - // } - // } - // } - // }, - - // Lock the /user prefix to require login - // locked: '/user', + auth: { + strategy: 'local', + options: { + users: { + admin: { + username: 'admin', + password: 'demo', + id: 'admin' + } + } + } + }, sessionSecret: 'whatever', @@ -102,7 +99,8 @@ function initJot(callback) { files: appy.files, areas: appy.areas, app: app, - uploadfs: uploadfs + uploadfs: uploadfs, + permissions: jotPermissions, }, callback); } @@ -117,16 +115,41 @@ function setRoutes(callback) { // Note the leading slash is included. Express automatically supplies / // if the URL is empty - app.get('*', function(req, res) { - var slug = req.params[0]; - jot.getAreasForPage(slug, function(e, info) { - if (e) { - console.log(e); - return fail(req, res); - } - return res.render('page.html', { slug: info.slug, main: info.main ? info.main.content : '', sidebar: info.sidebar ? info.sidebar.content : '' }); - }); - }); + app.get('*', + function(req, res, next) { + // Get content for this page + req.slug = req.params[0]; + jot.getAreasForPage(req.slug, function(e, info) { + if (e) { + console.log(e); + return fail(req, res); + } + req.page = info; + return next(); + }); + }, + function(req, res, next) { + // Get the shared footer + jot.getArea('footer', function(e, info) { + if (e) { + console.log(e); + return fail(req, res); + } + req.footer = info; + return next(); + }); + }, + function (req, res) { + return res.render('page.html', { + slug: req.slug, + main: req.page.main ? req.page.main.content : '', + sidebar: req.page.sidebar ? req.page.sidebar.content : '', + user: req.user, + edit: req.user && req.user.username === 'admin', + footer: req.footer ? req.footer.content : '' + }); + } + ); return callback(null); } @@ -148,3 +171,13 @@ function getTempPath(path) { return __dirname + '/temp' + path; } +// Allow only the admin user to edit anything with Jot + +function jotPermissions(req, action, fileOrSlug, callback) { + if (req.user && (req.user.username === 'admin')) { + // OK + return callback(null); + } else { + return callback('Forbidden'); + } +}