From 0c5fa597e8c8325ec77a1061fae65497cea57f7b Mon Sep 17 00:00:00 2001 From: mntmn <lukas@mntmn.com> Date: Tue, 2 Jun 2020 20:47:58 +0200 Subject: [PATCH] Allow embedding of folders and access to folders to anonymous editors with edit_hash/spaceAuth links (#63) * add subspaces to be listed with edit_hash/spaceAuth authorization * remove dead code from api_helpers.js * add edit_hash authorization for requested space thumbnails * handle /s/:hash links in frontend router * set space_auth via a function, allow passing it to load_space * rename variable in /s/:hash router in backend * hide search, profile, breadcrumb in folders if not logged in, construct links to subspaces differently for anonymous editors --- middlewares/api_helpers.js | 24 -------- public/javascripts/backend.js | 8 +-- public/javascripts/spacedeck_routes.js | 15 +++++ public/javascripts/spacedeck_sections.js | 7 ++- public/javascripts/spacedeck_spaces.js | 10 +++- routes/api/spaces.js | 76 ++++++++++++++---------- routes/root.js | 12 ++-- views/partials/folders.html | 30 +++++----- 8 files changed, 99 insertions(+), 83 deletions(-) diff --git a/middlewares/api_helpers.js b/middlewares/api_helpers.js index 893f35f..551c7c8 100644 --- a/middlewares/api_helpers.js +++ b/middlewares/api_helpers.js @@ -4,27 +4,6 @@ require('../models/db'); var config = require('config'); const redis = require('../helpers/redis'); -// FIXME TODO object.toJSON() - -var saveAction = (actionKey, object) => { - if (object.constructor.modelName == "Space") - return; - - let attr = { - action: actionKey, - space: object.space_id || object.space, - user: object.user_id || object.user, - editor_name: object.editor_name, - object: object - }; - - /*let action = new Action(attr); - action.save(function(err) { - if (err) - console.error("saved create action err:", err); - });*/ -}; - module.exports = (req, res, next) => { res.header("Cache-Control", "no-cache"); @@ -36,21 +15,18 @@ module.exports = (req, res, next) => { if (!object) return; redis.sendMessage("create", model, object, req.channelId); this.status(201).json(object); - saveAction("create", object); }; res['distributeUpdate'] = function(model, object) { if (!object) return; redis.sendMessage("update", model, object, req.channelId); this.status(200).json(object); - saveAction("update", object); }; res['distributeDelete'] = function(model, object) { if (!object) return; redis.sendMessage("delete", model, object, req.channelId); this.sendStatus(204); - saveAction("delete", object); }; next(); diff --git a/public/javascripts/backend.js b/public/javascripts/backend.js index b57acae..552f3e6 100644 --- a/public/javascripts/backend.js +++ b/public/javascripts/backend.js @@ -6,6 +6,10 @@ var websocket = null; var channel_id = null; var space_auth = null; +function set_space_auth(hash) { + space_auth = hash; +} + function load_resource(method, path, data, on_success, on_error, on_progress) { var req = new XMLHttpRequest(); req.onload = function(evt,b,c) { @@ -44,18 +48,14 @@ function load_resource(method, path, data, on_success, on_error, on_progress) { } req.withCredentials = true; - req.open(method, api_endpoint+"/api"+path, true); if (api_token) { req.setRequestHeader("X-Spacedeck-Auth", api_token); } - if (space_auth) { - console.log("set space auth", space_auth); req.setRequestHeader("X-Spacedeck-Space-Auth", space_auth); } - if (channel_id) { req.setRequestHeader("X-Spacedeck-Channel", channel_id); } diff --git a/public/javascripts/spacedeck_routes.js b/public/javascripts/spacedeck_routes.js index 9d9b382..8e4fd4a 100644 --- a/public/javascripts/spacedeck_routes.js +++ b/public/javascripts/spacedeck_routes.js @@ -17,6 +17,21 @@ var SpacedeckRoutes = { }.bind(this) } ]); + + this.router.add([ + { + path: "/s/:hash", + handler: function(params, on_success) { + var parts = params.hash.split("-"); + if (path.length > 0) { + this.load_space(parts.slice(1).join("-"), on_success, null, parts[0]); + } else { + // FIXME error handling + on_success(); + } + }.bind(this) + } + ]); this.router.add([ { diff --git a/public/javascripts/spacedeck_sections.js b/public/javascripts/spacedeck_sections.js index 7a9fb70..df5daa8 100644 --- a/public/javascripts/spacedeck_sections.js +++ b/public/javascripts/spacedeck_sections.js @@ -405,7 +405,12 @@ var SpacedeckSections = { } if (space.space_type == "folder") return ""; - return "background-image:url('/api/spaces/"+space._id+"/png')"; + var query_string = ""; + if (space_auth) { + query_string+="?spaceAuth="+space.edit_hash; + } + + return "background-image:url('/api/spaces/"+space._id+"/png"+query_string+"')"; }, reset_artifact_filters: function() { diff --git a/public/javascripts/spacedeck_spaces.js b/public/javascripts/spacedeck_spaces.js index aee3217..f802efa 100644 --- a/public/javascripts/spacedeck_spaces.js +++ b/public/javascripts/spacedeck_spaces.js @@ -99,12 +99,16 @@ var SpacedeckSpaces = { }.bind(this), {value: dft || "Guest "+parseInt(10000*Math.random()), ok: __("ok"), cancel: __("cancel")}); }, - load_space: function(space_id, on_success, on_error) { + load_space: function(space_id, on_success, on_error, space_auth) { this.folder_spaces_filter=""; this.folder_spaces_search=""; - space_auth = get_query_param("spaceAuth"); - + if (space_auth) { + set_space_auth(space_auth); + } else { + set_space_auth(get_query_param("spaceAuth")); + } + this.embedded = !!(get_query_param("embedded")); var userReady = function() { diff --git a/routes/api/spaces.js b/routes/api/spaces.js index a74c22d..a8b3a38 100644 --- a/routes/api/spaces.js +++ b/routes/api/spaces.js @@ -42,7 +42,52 @@ var spaceMapping = { thumbnail_url: 1 }; +function listSpacesInFolder(req, res, parent_space_id) { + db.Space + .findOne({where: { + _id: parent_space_id + }}) + .then(function(space) { + if (space) { + function spacesForRole(role) { + if (role == "none") { + if (space.access_mode == "public") { + role = "viewer"; + } + } + if (role != "none") { + db.Space + .findAll({where:{ + parent_space_id: parent_space_id + }, include:[db.CreatorSafeInclude(db)]}) + .then(function(spaces) { + res.status(200).json(spaces); + }); + } else { + res.status(403).json({"error": "not authorized"}); + } + } + + if (req["spaceAuth"] && space.edit_hash) { + // TODO could be editor, too + spacesForRole("none"); + } else { + db.getUserRoleInSpace(space, req.user, spacesForRole); + } + } else { + res.status(404).json({"error": "space not found"}); + } + }); +} + router.get('/', function(req, res, next) { + + if (req.query.parent_space_id && req["spaceAuth"]) { + // list subspaces of a space authorized anonymously + listSpacesInFolder(req, res, req.query.parent_space_id); + return; + } + if (!req.user) { res.status(403).json({ error: "auth required" @@ -83,36 +128,7 @@ router.get('/', function(req, res, next) { } else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) { // list spaces in a folder - db.Space - .findOne({where: { - _id: req.query.parent_space_id - }}) - .then(function(space) { - if (space) { - db.getUserRoleInSpace(space, req.user, function(role) { - if (role == "none") { - if (space.access_mode == "public") { - role = "viewer"; - } - } - - if (role != "none") { - db.Space - .findAll({where:{ - parent_space_id: req.query.parent_space_id - }, include:[db.CreatorSafeInclude(db)]}) - .then(function(spaces) { - res.status(200).json(spaces); - }); - } else { - res.status(403).json({"error": "no authorized"}); - } - }); - } else { - res.status(404).json({"error": "space not found"}); - } - }); - + listSpacesInFolder(req, res, req.query.parent_space_id); } else { // list home folder and spaces/folders that the user is a member of diff --git a/routes/root.js b/routes/root.js index 4c6deb4..1f736c2 100644 --- a/routes/root.js +++ b/routes/root.js @@ -115,16 +115,16 @@ router.get('/t/:id', (req, res) => { res.redirect(path); }); -router.get('/s/:token', (req, res) => { - var token = req.params.token; - if (token.split("-").length > 0) { - token = token.split("-")[0]; +router.get('/s/:hash', (req, res) => { + var hash = req.params.hash; + if (hash.split("-").length > 0) { + hash = hash.split("-")[0]; } - db.Space.findOne({where: {"edit_hash": token}}).then(function (space) { + db.Space.findOne({where: {"edit_hash": hash}}).then(function (space) { if (space) { if (req.accepts('text/html')){ - res.redirect("/spaces/"+space._id + "?spaceAuth=" + token); + res.redirect("/spaces/"+space._id + "?spaceAuth=" + hash); } else { res.status(200).json(space); } diff --git a/views/partials/folders.html b/views/partials/folders.html index 329a91a..e911aff 100644 --- a/views/partials/folders.html +++ b/views/partials/folders.html @@ -8,7 +8,7 @@ <span>[[ __('create_folder') ]]</span> </button> - <label class="relative compact-hidden"> + <label class="relative compact-hidden" v-if="logged_in"> <span class="icon icon-sm icon-zoom no-events absolute-top-left" style="margin: 5px;"></span> <input id="folder-search" type="search" name="search" @@ -18,7 +18,7 @@ v-model="folder_spaces_search" v-on:change="search_spaces"> </label> - <div class="dropdown top light m-r-20 compact-hidden" v-bind:class="{open : active_dropdown=='folder_sorting'}"> + <div class="dropdown top light m-r-20 compact-hidden" v-bind:class="{open : active_dropdown=='folder_sorting'}" v-if="logged_in"> <button class="btn btn-sm btn-nude" v-on:click="activate_dropdown('folder_sorting')"> <span>[[ __('sort_by') ]]</span>: <b v-if="folder_sorting=='updated_at'">[[ __('last_modified') ]]</b> @@ -49,7 +49,8 @@ <div class="header-right pull-right"> <div class="dropdown top right light" v-bind:class="{open: active_dropdown=='account'}"> <button - class="profile-avatar btn btn-md btn-icon btn-dark btn-round" + class="profile-avatar btn btn-md btn-icon btn-dark btn-round" + v-if="logged_in" v-bind:style="background_image_style([user.avatar_thumb_uri])" v-bind:class="{'has-avatar-image':!!user.avatar_thumb_uri}" v-on:click="show_account();"> <span class="icon icon-user" v-if="logged_in && !user.avatar_thumb_uri"></span></button> @@ -80,14 +81,6 @@ </div> </div> - - <!--div class="btn-group dark round" id="meta-toggle" style="margin-right:10px"> - <button class="btn btn-md btn-transparent btn-icon btn-icon" v-on:click="toggle_meta()"> - <span class="jewel" style="color: white; background-color: red" v-if="meta_unseen>0">{{meta_unseen}}</span> - <span class="icon icon-menu"></span> - </button> - </div--> - </div> </header> @@ -98,14 +91,14 @@ <div id="folder-breadcrumb"> - <span v-for="item in active_space_path" class="btn btn-sm btn-transparent" v-sd-droppable="handle_folder_drop;item"> + <span v-if="logged_in" v-for="item in active_space_path" class="btn btn-sm btn-transparent" v-sd-droppable="handle_folder_drop;item"> <a href="/{{item.space_type}}s/{{item._id}}">{{item.name}}</a> â–¶</span> <a v-if="(active_space_role != 'admin')" type="button" class="btn btn-sm btn-transparent"> <span>{{active_folder.name}}</span> </a> - <div class="dropdown top light" v-bind:class="{open:active_dropdown=='breadcrumb'}" v-if="(active_folder._id != user.home_folder_id) && ((active_space_role == 'admin') ||Â (active_space_role == 'editor'))"> + <div class="dropdown top light" v-bind:class="{open:active_dropdown=='breadcrumb'}" v-if="(active_folder._id != user.home_folder_id) && ((active_space_role == 'admin'))"> <button type="button" class="btn btn-sm btn-transparent btn-dropdown" data-toggle="dropdown" v-on:click=" activate_dropdown('breadcrumb')"> <span>{{active_folder.name}}</span> </button> @@ -142,8 +135,15 @@ v-sd-droppable="handle_folder_drop;item" draggable="true" class="item" v-bind:class="item.space_type" - v-bind:style="{'z-index': (active_profile_spaces.length - $index)}"> - <a href="/{{item.space_type}}s/{{item._id}}"> + v-bind:style="{'z-index': (active_profile_spaces.length - $index)}"> + + <!-- anonymous editors can go edit spaces in a folder --> + <a href="/s/{{item.edit_hash}}-{{item.edit_slug}}" v-if="active_space_role=='editor' && !logged_in"> + <span class="item-thumbnail thumbnail-loading" v-if="item.space_type=='space'"></span> + <span class="item-thumbnail" v-bind:style="space_thumbnail_style(item)"></span> + </a> + + <a v-if="active_space_role=='viewer' || logged_in" href="/{{item.space_type}}s/{{item._id}}"> <span class="item-thumbnail thumbnail-loading" v-if="item.space_type=='space'"></span> <span class="item-thumbnail" v-bind:style="space_thumbnail_style(item)"></span> </a> -- GitLab