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>&nbsp; â–¶</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