| @@ -5,9 +5,25 @@ var formidable = require("formidable"); | |||
| var crypto = require("crypto"); | |||
| var basepath = "/admin/api/"; | |||
| var slideshow; | |||
| exports.init = function(_slideshow) { | |||
| slideshow = _slideshow; | |||
| } | |||
| var tokens = {}; | |||
| function pad(str, n) { | |||
| if (str.length >= n) | |||
| return str; | |||
| var missing = str.length; | |||
| for (var i = 0; i < n - missing; ++i) | |||
| str = "0" + str; | |||
| return str; | |||
| } | |||
| // Used in every method handler to make sure the correct arguments are provided | |||
| function hasargs(query, respond, expected) { | |||
| var missing = []; | |||
| @@ -130,12 +146,79 @@ var methods = { | |||
| }); | |||
| form.parse(req, (err, fields, files) => { | |||
| if (err) | |||
| return respond(err); | |||
| }); | |||
| form.on("error", err => { | |||
| respond(err); | |||
| }); | |||
| form.on("end", () => { | |||
| respond(); | |||
| }); | |||
| }, | |||
| // Create a slide | |||
| // Lots of synchronous fs stuff, we don't want races | |||
| slide_create: function(query, conf, req, respond) { | |||
| var dirs; | |||
| try { | |||
| dirs = fs.readdirSync(conf.slides); | |||
| } catch (err) { | |||
| return respond(err); | |||
| } | |||
| dirs = dirs.sort(); | |||
| var biggest = dirs[dirs.length - 1]; | |||
| var newId = pad((parseInt(biggest) + 1).toString(), biggest.length); | |||
| var path = pathlib.join(conf.slides, newId); | |||
| try { | |||
| fs.mkdirSync(path); | |||
| fs.writeFileSync(pathlib.join(path, "index.md"), ""); | |||
| } catch (err) { | |||
| return respond(err); | |||
| } | |||
| slideshow.updateSlides(); | |||
| respond(null, newId); | |||
| }, | |||
| // Delete a slide | |||
| // Also synchronous fs stuff | |||
| slide_delete: function(query, conf, req, respond) { | |||
| if (!hasargs(query, respond, [ "slide" ])) return; | |||
| var path = pathlib.join(conf.slides, query.slide); | |||
| var files; | |||
| try { | |||
| files = fs.readdirSync(path); | |||
| } catch (err) { | |||
| return respond(err); | |||
| } | |||
| for (var f of files) { | |||
| try { | |||
| fs.unlinkSync(pathlib.join(path, f)); | |||
| } catch (err) { | |||
| return respond(err); | |||
| } | |||
| } | |||
| try { | |||
| fs.rmdirSync(path); | |||
| } catch (err) { | |||
| return respond(err); | |||
| } | |||
| respond(); | |||
| } | |||
| } | |||
| exports.canServe = function(parts) { | |||
| // Temporary, while working on stuff | |||
| var name = parts.pathname.replace(basepath, ""); | |||
| return methods[name] !== undefined || name === "login"; | |||
| @@ -163,7 +246,18 @@ function loginHandler(conf, req, respond) { | |||
| respond(null, token); | |||
| } | |||
| function validateToken(req) { | |||
| var token = req.headers["session-token"]; | |||
| var cookie = req.headers.cookie; | |||
| if (!cookie) | |||
| return false; | |||
| var token; | |||
| for (var c of cookie.split(/;\s*/)) { | |||
| var parts = c.split("="); | |||
| if (parts[0] === "token") { | |||
| token = parts[1]; | |||
| break; | |||
| } | |||
| } | |||
| if (!token) | |||
| return false; | |||
| @@ -205,12 +205,31 @@ function Slideshow(dir, changeInterval) { | |||
| } | |||
| } | |||
| // This function starts the slideshow and goes through the slides | |||
| // one by one. When done, it starts again by calling this function again. | |||
| function init() { | |||
| self.updateSlides = updateSlides; | |||
| function updateSlides() { | |||
| slides = fs.readdirSync(dir) | |||
| .sort() | |||
| .map(file => Slide(pathlib.join(dir, file))); | |||
| } | |||
| self.serve = serve; | |||
| function serve(parts, res) { | |||
| for (var slide of slides) { | |||
| if (slide.name === parts.pathname.substr(1, slide.name.length)) { | |||
| slide.serveFiles(parts, res); | |||
| return; | |||
| } | |||
| } | |||
| // We haven't found any matching slides | |||
| res.writeHead(404); | |||
| res.end("404"); | |||
| } | |||
| // This function starts the slideshow and goes through the slides | |||
| // one by one. When done, it starts again by calling this function again. | |||
| function init() { | |||
| updateSlides(); | |||
| slideIndex = 0; | |||
| currentSlide = slides[slideIndex]; | |||
| @@ -14,6 +14,7 @@ var index = fs.readFileSync("web/index.html", "utf-8") | |||
| .replace(/<<transition_time>>/g, conf.transition_time); | |||
| var slideshow = Slideshow(conf.slides, conf.interval); | |||
| admin.init(slideshow); | |||
| function onexit(code) { | |||
| console.log("exiting", code); | |||
| @@ -53,22 +54,7 @@ function handler(req, res) { | |||
| // Serve slide files | |||
| } else { | |||
| var served = false; | |||
| for (var slide of slideshow.getSlides()) { | |||
| // If client requests /{slide-name}/* | |||
| if (slide.name === pathname.substr(1, slide.name.length)) { | |||
| slide.serveFiles(parts, res); | |||
| served = true; | |||
| break; | |||
| } | |||
| } | |||
| if (!served) { | |||
| res.writeHead(404); | |||
| res.end("404"); | |||
| } | |||
| slideshow.serve(parts, res); | |||
| } | |||
| } | |||
| @@ -9,14 +9,12 @@ | |||
| window.error = error; | |||
| window.$$ = $$; | |||
| var sessToken = ""; | |||
| function apiLogin(pass, cb) { | |||
| var extraHeads = [ [ "Session-Pass", pass ] ]; | |||
| api("login", {}, (err, token) => { | |||
| if (token) { | |||
| sessToken = token; | |||
| document.cookie = "token = " + token; | |||
| cb(true); | |||
| } else { | |||
| cb(false); | |||
| @@ -32,13 +30,13 @@ | |||
| function api(method, args, cb, extraHeads) { | |||
| var heads = new Headers(); | |||
| heads.append("Session-Token", sessToken); | |||
| if (extraHeads) | |||
| extraHeads.forEach(h => heads.append(h[0], h[1])); | |||
| var opts = { | |||
| method: "POST", | |||
| headers: heads | |||
| headers: heads, | |||
| credentials: "same-origin" | |||
| }; | |||
| fetch("/admin/api/"+method+argstr(args), opts) | |||
| .then(response => response.json()) | |||
| @@ -89,6 +89,7 @@ | |||
| #root.edit #fileList, | |||
| #root.edit #slide > .uploader, | |||
| #root.edit #slide > .delete, | |||
| #root.edit #html { | |||
| margin-bottom: 24px; | |||
| } | |||
| @@ -18,27 +18,53 @@ | |||
| } | |||
| function viewLogin(root) { | |||
| function login(first) { | |||
| var pass; | |||
| do { | |||
| if (first) | |||
| pass = prompt("Password?"); | |||
| else | |||
| pass = prompt("Incorrect password."); | |||
| } while (!pass); | |||
| var msg; | |||
| var pwd; | |||
| elem("form", {}, [ | |||
| msg = elem("div", { | |||
| className: "msg", | |||
| innerHTML: "Log In" | |||
| }), | |||
| pwd = elem("input", { | |||
| type: "password", | |||
| placeholder: "Password" | |||
| }), | |||
| elem("button", { | |||
| innerHTML: "Log In" | |||
| }) | |||
| ]).on("submit", evt => { | |||
| evt.preventDefault(); | |||
| apiLogin(pass, success => { | |||
| apiLogin(pwd.value, success => { | |||
| if (success) | |||
| setView("main"); | |||
| else | |||
| login(false); | |||
| msg.innerHTML = "Incorrect password." | |||
| }); | |||
| } | |||
| login(true); | |||
| }).appendTo(root); | |||
| } | |||
| function viewMain(root) { | |||
| // New slide button | |||
| elem("button", { | |||
| className: "newSlide", | |||
| innerHTML: "New Slide" | |||
| }).on("click", () => { | |||
| api("slide_create", {}, (err, id) => { | |||
| if (err) | |||
| return error(err); | |||
| setView("edit", [id]); | |||
| }); | |||
| }).appendTo(root); | |||
| var slidesEl = elem("div", { | |||
| className: "slides" | |||
| }).appendTo(root); | |||
| // Get a list of the slides | |||
| api("list_slides", {}, (err, slides) => { | |||
| if (err) | |||
| @@ -64,7 +90,7 @@ | |||
| elem("div", { | |||
| className: "overlay" | |||
| }) | |||
| ]).appendTo(root); | |||
| ]).appendTo(slidesEl); | |||
| // Add 'disabled' to the class if it's disabled | |||
| api("slide_meta", { slide: s }, (err, res) => { | |||
| @@ -155,6 +181,21 @@ | |||
| slide: slide | |||
| }, populateFileList), | |||
| elem("button", { | |||
| className: "delete", | |||
| innerHTML: "Delete" | |||
| }).on("click", () => { | |||
| if (!confirm("Are you sure you want to delete slide "+slide+"?")) | |||
| return; | |||
| api("slide_delete", { slide: slide }, err => { | |||
| if (err) | |||
| return error(err); | |||
| setView("main"); | |||
| }); | |||
| }), | |||
| htmlTextEl = elem("textarea", { | |||
| id: "html" | |||
| }).on("keydown", debounce(() => { | |||
| @@ -213,7 +254,6 @@ | |||
| root.clear(); | |||
| root.className = name; | |||
| views[name].apply(null, args); | |||
| } | |||