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
This commit is contained in:
mntmn
2020-06-02 20:47:58 +02:00
committed by GitHub
parent 8ddbec6b68
commit 0c5fa597e8
8 changed files with 99 additions and 83 deletions

View File

@@ -4,27 +4,6 @@ require('../models/db');
var config = require('config'); var config = require('config');
const redis = require('../helpers/redis'); 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) => { module.exports = (req, res, next) => {
res.header("Cache-Control", "no-cache"); res.header("Cache-Control", "no-cache");
@@ -36,21 +15,18 @@ module.exports = (req, res, next) => {
if (!object) return; if (!object) return;
redis.sendMessage("create", model, object, req.channelId); redis.sendMessage("create", model, object, req.channelId);
this.status(201).json(object); this.status(201).json(object);
saveAction("create", object);
}; };
res['distributeUpdate'] = function(model, object) { res['distributeUpdate'] = function(model, object) {
if (!object) return; if (!object) return;
redis.sendMessage("update", model, object, req.channelId); redis.sendMessage("update", model, object, req.channelId);
this.status(200).json(object); this.status(200).json(object);
saveAction("update", object);
}; };
res['distributeDelete'] = function(model, object) { res['distributeDelete'] = function(model, object) {
if (!object) return; if (!object) return;
redis.sendMessage("delete", model, object, req.channelId); redis.sendMessage("delete", model, object, req.channelId);
this.sendStatus(204); this.sendStatus(204);
saveAction("delete", object);
}; };
next(); next();

View File

@@ -6,6 +6,10 @@ var websocket = null;
var channel_id = null; var channel_id = null;
var space_auth = 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) { function load_resource(method, path, data, on_success, on_error, on_progress) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.onload = function(evt,b,c) { 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.withCredentials = true;
req.open(method, api_endpoint+"/api"+path, true); req.open(method, api_endpoint+"/api"+path, true);
if (api_token) { if (api_token) {
req.setRequestHeader("X-Spacedeck-Auth", api_token); req.setRequestHeader("X-Spacedeck-Auth", api_token);
} }
if (space_auth) { if (space_auth) {
console.log("set space auth", space_auth);
req.setRequestHeader("X-Spacedeck-Space-Auth", space_auth); req.setRequestHeader("X-Spacedeck-Space-Auth", space_auth);
} }
if (channel_id) { if (channel_id) {
req.setRequestHeader("X-Spacedeck-Channel", channel_id); req.setRequestHeader("X-Spacedeck-Channel", channel_id);
} }

View File

@@ -17,6 +17,21 @@ var SpacedeckRoutes = {
}.bind(this) }.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([ this.router.add([
{ {

View File

@@ -405,7 +405,12 @@ var SpacedeckSections = {
} }
if (space.space_type == "folder") return ""; 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() { reset_artifact_filters: function() {

View File

@@ -99,12 +99,16 @@ var SpacedeckSpaces = {
}.bind(this), {value: dft || "Guest "+parseInt(10000*Math.random()), ok: __("ok"), cancel: __("cancel")}); }.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_filter="";
this.folder_spaces_search=""; 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")); this.embedded = !!(get_query_param("embedded"));
var userReady = function() { var userReady = function() {

View File

@@ -42,7 +42,52 @@ var spaceMapping = {
thumbnail_url: 1 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) { 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) { if (!req.user) {
res.status(403).json({ res.status(403).json({
error: "auth required" 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) { } else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) {
// list spaces in a folder // list spaces in a folder
db.Space listSpacesInFolder(req, res, req.query.parent_space_id);
.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"});
}
});
} else { } else {
// list home folder and spaces/folders that the user is a member of // list home folder and spaces/folders that the user is a member of

View File

@@ -115,16 +115,16 @@ router.get('/t/:id', (req, res) => {
res.redirect(path); res.redirect(path);
}); });
router.get('/s/:token', (req, res) => { router.get('/s/:hash', (req, res) => {
var token = req.params.token; var hash = req.params.hash;
if (token.split("-").length > 0) { if (hash.split("-").length > 0) {
token = token.split("-")[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 (space) {
if (req.accepts('text/html')){ if (req.accepts('text/html')){
res.redirect("/spaces/"+space._id + "?spaceAuth=" + token); res.redirect("/spaces/"+space._id + "?spaceAuth=" + hash);
} else { } else {
res.status(200).json(space); res.status(200).json(space);
} }

View File

@@ -8,7 +8,7 @@
<span>[[ __('create_folder') ]]</span> <span>[[ __('create_folder') ]]</span>
</button> </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> <span class="icon icon-sm icon-zoom no-events absolute-top-left" style="margin: 5px;"></span>
<input id="folder-search" <input id="folder-search"
type="search" name="search" type="search" name="search"
@@ -18,7 +18,7 @@
v-model="folder_spaces_search" v-on:change="search_spaces"> v-model="folder_spaces_search" v-on:change="search_spaces">
</label> </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')"> <button class="btn btn-sm btn-nude" v-on:click="activate_dropdown('folder_sorting')">
<span>[[ __('sort_by') ]]</span>: <span>[[ __('sort_by') ]]</span>:
<b v-if="folder_sorting=='updated_at'">[[ __('last_modified') ]]</b> <b v-if="folder_sorting=='updated_at'">[[ __('last_modified') ]]</b>
@@ -49,7 +49,8 @@
<div class="header-right pull-right"> <div class="header-right pull-right">
<div class="dropdown top right light" v-bind:class="{open: active_dropdown=='account'}"> <div class="dropdown top right light" v-bind:class="{open: active_dropdown=='account'}">
<button <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:style="background_image_style([user.avatar_thumb_uri])"
v-bind:class="{'has-avatar-image':!!user.avatar_thumb_uri}" v-on:click="show_account();"> 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> <span class="icon icon-user" v-if="logged_in && !user.avatar_thumb_uri"></span></button>
@@ -80,14 +81,6 @@
</div> </div>
</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> </div>
</header> </header>
@@ -98,14 +91,14 @@
<div id="folder-breadcrumb"> <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 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"> <a v-if="(active_space_role != 'admin')" type="button" class="btn btn-sm btn-transparent">
<span>{{active_folder.name}}</span> <span>{{active_folder.name}}</span>
</a> </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')"> <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> <span>{{active_folder.name}}</span>
</button> </button>
@@ -142,8 +135,15 @@
v-sd-droppable="handle_folder_drop;item" v-sd-droppable="handle_folder_drop;item"
draggable="true" draggable="true"
class="item" v-bind:class="item.space_type" class="item" v-bind:class="item.space_type"
v-bind:style="{'z-index': (active_profile_spaces.length - $index)}"> v-bind:style="{'z-index': (active_profile_spaces.length - $index)}">
<a href="/{{item.space_type}}s/{{item._id}}">
<!-- 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 thumbnail-loading" v-if="item.space_type=='space'"></span>
<span class="item-thumbnail" v-bind:style="space_thumbnail_style(item)"></span> <span class="item-thumbnail" v-bind:style="space_thumbnail_style(item)"></span>
</a> </a>