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');
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();

View File

@@ -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);
}

View File

@@ -18,6 +18,21 @@ var SpacedeckRoutes = {
}
]);
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([
{
path: "/confirm/:token",

View File

@@ -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() {

View File

@@ -99,11 +99,15 @@ 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"));

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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>
@@ -50,6 +50,7 @@
<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"
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>
@@ -143,7 +136,14 @@
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}}">
<!-- 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>