mirror of
https://github.com/spacedeck/spacedeck-open.git
synced 2025-12-15 17:37:30 +01:00
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:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> ▶</span>
|
<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">
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user