Commit 7ff29265 authored by mntmn's avatar mntmn
Browse files

initial commit.

parents
/*
SpacedeckSpaces
This module contains functions dealing with Spaces UI.
*/
var SpacedeckSpaces = {
data: {
active_space: {advanced:{}},
active_space_loaded: false,
active_space_role: "viewer",
active_space_version_dirty: true,
active_space_messages: [],
active_space_memberships: [],
active_folder_history_items: [],
active_space_users: [],
active_space_artifacts: [],
active_space_path: [],
access_settings_space: null,
access_settings_memberships: [],
duplicate_folders: [],
duplicate_folder_id: "",
pending_pdf_files: [],
meta_visible: false,
meta_unseen: 0,
present_mode: false,
space_editing_title: false,
create_space_title: "",
folder_reverse: 1,
embedded: false,
remix_cta: "Create Reply",
publish_cta: "Publish",
remix_copying: true,
remix_style: "",
guest_signup_enabled: false,
space_embed_html: "",
share_base: location.origin,
share_base_url: location.origin+"/spaces/",
share_base_url_enc: encodeURIComponent(location.origin+"/spaces/"),
social_bar: true,
can_add_comment: false,
space_info_section: "access",
editors_section: "list",
selected_member: null,
invite_member_role: 'viewer',
invite_email_error: null,
invite_email: "",
invite_message: "",
active_join_link: "",
join_link_role: "viewer",
mouse_state: "idle",
// folders
active_folder: null,
folder_sorting: "updated_at",
folder_spaces_filter: null,
active_path_length : 0,
space_comment: "",
folder_spaces_search: "",
// map of artifact IDs to medium rich text editor objects
medium_for_object: {},
},
methods: {
search_spaces: function() {
var query = this.folder_spaces_search;
console.log("search query: ",query);
load_spaces_search(query, function(spaces) {
console.log("results: ",spaces);
this.active_profile_spaces = spaces;
}.bind(this));
},
guest_logout: function() {
if ("localStorage" in window && localStorage) {
delete localStorage['guest_nickname'];
}
this.guest_nickname = "";
location.reload();
},
ask_guestname: function(dft, cb) {
console.log("ask_guestname");
var team_name = "Spacedeck";
if(subdomainTeam) {
team_name = subdomainTeam.name;
}
smoke.prompt(__('what_is_your_name', team_name) , function(content) {
if (!content || (content.length === 0)) {
this.ask_guestname(dft, cb);
} else {
this.guest_nickname = content;
if ("localStorage" in window && localStorage) {
try {
localStorage['guest_nickname'] = this.guest_nickname;
}catch(e) {
console.error(e);
}
}
if (cb) cb();
}
}.bind(this), {value: dft || "Guest "+parseInt(10000*Math.random()), ok: __("ok"), cancel: __("cancel")});
},
load_space: function(space_id, on_success, on_error) {
console.log("load space: ", space_id);
this.folder_spaces_filter="";
this.folder_spaces_search="";
space_auth = get_query_param("spaceAuth");
var userReady = function() {
if (get_query_param("embedded")) {
this.embedded = true;
this.guest_signup_enabled = true;
if (get_query_param("publish_cta")) {
this.publish_cta = get_query_param("publish_cta");
}
if (get_query_param("nosocial")) {
this.social_bar = false;
}
}
if (get_query_param("confirm") && this.logged_in) {
var token = get_query_param("confirm");
confirm_user(this.user, token, function() {
this.redirect_to("/spaces/"+space_id+"?show_access=1");
}.bind(this), function() {
alert("An error occured confirming your email with the given token.");
});
return;
}
this.close_dropdown();
this.active_space_loaded = false;
this.viewport_zoom = 1;
this.viewport_zoom_percent = 100;
this.loading_space_id = space_id;
this.present_mode = false;
this.active_space_is_readonly = true;
this.opened_dialog = "none";
this.open_space_dialog = "none";
this.selected_artifacts_dict = {};
this.update_selection_metrics();
this.can_add_comment = false;
var is_home = false;
if (this.user) {
is_home = (space_id == this.user.home_folder_id);
}
document.title = "Loading… | Spacedeck";
load_space(space_id, function(space, role) {
document.title = space.name;
this.active_space_role = role || "viewer"; //via req header from backend
this.space_embed_html = "<iframe width=\"1024\" height=\"768\" seamless src=\""+ENV.webEndpoint+"/spaces/"+space._id+"?embedded=1\"></iframe>";
if (!is_home) {
//console.log(space);
load_members(space, function(members) {
this.active_space_memberships = members;
}.bind(this));
}
console.log("[websocket] auth start");
if (space.space_type == "folder") {
this.active_space = {advanced:{}};
document.title = "Spacedeck";
load_spaces(space_id, is_home, function(spaces) {
space.children = spaces;
this.loading_space_id = null;
this.active_profile_spaces = space.children;
this.active_folder = space;
this.access_settings_space = space;
this.auth_websocket(this.active_folder);
this.load_space_path(this.active_folder);
if (is_home) {
this.root_folder = space;
}
load_history(space, function(history) {
console.log("loaded digest", history);
this.active_folder_history_items = history;
this.meta_unseen = 0;
if ("localStorage" in window && localStorage) {
var last_seen = parseInt(localStorage[this.meta_last_seen_key()], 10);
} else {
var last_seen = 0;
}
for (var i=0; i<history.length; i++) {
var item = history[i];
var t = new Date(item.last_action).getTime();
var my_own = false;
if (item.users.length==1 && item.users[0]=="you") {
my_own = true;
}
if (t>last_seen && !my_own) this.meta_unseen++;
}
}.bind(this));
this.active_view = "folders";
}.bind(this));
if ("localStorage" in window) {
var key = "folder_sorting_"+space_id;
var key2 = "folder_reverse_"+space_id;
if (localStorage[key] && localStorage[key2]) {
this.folder_sorting = localStorage[key];
this.folder_reverse = parseInt(localStorage[key2]);
console.log("loaded folder sorting: ",this.folder_sorting,this.folder_reverse);
}
}
// legacy fix
if (this.folder_sorting == "opened_at") {
this.folder_sorting = "name";
}
} else if (space.space_type == "space") {
this.artifacts = [];
this.loading_space_id = null;
document.title = space.name;
if (space_auth || this.logged_in) {
this.can_add_comment = true;
}
this.setup_watches();
load_artifacts(space._id, function(artifacts) {
// FIXME: how to cleanly handle this error?
if (!artifacts) {
artifacts = [];
}
// FIXME: remove kludge
for (var i=0; i<artifacts.length; i++) {
this.update_board_artifact_viewmodel(artifacts[i]);
}
this.active_space_artifacts = artifacts;
this.$set("active_space", space);
this.active_space = space;
this.auth_websocket(this.active_space);
this.active_view = "space";
this.fixup_space_size();
if (space._id != this.active_space._id) {
this.present_mode = true;
this.active_space_is_readonly = true;
} else {
this.active_space_is_readonly = false;
}
this.discover_zones();
window.setTimeout(function() {
this.zoom_to_fit();
}.bind(this),10);
if (on_success) {
on_success();
}
this.active_space_loaded = true;
this.extract_properties_from_selection(); // populates zones etc
load_comments(space._id, function(messages) {
if (!messages) messages = [];
this.active_space_messages = messages;
this.refresh_space_comments();
}.bind(this), function(xhr) { console.error(xhr); });
}.bind(this));
if (this.active_space_role == "editor" || this.active_space_role == "admin") {
this.present_mode = false;
this.active_space_is_readonly = false;
}
// FIXME
this.active_join_link = "";
this.join_link_role = "viewer";
// FIXME
if (this.active_space_role == "admin") {
this.space_info_section="access";
} else if (this.active_space_role == "editor") {
//this.space_info_section="versions";
} else {
this.space_info_section="info";
}
}
}.bind(this), function(xhr) {
if (on_error) {
return on_error(xhr);
}
if (xhr.status == 403) {
if (!this.logged_in) {
this.redirect_to("/login?space_id="+space_id);
} else {
this.redirect_to("/");
}
} else {
this.redirect_to("/not_found");
console.error(xhr);
}
}.bind(this));
}.bind(this);
var default_guest = "";
if (("localStorage" in window && localStorage) && localStorage['guest_nickname']) {
this.guest_nickname = localStorage['guest_nickname'];
default_guest = this.guest_nickname;
userReady();
}
if (space_auth) {
if (this.guest_nickname) {
userReady();
} else {
this.ask_guestname(default_guest, function() {
userReady();
});
}
} else {
this.guest_nickname = "";
userReady();
}
},
refresh_space_comments: function() {
this.meta_unseen = 0;
var messages = this.active_space_messages;
var last_seen = 0;
if ("localStorage" in window && localStorage) {
last_seen = parseInt(localStorage[this.meta_last_seen_key()], 10);
}
for (var i=0; i<messages.length; i++) {
var item = messages[i];
var t = new Date(item.updated_at).getTime();
var my_own = false;
if (this.user && this.user._id!=item.user_id && !item.editor_name) {
my_own = true;
}
if (t>last_seen && !my_own) this.meta_unseen++;
}
},
go_to_next_space: function() {
var space_ids = this.active_folder.children.map(function(s){return s._id});
var idx = space_ids.indexOf(this.active_space._id);
console.log("index: ",idx);
var cur_idx = idx;
var done = false;
while (!done) {
var next = this.active_folder.children[(idx+1)%space_ids.length];
if (next.space_type == "folder") {
done = false;
idx++;
} else {
done = true;
}
if (cur_idx == idx) done = true; // wraparound
}
this.load_space(next._id);
},
go_to_previous_space: function() {
var space_ids = this.active_folder.children.map(function(s){return s._id});
var idx = space_ids.indexOf(this.active_space._id);
console.log("index: ",idx);
var cur_idx = idx;
var done = false;
while (!done) {
var idx = (idx<1?space_ids.length:idx)-1;
var prev = this.active_folder.children[idx];
if (prev.space_type == "folder") {
done = false;
idx--;
} else {
done = true;
}
if (cur_idx == idx) done = true; // wraparound
}
this.load_space(prev._id);
},
filtered_folder_children: function(type){
var type = type || "space";
return _.filter(this.active_folder.children, function(s){
return s.space_type == type;
})
},
load_space_path: function(space) {
if (!space) return [];
load_space_path(space._id, function(path) {
this.active_space_path = path;
}.bind(this), function() { console.log("could not load folder path")});
},
is_active_space_role: function(rolename) {
if(!this.active_space) return false;
return this.active_space_role == rolename;
},
create_space: function(space_type) {
if (!this.active_folder) return;
this.close_modal();
this.folder_spaces_filter="";
if (!this.active_folder.children) {
this.active_folder.children = [];
}
if (!space_type) space_type = "space";
var s = {
name: space_type == "space" ? __("untitled_space") : __("untitled_folder") ,
artifacts: [],
space_type: space_type,
parent_space_id: this.active_folder._id
};
if (this.create_space_title.length) {
s.name = this.create_space_title;
}
save_space(s, function(saved_space) {
this.active_folder.children.push(saved_space);
if (space_type != "folder") {
this.redirect_to("/"+saved_space.space_type+"s/"+saved_space._id, function(succ) {
});
} else {
this.rename_folder(saved_space);
}
}.bind(this), function(xhr) {
alert("Error: Could not create Space ("+xhr.status+").");
}.bind(this));
},
save_space: function(s) {
save_space(s);
},
create_space_version: function() {
if (!this.is_pro(this.user)) {
// pro feature
smoke.confirm(__("spacedeck_pro_ad_versions"), function(confirmed) {
if (confirmed) this.show_upgrade_modal();
}.bind(this));
return;
}
this.version_saving = true;
this.present_mode = false;
var s = this.active_space.draft_space;
console.log("create_space_version:", s);
duplicate_space(s, null, function(new_version_space) {
load_spaces(this.active_space._id, false, function(space) {
this.version_saving = false;
this.activate_space_version(space, space.draft_space);
alert("Version saved.");
}.bind(this));
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
finalize_folder_profile_edit: function() {
save_space(this.active_folder, function(saved_space) {
this.close_modal();
}.bind(this));
},
finalize_space_profile_edit: function() {
save_space(this.active_space, function(saved_space) {
this.close_modal();
}.bind(this));
},
delete_space: function(space) {
smoke.confirm("Really delete "+space.name+"?", function(confirmed) {
if (!confirmed) return;
var idx = this.active_folder.children.indexOf(space);
delete_space(space, function() {
if (space.parent_space_id){
this.redirect_to("/folders/"+space.parent_space_id, function(succ) {});
} else {
this.redirect_to("/spaces", function(succ) {});
}
this.close_modal();
this.active_folder.children.splice(idx,1);
}.bind(this));
}.bind(this));
},
duplicate_space: function(space) {
duplicate_space(space, null, function(new_space) {
//alert("Space duplicated.");
this.active_folder.children.push(new_space);
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
remove_avatar: function(space) {
remove_avatar_file("space", space, function(s) {
this.active_space = s;
}.bind(this));
},
rename_space: function(space) {
this.close_dropdown();
if (space.space_type == "folder") return this.rename_folder(space);
smoke.prompt(__("new_space_title"), function(title) {
if (title && title.length) {
space.name = title;
save_space(space);
}
}.bind(this), {value: space.name});
},
rename_folder: function(folder) {
this.close_dropdown();
smoke.prompt(__("new_folder_title"), function(title) {
if (title && title.length) {
folder.name = title;
save_space(folder);
}
}.bind(this), {value: folder.name});
},
edit_space_title: function() {
this.close_dropdown();
if (this.active_space_role=="editor" || this.active_space_role=="admin") {
this.space_editing_title = true;
$("#space-title").focus();
}
},
save_space_title: function(name) {
this.active_space.name = name;
save_space(this.active_space, function() {
this.space_editing_title = false;
}.bind(this));
},
save_space_keydown: function($event) {
if ($event) {
if ($event.keyCode != 13) {
this.space_editing_title = true;
return;
}
$event.preventDefault();
$event.stopPropagation();
$event.target.blur();
}
save_space(this.active_space, function(updated_space) {
this.active_space.edit_slug = updated_space.edit_slug;
this.space_editing_title = false;
}.bind(this));
},
save_space_description: function($event) {
$event.preventDefault();
$event.stopPropagation();
var val = $event.target.innerText;
$event.target.blur();
this.active_space.description = val;
save_space(this.active_space);
},
save_space_domain: function($event) {
$event.preventDefault();
$event.stopPropagation();
var val = $event.target.innerText;
$event.target.blur();
this.active_space.domain = val;
save_space(this.active_space);
},
download_space: function() {
smoke.quiz(__("download_space"), function(e, test) {
if (e == "PDF"){
this.download_space_as_pdf(this.active_space);
}else if (e == "ZIP"){
this.download_space_as_zip(this.active_space);
}else if (e == "TXT"){
this.download_space_as_list(this.active_space);
}
}.bind(this), {
button_1: "PDF",
button_2: "ZIP",
button_3: "TXT",
button_cancel:__("cancel")
});
},
download_space_as_png: function(space) {
window.open(ENV.apiEndpoint + "/api/spaces/" + space._id + "/png");
},
download_space_as_pdf: function(space) {
this.global_spinner = true;
get_resource("/spaces/" + space._id + "/pdf", function(o) {
this.global_spinner = false;
location.href = o.url;
}.bind(this), function(xhr) {
this.global_spinner = false;
alert("PDF export problem (" + xhr.status + ").");
}.bind(this));
},
download_space_as_zip: function(space) {
this.global_spinner = true;
get_resource("/spaces/" + space._id + "/zip", function(o) {
this.global_spinner = false;
location.href = o.url;
}.bind(this), function(xhr) {
this.global_spinner = false;
alert("ZIP export problem (" + xhr.status + ").");
}.bind(this));
},
download_space_as_list: function(space) {
this.global_spinner = true;
location.href = "/api/spaces/" + space._id + "/list";
},
duplicate_space_into_folder: function() {
load_writable_folders( function(folders){
this.duplicate_folders = _.sortBy(folders, function (folder) { return folder.name; });
}.bind(this), function(xhr) {
console.error(xhr);
});
},
duplicate_folder_confirm: function() {
var folderId = this.duplicate_folder_id;
var idx = _.findIndex(this.duplicate_folders, function(s) { return s._id == folderId;});
if (idx<0) idx = 0;
var folder = this.duplicate_folders[idx];
console.log("df f",folder);
if (!folder) return;
duplicate_space(this.active_space, folder._id, function(new_space) {
this.duplicate_folders = [];
this.duplicate_folder = null;
smoke.quiz(__("duplicate_success", this.active_space.name, folder.name), function(e, test){
if (e == __("goto_space", new_space.name)){
this.redirect_to("/spaces/" + new_space._id);
}else if (e == __("goto_folder", folder.name)){
this.redirect_to("/folders/" + folder._id);
}
}.bind(this), {
button_1: __("goto_space", new_space.name),
button_2: __("goto_folder", folder.name),
button_cancel:__("stay_here")
});
}.bind(this), function(xhr){
console.error(xhr);
smoke.prompt("error: " + xhr.statusText);
}.bind(this));
},
toggle_follow_mode: function() {
this.deselect();
this.follow_mode = !this.follow_mode;
},
toggle_present_mode: function() {
this.deselect();
this.present_mode = !this.present_mode;
if (this.present_mode) {
//this.go_to_first_zone();
}
},
meta_last_seen_key: function() {
var seen_key = "meta-seen-";
if (this.active_view == 'space') {
if (!this.active_space) return "invalid";
seen_key += this.active_space._id;
} else if (this.active_view == 'folders') {
if (!this.active_folder) return "invalid";
seen_key += this.active_folder._id;
}
return seen_key;
},
toggle_meta: function() {
this.meta_visible = !this.meta_visible;
if (this.meta_visible) {
var seen_key = this.meta_last_seen_key();
if ("localStorage" in window && localStorage) {
localStorage[seen_key] = new Date().getTime();
console.log("seen_key: ",seen_key,localStorage[seen_key]);
this.meta_last_seen = localStorage[seen_key];
}
this.meta_unseen = 0;
}
},
toggle_space_access_mode: function() {
this.access_settings_space.access_mode = (this.access_settings_space.access_mode=="public")?"private":"public";
save_space(this.access_settings_space);
},
save_space_access_mode: function(evt) {
// FIXME really bad that i have to do this manually. why is the
// value not already updated when v-on="change" is fired?!
this.access_settings_space.access_mode = evt.currentTarget.value;
save_space(this.access_settings_space);
},
save_space_editors_locking: function(evt) {
// FIXME same issue as above
this.access_settings_space.editors_locking = evt.currentTarget.checked;
save_space(this.access_settings_space);
},
create_join_link: function() {
create_join_link(this.active_space._id, this.join_link_role, function(result) {
this.active_join_link = "https://"+location.host+"/invitations/"+result.code+"/accept";
}.bind(this));
},
delete_join_link: function() {
get_join_link(this.active_space._id, function(result) {
if (result && result.length) {
delete_join_link(result[result.length-1]._id, function() {
this.active_join_link = "";
}.bind(this));
}
}.bind(this));
},
invite_member: function(space, emails_text, txt, role) {
this.invite_email_error = null;
var emails = emails_text.split(",");
var displayed_success = false;
_.each(emails, function(email) {
email = email.trim();
if (!validateEmail(email)) {
this.invite_email_error = "Please enter a valid address."
return;
}
var m = {
email_invited: email,
personal_message: txt,
role: role
}
create_membership(space, m, function(m) {
this.access_settings_memberships.push(m);
console.log("membership created:", m);
this.editors_section="list";
if (!displayed_success) {
displayed_success = true;
smoke.alert("Invitation(s) sent.");
this.invite_email = "";
this.invite_message = "";
}
}.bind(this), function(xhr){
text = JSON.stringify(xhr.responseText);
smoke.alert("Error: "+text);
}.bind(this));
}.bind(this));
},
update_member: function(space, m, role) {
m.role = role;
save_membership(space, m, function() {
console.log("saved")
}.bind(this), function(xhr) {
console.error(xhr);
}.bind(this));
},
// revoke
remove_member: function(space, m) {
delete_membership(space, m, function() {
this.access_settings_memberships.splice(this.access_settings_memberships.indexOf(m), 1);
}.bind(this), function(xhr) {
console.error(xhr);
}.bind(this));
},
history_back: function() {
window.history.back();
},
create_space_comment: function(comment) {
if (!comment.length) return;
var data = {
space: this.active_space._id,
message: comment,
editor_name: this.guest_nickname,
user: this.user
};
save_comment(this.active_space._id, data, function(comment) {
console.log("comment saved: ",comment.created_at);
this.active_space_messages.push(comment);
this.space_comment = "";
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
remove_space_comment: function(comment) {
delete_comment(this.active_space._id, comment._id, function() {
console.log("comment id:",comment._id);
this.active_space_messages = _.filter(this.active_space_messages, function(c){return c._id!=comment._id;});
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
emojified_comment: function(comment) {
return twemoji.parse(comment);
},
set_folder_sorting: function(key,reverse) {
this.folder_sorting = key;
this.folder_reverse = reverse?-1:1;
console.log(key, reverse);
if ("localStorage" in window) {
localStorage["folder_sorting_"+this.active_folder._id] = this.folder_sorting;
localStorage["folder_reverse_"+this.active_folder._id] = this.folder_reverse;
}
},
activate_space_info_section: function(id) {
this.space_info_section = id;
this.editors_section = "list";
if (id == "versions") {
// load space versions
load_spaces(this.active_space._id, null, function(space_with_children) {
this.active_space.children = space_with_children.children;
console.log("loaded: ",space_with_children);
}.bind(this));
} else if (id == "info") {
// load replies
}
},
handle_folder_drop: function(evt, dest) {
try {
var source = JSON.parse(evt.dataTransfer.getData("application/json"));
} catch (e) {
return;
}
if (!source || !source._id || !source.parent_space_id || !dest._id) return;
if (source._id==dest._id) return;
if (dest.space_type!="folder") {
alert("Spaces can only be moved into folders.");
return;
}
source.parent_space_id = dest._id;
save_space(source, function() {
var idx = _.findIndex(this.active_folder.children, function(s) { return s._id == source._id;});
if (idx>=0) {
this.active_folder.children.splice(idx,1);
console.log("spliced: ",idx);
}
}.bind(this));
},
activate_access: function() {
this.activate_modal("access");
//this.meta_visible = false;
if (this.active_space._id) {
this.access_settings_space = this.active_space;
} else if (this.active_folder && this.active_folder._id) {
this.access_settings_space = this.active_folder;
} else {
return;
}
this.access_settings_memberships = this.active_space_memberships;
},
close_access: function() {
this.close_modal();
},
show_offline_help: function() {
smoke.confirm(__('was_offline'), function(confirmed) {
if (!confirmed) return;
location.reload();
});
}
}
}
/*
SpacedeckTeams
This module contains functions dealing with Teams.
*/
var SpacedeckTeams = {
data: {
team_members: [],
team_loading: false,
team_logo: "",
team_emails: "",
team_email_invited: false,
team_plan_calculation: "",
},
methods: {
is_admin: function(user) {
return _.filter(user.team.admins, function(admin_id) {
return admin_id == user._id;
}).length > 0;
},
calculate_team: function() {
this.team_plan_calculation = "";
},
load_team: function() {
if (this.user.team) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships", null, function(members) {
this.team_members = members;
this.calculate_team();
}.bind(this), function(xhr, textStatus, errorThrown) {
console.log(xhr, textStatus, errorThrown);
});
}
},
team_save: function() {
load_resource("PUT", "/teams/" + this.user.team._id , this.user.team, function(res, xhr) {
alert("Team updated.");
}.bind(this), function(xhr) {
console.error(xhr);
alert("Could not update Team.");
});
},
team_update_member: function(m){
load_resource("PUT", "/teams/" + this.user.team._id + "/memberships/" + m._id, m, function(res, xhr) {
console.log("members updated");
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_invite_members: function(emails) {
var emailList = emails.split(",");
for (_i = 0, _len = emailList.length; _i < _len; _i++) {
email = emailList[_i];
email = email.replace(new RegExp(" ", "g"), "").toLowerCase();
if (validateEmail(email)) {
var data = {
email: email
};
load_resource("POST", "/teams/" + this.user.team._id + "/memberships", data, function(res, xhr) {
this.team_email_invited = true;
this.team_members.push(res);
var timeoutID = window.setTimeout(function(){
this.team_email_invited = false;
}.bind(this), 1000);
this.team_emails = "";
}.bind(this), function(xhr, textStatus, errorThrown) {
console.log(xhr, textStatus, errorThrown);
this.team_invite_error = JSON.parse(xhr.responseText).error;
}.bind(this));
}
}
},
team_promote_member: function(m) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships/" + m._id + "/promote" , null, function(res, xhr) {
this.load_user(function() {
this.load_team();
}.bind(this));
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_demote_member: function(m) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships/" + m._id + "/demote" , null, function(res, xhr) {
this.load_user(function() {
this.load_team();
}.bind(this));
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_remove_member: function(m) {
if (confirm("Really delete this member?")) {
if (m.user_id && m.state === "active") {
load_resource("DELETE", "/users/" + m._id, null, function(res, xhr) {
var idx = this.team_members.indexOf(m);
this.team_members.splice(idx, 1);
}.bind(this), function(xhr) {
console.error(xhr);
});
} else {
load_resource("DELETE", "/teams/" + this.user.team._id + "/memberships/" + m._id, null, function(res, xhr) {
var idx = this.team_members.indexOf(m);
this.team_members.splice(idx, 1);
}.bind(this), function(xhr) {
console.error(xhr);
});
}
}
}
}
}
/*
SpacedeckUpdates
This module contains functions dealing with Updates / Notifications.
*/
SpacedeckUpdates = {
user_notifications: [],
updates_items: [],
update_name_for_key: function(key) {
var updates_mapping = {
'space_like': "liked",
'space_comment': "commented in",
'space_follow': "is now following",
'space_publish': "published a new version of"
}
var name = updates_mapping[key];
if(name) return name;
return key;
},
load_updates: function() {
load_notifications(this.user, function(notifications) {
this.user_notifications = notifications;
});
},
activate_updates: function() {
$location.path("/updates");
}
}
\ No newline at end of file
/*
SpacedeckUsers
This module contains functions dealing with Users and Authentication.
*/
SpacedeckUsers = {
data: {
user_forms_email: "",
user_forms_name: "",
invitation_token: null,
login_email: "",
login_password: "",
signup_password: "",
signup_password_confirmation: "",
account_remove_error: null,
loading_user: false,
password_reset_confirm_error: "",
password_reset_error: ""
},
methods:{
load_user: function(on_success, on_error) {
this.loading_user = true;
load_current_user(function(user) {
this.user = user;
this.loading_user = false;
this.logged_in = true;
if (on_success) {
on_success(user);
}
}.bind(this), function() {
// error
this.loading_user = false;
this.logout();
if (on_error) {
on_error();
}
}.bind(this));
},
login_google: function(evt) {
this.loading_user = true;
create_oauthtoken(function(data){
this.loading_user = false;
location.href = data.url;
}, function(xhr){
this.loading_user = false;
alert("could not get oauth token");
});
},
finalize_login: function(session_token, on_success) {
if(!window.socket_auth || window.socket_auth == '' || window.socket_auth == 'null') {
window.socket_auth = session_token;
}
this.load_user(function(user) {
if (this.invitation_token) {
accept_invitation(this.invitation_token, function(memberships){
this.redirect_to("/spaces/"+memberships.space_id);
}.bind(this), function(xhr){
console.error(xhr);
alert("Could not accept invitation. Maybe it was already accepted?");
this.redirect_to("/spaces");
}.bind(this));
} else {
if (on_success) {
on_success(this.user);
} else {
if (get_query_param("space_id") && get_query_param("space_id").length==24) {
this.redirect_to("/spaces/"+get_query_param("space_id"));
} else {
this.redirect_to("/spaces", function() {});
}
}
}
}.bind(this));
},
login_with_token: function(token) {
create_session_for_oauthtoken(token, function(session) {
this.session = session;
this.finalize_login(session.token);
}.bind(this), function(xhr){
// FIXME: handle error
}.bind(this));
},
login_submit: function(email, password, $event, on_success) {
this.loading_user = true;
this.login_error = null;
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
create_session(email, password, function(session) {
console.log("session: ", session);
this.loading_user = false;
this.session = session;
this.finalize_login(session.token, on_success);
}.bind(this), function(req) {
this.loading_user = false;
var msg = "";
if (req.status>=403) {
var msg = "error_unknown_email";
} else {
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
}
this.login_error = __(msg);
}.bind(this));
},
login_submit_modal: function(email, password) {
this.login_submit(email, password, null, function() {
location.reload();
});
},
signup_guest: function(on_success) {
},
signup_submit: function($event, name, email, password, password_confirmation, on_success) {
this.creating_user = true;
this.signup_error = null;
if (("localStorage" in window) && localStorage) {
localStorage["sd_api_token"] = null;
}
api_token = null;
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
create_user(name, email, password, password_confirmation, function(session) {
this.creating_user = false;
this.login_submit(email, password, null, on_success);
}.bind(this), function(req) {
this.creating_user = false;
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
var msg = __(msg);
this.signup_error = msg;
}.bind(this));
},
signup_submit_modal: function($event, name, email, password, password_confirmation) {
this.signup_submit($event, name, email, password, password_confirmation, function() {
alert("Success.");
location.reload();
});
},
password_reset_submit: function(evt, email) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
this.password_reset_error = null;
this.password_reset_send = false;
if (email === undefined || email.length < 3) {
this.password_reset_error = "This is not a valid email address";
return;
}
create_password_reset(email, function(parsed,req) {
if(req.status==201) {
this.password_reset_send = true;
}
}.bind(this), function(req) {
console.log(req.status);
if (req.status==404) {
var msg = "error_unknown_email";
} else {
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
}
this.password_reset_error = __(msg);
}.bind(this));
},
password_reset_confirm: function(evt, password, password_confirmation) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
this.password_reset_confirm_error = null;
this.password_reset_send = false;
if(password != password_confirmation) {
this.password_reset_confirm_error = "Passwords do not match.";
return;
}
if(password.length < 5) {
this.password_reset_confirm_error = "Password too short (must have at least 5 characters).";
return;
}
confirm_password_reset(password, this.reset_token, function(parsed,req) {
if(req.status==201){
this.active_view = "login";
}
}.bind(this), function(req) {
if (req.status==404) {
var msg = "user not found";
} else {
var msg = "error: " + req.statusText;
}
this.password_reset_confirm_error = msg;
}.bind(this));
},
logout: function() {
this.active_view="login";
this.logged_in = false;
delete_session(function() {
this.active_space = {advanced:{}};
this.active_space_loaded = false;
this.active_sidebar_item = "none";
this.sidebar_state = "closed";
this.loading_user = false;
api_token = null;
this.user = {};
this.active_content_type = "login";
this.redirect_to("/");
}.bind(this));
},
send_feedback: function(text) {
if (text.length>0) {
create_feedback(this.user, text, function(xhr) {
alert(__("feedback_sent"));
this.close_modal()
}.bind(this), function(xhr) {
console.error(xhr);
});
}
},
remove_account: function(password, reason) {
this.account_remove_error = null;
if (reason && reason.length && (reason.length > 1)) {
create_feedback(this.user, reason, function(xhr) {
console.log("feedback sent");
}, function(xhr){});
}
if (!password) {
this.account_remove_error = "Password not correct";
return;
}
delete_user(this.user, password, function(xhr) {
alert("Sorry to see you go. Goodbye!");
this.logout();
}.bind(this), function(xhr) {
this.account_remove_error = "Password not correct ("+xhr.status+")";
}.bind(this));
},
user_avatar_image: function(user) {
return user.avatar_thumb_uri;
},
user_initials: function(user) {
var parts = (user?(user.nickname||user.email):"anonymous").replace(/[^a-zA-Z]/g,' ').replace(/ +/g,' ').split(' ');
if (parts.length>1) {
return parts[0][0]+parts[1][0];
}
return parts[0].substring(0,2);
},
has_avatar_image: function(user) {
return !!(user && user.avatar_thumb_uri && user.avatar_thumb_uri.length>0);
},
is_pro: function(user) {
return true;
},
}
}
function boot_spacedeck() {
console.log("booting...");
// custom directives
setup_directives();
setup_whiteboard_directives();
setup_exclusive_audio_video_playback();
var data = {
active_view: null,
online: true,
was_offline: false,
account: "profile",
logged_in: false,
guest_nickname: null,
user: {},
active_profile: null,
active_profile_spaces: [],
active_dropdown: "none",
creating_user: false,
signup_error: null,
login_error: null,
password_reset_send: false,
password_reset_error: null,
password_reset_email: null,
password_reset_confirm_error: null,
reset_token: null,
global_spinner: false
};
var methods = {
activate_dropdown: function(id, evt) {
if (this.active_dropdown == id) {
this.active_dropdown = "none";
return;
}
this.active_dropdown = id;
},
close_dropdown: function(evt) {
if (evt) {
if ($(evt.target).parents(".dropdown").length) {
return;
}
}
this.active_dropdown = "none";
},
translate: function() {
return i18n.t(arguments)
},
};
// mix in functions from all Spacedeck modules
methods = _.extend(methods, SpacedeckUsers.methods);
methods = _.extend(methods, SpacedeckWebsockets.methods);
methods = _.extend(methods, SpacedeckSpaces.methods);
methods = _.extend(methods, SpacedeckTeams.methods);
methods = _.extend(methods, SpacedeckBoardArtifacts);
methods = _.extend(methods, SpacedeckFormatting);
methods = _.extend(methods, SpacedeckSections.methods);
methods = _.extend(methods, SpacedeckAvatars.methods);
methods = _.extend(methods, SpacedeckModals.methods);
methods = _.extend(methods, SpacedeckAccount.methods);
methods = _.extend(methods, SpacedeckRoutes);
data = _.extend(data, SpacedeckUsers.data);
data = _.extend(data, SpacedeckAccount.data);
data = _.extend(data, SpacedeckWebsockets.data);
data = _.extend(data, SpacedeckSpaces.data);
data = _.extend(data, SpacedeckTeams.data);
data = _.extend(data, SpacedeckSections.data);
data = _.extend(data, SpacedeckAvatars.data);
data = _.extend(data, SpacedeckModals.data);
Vue.filter('select', function (array, key, operant, value) {
var res = _.filter(array, function(e){
var test = eval(e[key] + " " + operant + " " + value);
return test;
});
return res;
});
Vue.filter('date', function (value, format) {
var day = moment(value);
return day.format(format).replace("\'", "").replace("\'", "");
});
Vue.filter('exceptFilter', function (array, key) {
var filtered = _.filter(array, function(i){
return i[key]==undefined;
});
return filtered;
});
Vue.filter('size', function (array) {
return array.length;
});
Vue.filter('empty?', function (array) {
return array.length==0;
});
Vue.filter('urls_to_links', function (text) {
return urls_to_links(text);
});
window.spacedeck = new Vue({
el: "body",
data: data,
methods: methods
});
var lang = "en";
window.refreshLocale = function() {
if (spacedeck && spacedeck.user && spacedeck.user.preferences) {
lang = spacedeck.user.preferences.language || "en";
} else if (window.browser_lang) {
lang = window.browser_lang;
}
}
window.refreshLocale();
i18n.init({ lng: lang, resStore: window.locales }, function(err, t) {
console.log("i18n initialized: "+lang);
});
window.__ = function() {
var params = Array.prototype.slice.call(arguments);
params.shift();
window.refreshLocale();
return i18n.t(arguments[0], { postProcess: "sprintf", sprintf: params });
};
spacedeck.setup_section_module();
spacedeck.load_user(function() {
spacedeck.route();
},function() {
spacedeck.route();
});
window.addEventListener("paste", function(evt) {
if (evt.target.nodeName=="INPUT" || (evt.target.nodeName=="TEXTAREA" && evt.target.id!="clipboard-ta") || evt.target.contenteditable) {
// cancel
return;
}
if (spacedeck.active_space) {
spacedeck.handle_section_paste(evt);
}
});
}
$(document).ready(function(){
window.smoke = smoke;
window.alert = smoke.alert;
FastClick.attach(document.body);
boot_spacedeck();
});
SpacedeckWebsockets = {
data: {
users_online: {}
},
methods: {
handle_live_updates: function(msg) {
if (msg.model == "Space" && msg.object) {
if (msg.object.space_type == "space") {
if (this.active_space) {
if (this.active_space._id == msg.object._id) {
this.active_space = _.merge(this.active_space, msg.object);
}
}
}
}
if (msg.model == "Message") {
if (msg.action == "create" && msg.object) {
var new_message = msg.object;
if(this.active_space && this.active_space._id == new_message.space._id) {
this.active_space_messages.push(new_message);
this.refresh_space_comments();
} else console.log("message created in another space.");
}
}
if (msg.model == "Artifact") {
if (msg.action == "create" && msg.object) {
var new_artifact = msg.object;
if (this.active_space && this.active_space._id == new_artifact.space_id) {
var o = new_artifact;
if (o._id && !this.find_artifact_by_id(o._id)) {
this.update_board_artifact_viewmodel(new_artifact);
this.active_space_artifacts.push(new_artifact)
} else {
console.log("warning: got create on existing artifact.");
msg.action = "update"; // hackety hack!
}
} else console.log("artifact created in another space.");
}
else if (msg.action == "update" && msg.object) {
if (this.active_space) {
var o = msg.object;
if (o && o._id) {
var existing_artifact = this.find_artifact_by_id(o._id);
if (!existing_artifact) {
existing_artifact = o;
} else {
for (key in o) {
existing_artifact[key] = o[key];
this.update_board_artifact_viewmodel(existing_artifact);
}
}
}
}
}
else if (msg.action == "delete" && msg.object) {
if (this.active_space) {
var o = msg.object;
if(o._id){
var existing_artifact = this.find_artifact_by_id(o._id);
if (existing_artifact) {
var idx = this.active_space_artifacts.indexOf(existing_artifact);
this.active_space_artifacts.splice(idx, 1);
} else console.log("existing artifact to delete not found");
}else console.error("object without _id");
}
}
}
},
subscribe: function(space) {
if (this.websocket && this.websocket.readyState==1) {
this.websocket.send(JSON.stringify({action: "subscribe", space_id: space._id}));
} else {
console.error("socket not ready yet. (subscribe)");
}
},
is_member_online: function(space, member) {
if (!member.user) {
return false;
}
if (!this.users_online[space._id]) {
return false;
}
var isOnline = _.find(this.users_online[space._id], function(u) {
return (u._id == member.user._id);
});
return isOnline;
},
auth_websocket: function(space){
if (!this.websocket) {
this.init_websocket();
}
if (this.websocket && this.websocket.readyState==1) {
var auth_params = {
action: "auth",
editor_auth: space_auth,
editor_name: this.guest_nickname,
auth_token: window.socket_auth,
space_id: space._id
};
console.log("[websocket] auth space");
this.websocket.send(JSON.stringify(auth_params));
}
},
websocket_send: function(msg) {
if (!this.websocket) return;
if (this.websocket.readyState!=1) return;
try {
this.websocket.send(JSON.stringify(msg));
} catch (e) {
// catch NS problems
}
},
init_websocket: function() {
if (this.websocket) this.websocket = null;
if (this.current_timeout) {
clearTimeout(this.current_timeout);
this.current_timeout = null;
}
try {
this.websocket = new WebSocket(ENV.websocketsEndpoint + "/socket");
} catch (e) {
console.log("[websocket] cannot establish websocket connection: ",e);
this.current_timeout = setTimeout(function() {
console.log("[websocket] reconnecting", e);
this.init_websocket();
}.bind(this),5000);
}
if (!this.websocket) {
console.log("[websocket] no websocket support?");
return;
}
this.websocket.onopen = function(evt) {
if (this.current_timeout) {
clearTimeout(this.current_timeout);
this.current_timeout = null;
}
if (this.active_space_loaded) {
this.auth_websocket(this.active_space);
}
this.online = true;
}.bind(this);
this.websocket.onclose = function(evt) {
if (!window._spacedeck_location_change) {
this.online = false;
}
if (!this.current_timeout) {
this.current_timeout = setTimeout(function() {
console.log("[websocket] onclose: reconnecting", evt);
this.init_websocket();
}.bind(this),5000);
}
}.bind(this);
this.websocket.onmessage = function(evt) {
this.online = true;
try {
var msg = JSON.parse(evt.data);
} catch (e) {
console.log("[websocket] malformed message: ",evt.data);
return;
}
if (msg.channel_id == channel_id) {
return;
}
if (msg.action == "cursor") {
this.handle_user_cursor_update(msg);
}
else if (msg.action == "viewport") {
this.handle_presenter_viewport_update(msg);
}
else if (msg.action == "media") {
this.handle_presenter_media_update(msg);
}
if (msg.action == "update" || msg.action == "create" || msg.action == "delete"){
this.handle_live_updates(msg);
}
if (msg.action == "init") {
channel_id = msg.channel_id;
}
if (msg.action == "auth_valid") {
if (this.active_space) {
this.subscribe(this.active_space);
if (this.unsaved_transactions()) {
console.log("[websockets-saver] found unsaved transactions, triggering save.");
this.process_artifact_save_queue();
}
}
}
if (msg.action == "subscription_valid") {
console.log("subscription_valid");
}
if (msg.action == "status_update") {
var spaceId = msg.space_id;
var users = msg.users;
// filter ourselves
if (this.user && this.user._id) {
users = _.filter(users, function(u) {
return (u && (u._id != this.user._id));
}.bind(this));
}
users = _.filter(users, function(u) {
return (u && (u._id || u.nickname));
});
this.users_online[spaceId] = users;
if (this.active_space) {
if (this.active_space._id == spaceId) {
this.active_space_users = users;
}
}
}
}.bind(this);
this.websocket.onerror = function(evt) {
console.log("websocket.onerror:", evt);
if (!window._spacedeck_location_change) {
this.online = false;
this.was_offline = true;
}
if (!this.current_timeout) {
this.current_timeout = setTimeout(function() {
console.log("websocket.onerror: reconnecting", evt);
this.init_websocket();
}.bind(this),5000);
}
}.bind(this);
}
}
}
/*
Spacedeck Whiteboard Directive
This module registers a custom Vue directive that handles Whiteboard sections.
*/
function setup_whiteboard_directives() {
if ('ontouchstart' in window) {
var edown = "touchstart";
var emove = "touchmove";
var eup = "touchend";
} else {
var edown = "mousedown";
var emove = "mousemove";
var eup = "mouseup";
}
Vue.directive('sd-whiteboard', {
bind: function () {
var el = this.el;
$(el).on(edown, ".artifact", this.handle_mouse_down_artifact.bind(this));
$(el).on("dblclick", ".artifact", this.handle_double_click_artifact.bind(this));
$(el).on("keyup", ".artifact", this.handle_key_up_artifact.bind(this));
$(el).on("keydown", ".artifact", this.handle_key_down_artifact.bind(this));
$(el).bind(edown, this.handle_mouse_down_space.bind(this));
$(el).bind(emove, this.handle_mouse_move.bind(this));
$(el).bind(eup, this.handle_mouse_up_space.bind(this));
$(el).bind("wheel", this.handle_wheel_space.bind(this));
$(document.body).bind("mouseleave", this.handle_mouse_leave.bind(this));
$(el).find(".handle.resize-nw").bind(edown, function(e){this.handle_transform_mouse_down(e,1,1)}.bind(this));
$(el).find(".handle.resize-n").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,1)}.bind(this));
$(el).find(".handle.resize-ne").bind(edown, function(e){this.handle_transform_mouse_down(e,0,1)}.bind(this));
$(el).find(".handle.resize-e").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0.5)}.bind(this));
$(el).find(".handle.resize-se").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0)}.bind(this));
$(el).find(".handle.resize-s").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,0)}.bind(this));
$(el).find(".handle.resize-sw").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0)}.bind(this));
$(el).find(".handle.resize-w").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0.5)}.bind(this));
$(el).find(".edge-handle.resize-n").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,1)}.bind(this));
$(el).find(".edge-handle.resize-s").bind(edown, function(e){this.handle_transform_mouse_down(e,0.5,0)}.bind(this));
$(el).find(".edge-handle.resize-e").bind(edown, function(e){this.handle_transform_mouse_down(e,0,0.5)}.bind(this));
$(el).find(".edge-handle.resize-w").bind(edown, function(e){this.handle_transform_mouse_down(e,1,0.5)}.bind(this));
$(el).on(edown, ".vector-handle", function(e){this.handle_vector_transform_mouse_down(e)}.bind(this));
var $scope = this.vm.$root;
this.space_zoom = 1;
this.artifacts_before_transaction = [];
$scope.active_tool = "pointer";
},
update: function () {
},
unbind: function () {
// do clean up work
// e.g. remove event listeners added in bind()
var el = this.el;
$(el).off(edown+" "+emove+" "+eup+" "+"keyup keydown mouseleave");
$(document.body).unbind("mouseleave");
},
handle_key_down_artifact: function(evt) {
var $scope = this.vm.$root;
},
handle_key_up_artifact: function(evt) {
var $scope = this.vm.$root;
},
handle_mouse_down_artifact: function(evt) {
var $scope = this.vm.$root;
if (!$scope.editing_artifact_id) {
evt.preventDefault();
evt.stopPropagation();
}
var a = $scope.find_artifact_by_id(evt.currentTarget.id.replace("artifact-",""));
if ($scope.active_tool == "zoom") return;
if ($scope.active_tool == "eyedrop") {
var arts = $scope.selected_artifacts();
if (!$scope.is_selected(a) && arts.length > 0) {
// copy style from clicked artifact to selected artifacts
$scope.begin_transaction();
$scope.update_selected_artifacts(function(selected_artifact) {
selected_artifact.style = _.clone(a.style);
});
$scope.active_tool = "pointer";
return;
}
}
if ($scope.active_tool == "pan") {
this.start_pan(evt);
return;
}
if ($scope.active_tool == "pointer") {
if (!$scope.is_selected(a) || evt.shiftKey) {
this.select(evt,a);
}
// copy via alt+move
if (evt.altKey) {
a = $scope.clone_artifact(a);
this.select(evt,a);
}
}
$scope.begin_transaction();
var cursor = this.cursor_point_to_space(evt);
$scope.mouse_ox = cursor.x;
$scope.mouse_oy = cursor.y;
$scope.mouse_moved = false;
this.mouse_state = "move";
evt.stopPropagation();
},
handle_double_click_artifact: function(evt) {
var $scope = this.vm.$root;
var a = $scope.find_artifact_by_id(evt.currentTarget.id.replace("artifact-",""));
if (!a) return;
if (a.payload_uri) {
$scope.download_selected_artifacts();
}
$scope.toggle_selected_artifact_editing(true);
},
handle_transform_mouse_down: function(evt,origin_x,origin_y) {
evt.stopPropagation();
evt.preventDefault();
var $scope = this.vm.$root;
$scope.begin_transaction();
var cursor = this.cursor_point_to_space(evt);
this.mouse_state = "transform";
$scope.mouse_ox = cursor.x;
$scope.mouse_oy = cursor.y;
$scope.transform_ox = origin_x;
$scope.transform_oy = origin_y;
},
handle_vector_transform_mouse_down: function(evt) {
evt.stopPropagation();
evt.preventDefault();
var $scope = this.vm.$root;
var idx = parseInt($(evt.currentTarget).attr("data-idx"));
$scope.selected_control_point_idx = idx;
$scope.begin_transaction();
var cursor = this.cursor_point_to_space(evt);
this.mouse_state = "vector_transform";
$scope.mouse_ox = cursor.x;
$scope.mouse_oy = cursor.y;
//$scope.transform_ox = origin_x;
//$scope.transform_oy = origin_y;
},
handle_wheel_space: function(evt) {
var $scope = this.vm.$root;
if (!evt.ctrlKey && !evt.shiftKey) return;
evt.preventDefault();
evt.stopPropagation();
var amount = 1;
var dy = evt.originalEvent.deltaY;
if (dy>0) {
amount=1.2;
if ($scope.viewport_zoom<=0.05) return false;
} else if (dy<0) {
amount=0.9;
if ($scope.viewport_zoom>=2) return false;
} else {
return false;
}
$scope.zoom_to_cursor(evt,amount);
},
handle_mouse_down_space: function(evt) {
if (evt.target != evt.currentTarget && !_.include(["wrapper"],evt.target.className)) return;
var $scope = this.vm.$root;
$scope.opened_dialog="none";
var cursor = this.cursor_point_to_space(evt);
$scope.mouse_ox = cursor.x;
$scope.mouse_oy = cursor.y;
if (evt.which == 2 || evt.buttons == 4) {
$scope.active_tool = "pan";
}
if ($scope.active_tool=="note") {
this.deselect();
this.mouse_state = "transform";
$scope.mouse_state = this.mouse_state;
this.start_adding_note(evt);
return;
} else if ($scope.active_tool=="arrow") {
this.deselect();
this.mouse_state = "vector_transform";
$scope.mouse_state = this.mouse_state;
this.start_drawing_arrow(evt);
return;
} else if ($scope.active_tool=="line") {
this.deselect();
this.mouse_state = "vector_transform";
$scope.mouse_state = this.mouse_state;
this.start_drawing_line(evt);
return;
} else if ($scope.active_tool=="scribble") {
this.deselect();
this.mouse_state = "scribble";
$scope.mouse_state = this.mouse_state;
this.start_drawing_scribble(evt);
return;
} else if ($scope.active_tool=="zoom") {
if (evt.altKey) {
$scope.zoom_out();
} else {
$scope.zoom_in();
}
return;
} else if ($scope.active_tool=="pointer") {
this.mouse_state = "lasso";
this.start_lasso(evt);
} else if ($scope.active_tool=="zone") {
this.deselect();
this.mouse_state = "transform";
$scope.start_adding_zone(evt);
return;
} else if ($scope.active_tool=="image") {
this.deselect();
this.mouse_state = "transform";
$scope.start_adding_placeholder(evt);
return;
} else if ($scope.active_tool=="pan") {
this.start_pan(evt);
return;
}
if ($scope.selection_metrics.count>0) {
this._no_artifact_toolbar_this_round = true;
}
this.deselect();
},
start_pan: function(evt) {
var $scope = this.vm.$root;
el = $("#space")[0];
if (el) {
this.mouse_state = "pan";
this.old_panx = el.scrollLeft;
this.old_pany = el.scrollTop;
}
var cursor = this.cursor_point_to_space(evt);
$scope.mouse_ox = cursor.x;
$scope.mouse_oy = cursor.y;
$scope.mouse_moved = false;
},
deselect: function() {
var $scope = this.vm.$root;
$scope.deselect();
},
select: function(evt, a) {
var $scope = this.vm.$root;
$scope.select(evt, a);
},
multi_select: function(arts) {
var $scope = this.vm.$root;
$scope.multi_select(arts);
},
start_lasso: function(evt) {
var point = this.cursor_point_to_space(evt);
this.lasso = {
x: point.x,
y: point.y,
w: 0,
h: 0
}
},
rects_intersecting: function(r1,r2) {
if (!r1 || !r2) return false;
if ( (r1.x+r1.w < r2.x)
|| (r1.x > r2.x+r2.w)
|| (r1.y+r1.h < r2.y)
|| (r1.y > r2.y+r2.h) ) return false;
return true;
},
artifacts_in_rect: function(rect) {
if (!rect) return [];
var $scope = this.vm.$root;
return _.filter($scope.active_space_artifacts, function(a) {
return this.rects_intersecting(a.board, rect);
}.bind(this));
},
abs_rect: function(rect) {
var res = {
x: rect.x,
y: rect.y,
w: Math.abs(rect.w),
h: Math.abs(rect.h)
}
if (rect.w<0) res.x+=rect.w;
if (rect.h<0) res.y+=rect.h;
return res;
},
lasso_style: function() {
var $scope = this.vm.$root;
if (!this.lasso) return "";
var lasso_scaled = {
x:this.lasso.x,
y:this.lasso.y,
w:this.lasso.w*$scope.viewport_zoom,
h:this.lasso.h*$scope.viewport_zoom
}
lasso_scaled = this.abs_rect(lasso_scaled);
lasso_scaled.x += $scope.bounds_margin_horiz;
lasso_scaled.y += $scope.bounds_margin_vert;
var s = "left:" +lasso_scaled.x+"px;";
s += "top:" +lasso_scaled.y+"px;";
s += "width:" +lasso_scaled.w+"px;";
s += "height:"+lasso_scaled.h+"px;";
s += "opacity: 1;";
return s;
},
render_lasso: function() {
if (!this.lasso) {
$("#lasso").hide();
return;
}
$("#lasso").attr("style", this.lasso_style());
$("#lasso").show();
},
cursor_point_to_space: function(evt) {
var $scope = this.vm.$root;
var offset = {left: 0, top: 0};
evt = fixup_touches(evt);
return {
x: (parseInt(evt.pageX) - parseInt(offset.left) - $scope.bounds_margin_horiz) / this.space_zoom,
y: (parseInt(evt.pageY) - parseInt(offset.top) - $scope.bounds_margin_vert) / this.space_zoom
};
},
rect_to_points: function(rect) {
return [
{x:rect.x,y:rect.y},
{x:rect.x+rect.w,y:rect.y},
{x:rect.x,y:rect.y+rect.h},
{x:rect.x+rect.w,y:rect.y+rect.h}
];
},
old_selection_rect: function() {
var $scope = this.vm.$root;
var selected = $scope.selected_artifacts().map(function(a){
return $scope.find_artifact_before_transaction(a);
}.bind(this));
return $scope.enclosing_rect(selected);
},
snap_point: function(x,y,snap_middle) {
var $scope = this.vm.$root;
var TOL = 8;
var dists = [];
if (snap_middle) {
dists.push([[x-window.innerWidth/2,Math.abs(y-window.innerHeight/2)],[x-window.innerWidth/2,Math.abs(y-window.innerHeight/2)]]);
}
if ($scope.grid_active) {
// snap to grid
var gw = $scope.grid.spacing/$scope.grid.subdivisions;
var gh = $scope.grid.spacing/$scope.grid.subdivisions;
var sx1 = parseInt(x/gw)*gw;
var sy1 = parseInt(y/gh)*gh;
var sx2 = (parseInt(x/gw)+1)*gw;
var sy2 = (parseInt(y/gh)+1)*gh;
dists = [[[Math.abs(sx1-x),sx1], [Math.abs(sy1-y),sy1]],
[[Math.abs(sx2-x),sx2], [Math.abs(sy2-y),sy2]]];
} else {
// snap to other artifacts
dists = $scope.unselected_artifacts().map(function(a){
var r = this.rect_to_points(a.board);
var xd1 = Math.abs(r[0].x-x);
var xd2 = Math.abs(r[1].x-x);
var xd3 = Math.abs(r[0].x+a.board.w/2 - x);
var yd1 = Math.abs(r[0].y-y);
var yd2 = Math.abs(r[2].y-y);
var yd3 = Math.abs(r[0].y+a.board.h/2 - y);
if (!snap_middle) {
if (xd2<xd1) {
var xd = xd2;
var sx = r[1].x;
} else {
var xd = xd1;
var sx = r[0].x;
}
if (yd2<yd1) {
var yd = yd2;
var sy = r[2].y;
} else {
var yd = yd1;
var sy = r[0].y;
}
}
if (snap_middle) {
var xd = xd3;
var sx = r[0].x+a.board.w/2;
var yd = yd3;
var sy = r[0].y+a.board.h/2;
}
return [[xd,sx],[yd,sy]];
}.bind(this));
}
// snap to space edges
dists.push([[Math.abs(x),0],[Math.abs(y),0]]);
//dists.push([[Math.abs(dims.width-x),dims.width],[Math.abs(dims.height-y),dims.height]]);
var unzipped = _.unzip(dists);
var xdists = _.sortBy(unzipped[0], function(pair) {return pair[0];});
var ydists = _.sortBy(unzipped[1], function(pair) {return pair[0];});
var results = {snapx:xdists[0], snapy:ydists[0]};
if (!xdists[0] || xdists[0][0]>TOL) {
results.snapx = [0,x]; // distance, coordinate
} else {
//$scope.snap_ruler_x = xdists[0][1];
}
if (!ydists[0] || ydists[0][0]>TOL) {
results.snapy = [0,y];
} else {
//$scope.snap_ruler_y = ydists[0][1];
}
return results;
},
offset_point_in_wrapper: function(point) {
var $scope = this.vm.$root;
var section_el = $(this.el)[0];
var z = $scope.viewport_zoom;
var pt = parseInt($("#space").css("padding-top"));
point.y=(point.y+section_el.scrollTop-pt)/z;
point.x=(point.x+section_el.scrollLeft)/z;
return point;
},
start_drawing_scribble: function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $scope = this.vm.$root;
var point = this.offset_point_in_wrapper(this.cursor_point_to_space(evt));
var z = $scope.highest_z()+1;
$scope.deselect();
var a = {
space_id: $scope.active_space._id,
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "scribble"
}
};
$scope.save_artifact(a, function(saved_a) {
$scope.update_board_artifact_viewmodel(saved_a);
$scope.active_space_artifacts.push(saved_a);
this.select(evt,saved_a);
//$scope.tool_artifact = a;
$scope.transform_ox = 0;
$scope.transform_oy = 0;
$scope.begin_transaction();
}.bind(this));
},
start_drawing_arrow: function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $scope = this.vm.$root;
var point = this.cursor_point_to_space(evt);
this.offset_point_in_wrapper(point);
var z = $scope.highest_z()+1;
var a = {
space_id: $scope.active_space._id,
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0},{dx:0,dy:0},{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "arrow"
}
};
$scope.save_artifact(a, function(saved_a) {
$scope.update_board_artifact_viewmodel(saved_a);
$scope.active_space_artifacts.push(saved_a);
$scope.select(evt,a);
$scope.selected_control_point_idx = 1;
$scope.transform_ox = 0;
$scope.transform_oy = 0;
$scope.begin_transaction();
}.bind(this));
},
// FIXME: consolidate with arrow drawing?
start_drawing_line: function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $scope = this.vm.$root;
var point = this.cursor_point_to_space(evt);
this.offset_point_in_wrapper(point);
var z = $scope.highest_z()+1;
var a = {
space_id: $scope.active_space._id,
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0},{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "line"
}
};
$scope.save_artifact(a, function(saved_a) {
$scope.update_board_artifact_viewmodel(saved_a);
$scope.active_space_artifacts.push(saved_a);
$scope.select(evt,a);
$scope.selected_control_point_idx = 1;
$scope.transform_ox = 0;
$scope.transform_oy = 0;
$scope.begin_transaction();
}.bind(this));
},
snap_point_simple: function(point) {
var snapped = this.snap_point(point.x, point.y);
return {
x: snapped.snapx[1],
y: snapped.snapy[1]
}
},
handle_mouse_up_space: function(evt) {
var $scope = this.vm.$root;
evt.preventDefault();
if (this.mouse_state == "lasso") {
var lasso_rect = this.abs_rect(this.offset_point_in_wrapper(this.lasso));
// convert to space coordinates
if (lasso_rect.w>0 && lasso_rect.h>0) {
var arts = this.artifacts_in_rect(lasso_rect);
this.multi_select(arts);
} else {
if (this._no_artifact_toolbar_this_round) {
this._no_artifact_toolbar_this_round = false;
} else {
$scope.start_adding_artifact(evt);
}
}
this.lasso = null;
this.render_lasso();
}
else if (_.include(["transform","move","vector_transform","scribble"],this.mouse_state)) {
var ars = $scope.selected_artifacts();
for (var i=0; i<ars.length; i++) {
if (_.include(["text","placeholder"],$scope.artifact_major_type(ars[i]))) {
// some types of artifact need a minimum size
if (ars[i].board.w<10) {
ars[i].board.w = 10;
}
if (ars[i].board.h<10) {
ars[i].board.h = 10;
}
}
//save_artifact(ars[i], null, $scope.display_saving_error);
}
}
if (this.mouse_state == "text_editor") {
return;
}
if (_.include(["zoom"], $scope.active_tool)) {
// tools that stay active after use
this.mouse_state = "idle";
$scope.mouse_state = this.mouse_state;
$scope.end_transaction();
$scope.deselect();
return;
}
this.mouse_state = "idle";
$scope.mouse_state = this.mouse_state;
this.lasso = null;
$scope.active_tool = "pointer";
$scope.end_transaction();
$scope.show_toolbar_props();
},
handle_mouse_leave: function(evt) {
var $scope = this.vm.$root;
this.mouse_state = "idle";
this.lasso = null;
$scope.active_tool = "pointer";
$scope.end_transaction();
this.render_lasso();
},
handle_mouse_move: function(evt) {
var $scope = this.vm.$root;
if (!$scope.active_space) return;
if (!$scope.editing_artifact_id) {
evt.preventDefault();
evt.stopPropagation();
}
$scope.handle_scroll();
var cursor = this.cursor_point_to_space(evt);
var dx = cursor.x - $scope.mouse_ox;
var dy = cursor.y - $scope.mouse_oy;
var dt = (new Date()).getTime() - this.last_mouse_move_time;
this.last_mouse_move_time = (new Date()).getTime();
var zoom = $scope.viewport_zoom||1;
if (zoom) {
dx/=zoom;
dy/=zoom;
}
// send cursor
if (dx>10 || dy>10 || dt>100) {
var name = "anonymous";
if ($scope.logged_in) {
name = $scope.user.nickname || $scope.user.email;
} else {
name = $scope.guest_nickname || "anonymous";
}
var cursor_msg = {
action: "cursor",
x: cursor.x/zoom,
y: cursor.y/zoom,
name: name,
id: $scope.user._id||name
};
$scope.websocket_send(cursor_msg);
}
// side effects ftw!
$scope.snap_ruler_x = -1000;
$scope.snap_ruler_y = -1000;
$scope.mouse_moved = true;
$scope.transform_lock = evt.shiftKey;
if ($scope.transform_lock) {
if (this.mouse_state == "transform") {
// lock aspect is done in transform
} else {
// lock axis
if (Math.abs(dy)>Math.abs(dx)) {
dx = 0;
} else {
dy = 0;
}
}
}
if (this.mouse_state == "move") {
$scope.hide_toolbar_props();
var snap_dx = 0;
var snap_dy = 0;
var selected = $scope.selected_artifacts();
var snap_edges = this.old_selection_rect();
if (selected.length && selected[0]._id==$scope.editing_artifact_id) {
// bail out of moving editable artifact
return;
}
if (snap_edges) {
var mx = snap_edges.x1 + (snap_edges.x2-snap_edges.x1)/2;
var my = snap_edges.y1 + (snap_edges.y2-snap_edges.y1)/2;
var snapped1 = this.snap_point(snap_edges.x1 + dx, snap_edges.y1 + dy, false);
var snapped2 = this.snap_point(snap_edges.x2 + dx, snap_edges.y2 + dy, false);
var snapped3 = this.snap_point(mx + dx, my + dy, true);
if (snapped3.snapx[0]>0) {
snap_dx = mx + dx - snapped3.snapx[1];
} else if (snapped2.snapx[0]>0) {
snap_dx = snap_edges.x2 + dx - snapped2.snapx[1];
} else {
snap_dx = snap_edges.x1 + dx - snapped1.snapx[1];
}
if (snapped3.snapy[0]>0) {
snap_dy = my + dy - snapped3.snapy[1];
} else if (snapped2.snapy[0]>0) {
snap_dy = snap_edges.y2 + dy - snapped2.snapy[1];
} else {
snap_dy = snap_edges.y1 + dy - snapped1.snapy[1];
}
}
$scope.update_selected_artifacts(function(a) {
var old_a = $scope.find_artifact_before_transaction(a);
if (old_a) {
return {
board: _.extend(a.board, {
x: old_a.board.x + dx - snap_dx,
y: old_a.board.y + dy - snap_dy
})
};
} else {
// deleted?
return {};
}
}.bind(this));
} else if (this.mouse_state == "transform") {
var selected = $scope.selected_artifacts();
var edges = this.old_selection_rect();
if (!edges) {
this.mouse_state = "idle";
return;
}
$scope.hide_toolbar_props();
var ew = (edges.x2-edges.x1);
var eh = (edges.y2-edges.y1);
var origin_x = edges.x1 + ew * $scope.transform_ox;
var origin_y = edges.y1 + eh * $scope.transform_oy;
// "leading point"
var lead_x = edges.x1 + ew * (1-$scope.transform_ox) - origin_x;
var lead_y = edges.y1 + eh * (1-$scope.transform_oy) - origin_y;
var lead_snapped = this.snap_point(origin_x + lead_x + dx, origin_y + lead_y + dy);
var moved_x = (lead_snapped.snapx[1] - origin_x);
var moved_y = (lead_snapped.snapy[1] - origin_y);
var scale_x = lead_x ? (moved_x)/lead_x : 1;
var scale_y = lead_y ? (moved_y)/lead_y : 1;
if ($scope.transform_lock) scale_y = scale_x;
$scope.update_selected_artifacts(function(a) {
var old_a = $scope.find_artifact_before_transaction(a);
var x1 = origin_x + ((old_a.board.x - origin_x) * scale_x);
var y1 = origin_y + ((old_a.board.y - origin_y) * scale_y);
var x2 = origin_x + (((old_a.board.x + old_a.board.w) - origin_x) * scale_x);
var y2 = origin_y + (((old_a.board.y + old_a.board.h) - origin_y) * scale_y);
if (x1>x2) { var t = x1; x1 = x2; x2 = t; }
if (y1>y2) { var t = y1; y1 = y2; y2 = t; }
return {
board: _.extend(a.board, {
x: x1,
y: y1,
w: x2 - x1,
h: y2 - y1
})
};
}.bind(this));
} else if (this.mouse_state == "lasso") {
this.lasso.w = dx;
this.lasso.h = dy;
this.render_lasso();
} else if (this.mouse_state == "vector_transform") {
$scope.hide_toolbar_props();
var _this = this;
$scope.update_selected_artifacts(function(a) {
var old_a = $scope.find_artifact_before_transaction(a);
var control_points = _.cloneDeep(old_a.control_points);
var board = _.clone(old_a.board);
var cp = control_points[$scope.selected_control_point_idx];
var snapped = _this.snap_point(board.x+cp.dx+dx, board.y+cp.dy+dy);
dx = snapped.snapx[1]-(board.x+cp.dx);
dy = snapped.snapy[1]-(board.y+cp.dy);
cp.dx += dx;
cp.dy += dy;
// special case for arrow's 3rd point
if (a.style.shape == "arrow" && $scope.selected_control_point_idx!=2) {
/*control_points[2].dx += dx/2;
control_points[2].dy += dy/2; */
control_points[2].dx = (control_points[0].dx+control_points[1].dx)/2;
control_points[2].dy = (control_points[0].dy+control_points[1].dy)/2;
}
return _this.normalize_control_points(control_points, board);
});
} else if (this.mouse_state == "scribble") {
$scope.update_selected_artifacts(function(a) {
var old_a = a;
var control_points = _.cloneDeep(old_a.control_points);
var board = _.clone(old_a.board);
var offset = this.offset_point_in_wrapper({x:cursor.x,y:cursor.y});
control_points.push({
dx: offset.x-board.x,
dy: offset.y-board.y
});
return this.normalize_control_points(simplify_scribble_points(control_points), board);
}.bind(this));
var arts = $scope.selected_artifacts();
if (arts.length) {
$scope.update_board_artifact_viewmodel(arts[0]);
}
}
else if (this.mouse_state == "pan") {
if (!$("#space").length) return;
el = $("#space")[0];
el.scrollLeft = this.old_panx - dx*$scope.viewport_zoom;
el.scrollTop = this.old_pany - dy*$scope.viewport_zoom;
$scope.handle_scroll();
}
},
normalize_control_points: function(control_points, board) {
var x1 = _.min(control_points,"dx").dx;
var y1 = _.min(control_points,"dy").dy;
var x2 = _.max(control_points,"dx").dx;
var y2 = _.max(control_points,"dy").dy;
var shiftx = -x1;
var shifty = -y1;
var shifted_cps = control_points.map(function(cp) {
return {
dx: cp.dx + shiftx,
dy: cp.dy + shifty
};
});
var w = Math.abs(x2 - x1);
var h = Math.abs(y2 - y1);
var bshiftx = 0;
var bshifty = 0;
if (board.w < 0) bshiftx = -board.w;
if (board.h < 0) bshifty = -board.h;
var shifted_board = {
x: board.x + bshiftx - shiftx,
y: board.y + bshifty - shifty,
w: w,
h: h,
z: board.z
};
return {
board: shifted_board,
control_points: shifted_cps
};
}
});
}
function vec2_add(a,b) {
return {dx:a.dx+b.dx, dy:a.dy+b.dy};
}
function vec2_sub(a,b) {
return {dx:a.dx-b.dx, dy:a.dy-b.dy};
}
function vec2_mul(a,f) {
return {dx:a.dx*f, dy:a.dy*f};
}
function vec2_magn(v) {
return Math.sqrt(v.dx*v.dx + v.dy*v.dy);
}
function vec2_unit(v) {
var m = vec2_magn(v);
if (m == 0) return {dx:0, dy:0};
return {dx:v.dx/m, dy:v.dy/m};
}
function vec2_angle(v) {
if (v.dx==0) return Math.atan2(v.dx+0.01, v.dy);
return Math.atan2(v.dx, v.dy);
}
function render_vector_drawing(a, padding) {
var shape = a.style.shape || "";
var path = [];
var p = a.control_points[0];
if (!p) return "";
path.push("M" + (p.dx + padding) + "," + (p.dy + padding) + " ");
if (shape.match("arrow")) {
var cps = a.control_points[0];
var cpe = a.control_points[1];
var cpm = a.control_points[2];
if (!cpm) cpm = cpe;
var markerId = a._id;
var origin = cps;
var end = cpe;
var vec = vec2_sub(end, origin);
var length = vec2_magn(vec);
var middleVec = vec2_mul(vec2_unit(vec),length / 2);
var middlePoint = vec2_add(origin, middleVec);
var ortho = vec2_sub(cpm, middlePoint);
var scaledMiddlePoint = vec2_add(vec2_mul(ortho,2), middlePoint);
var d = "M" + (cps.dx + padding) + "," + (cps.dy + padding) + " Q" + (scaledMiddlePoint.dx + padding) + "," + (scaledMiddlePoint.dy + padding) + " " + (cpe.dx + padding) + "," + (cpe.dy + padding);
var tip = "<defs><marker id='ae" + markerId + "' refX=\"0.1\" refY=\"3\" markerWidth=\"3\" markerHeight=\"6\" orient=\"auto\">";
tip += "<path d=\"M-3,0 V6 L3,3 Z\" fill=\""+a.style.stroke_color+"\" stroke-width=\"0\"/></marker></defs>";
var svg = tip + "<path d='" + d + "' style='stroke-width:" + a.style.stroke + ";' marker-end='url(#ae" + markerId + ")'/>";
return svg;
}
else if (false /*shape.match("scribble")*/) {
var idx = 0;
while (idx < a.control_points.length - 1) {
var prevP = a.control_points[idx];
if (a.control_points.length > idx + 1) {
var p = a.control_points[idx + 1];
} else {
var p = prevP;
}
if (a.control_points.length > idx + 2) {
var nextP = a.control_points[idx + 2];
} else {
var nextP = p;
}
var dpy = (p.dy - prevP.dy);
var dpx = (p.dx - prevP.dx);
var dny = (nextP.dy - p.dy);
var dnx = (nextP.dx - p.dx);
var distToNext = Math.sqrt(dny * dny + dnx * dnx);
var distToPrev = Math.sqrt(dpy * dpy + dpx * dpx);
var r = Math.sqrt((distToNext + distToPrev) / 2) * 2;
var prevAngle = Math.atan2(dpy, dpx);
var nextAngle = Math.atan2(dny, dnx);
var bisectAngle = (prevAngle + nextAngle) / 2;
var tangentAngle = bisectAngle;
var cp1x = p.dx + Math.cos(tangentAngle) * -r;
var cp1y = p.dy + Math.sin(tangentAngle) * -r;
var cp2x = p.dx + Math.cos(tangentAngle) * r;
var cp2y = p.dy + Math.sin(tangentAngle) * r;
var dcp1x = cp1x - nextP.dx;
var dcp1y = cp1y - nextP.dy;
var dcp2x = cp2x - nextP.dx;
var dcp2y = cp2y - nextP.dy;
var distToCp1 = Math.sqrt(dcp1x * dcp1x + dcp1y * dcp1y) / r;
var distToCp2 = Math.sqrt(dcp2x * dcp2x + dcp2y * dcp2y) / r;
if (distToCp1 > distToCp2) {
var curve = "S" + (cp1x + padding) + "," + (cp1y + padding) + " " + (p.dx + padding) + "," + (p.dy + padding);
}
else {
var curve = "S" + (cp2x + padding) + "," + (cp2y + padding) + " " + (p.dx + padding) + "," + (p.dy + padding);
}
path.push(curve);
idx += 1;
}
} else {
for (var idx=0; idx<a.control_points.length; idx++) {
var p = a.control_points[idx];
var command = (idx==0) ? 'M' : 'L';
path.push(command+(p.dx+padding)+','+(p.dy+padding));
}
}
return "<path d='"+path.join(' ')+"'>";
}
function render_vector_star(edges,xradius,yradius,offset) {
edges *= 2;
var points = [];
var degrees = 360 / edges;
for (var i=0; i < edges; i++) {
var a = i * degrees - 90;
var xr = xradius;
var yr = yradius;
if (i%2) {
if (edges==20) {
xr/=1.5;
yr/=1.5;
} else {
xr/=2.8;
yr/=2.8;
}
}
var x = offset + xradius + xr * Math.cos(a * Math.PI / 180);
var y = offset + yradius + yr * Math.sin(a * Math.PI / 180);
points.push(x+","+y);
}
return "<polygon points='"+points.join(" ")+"'/>";
}
function transform_vector_template(cmds, xr, yr, offset) {
var cmd_str = "";
for (var i = 0; i<cmds.length; i+=2) {
var vals = cmds[i+1];
for (var j = 0; j<vals.length; j+=2) {
vals[j]*=(2*xr/100.0);
vals[j+1]*=(2*yr/100.0);
}
cmd_str+=cmds[i]+cmds[i+1].join(',')+" ";
}
return cmd_str;
}
function render_vector_heart(xr, yr, offset) {
var cmds = ['M',[50.141,98.5],
'c',[0,0,-49,-38.334,-49,-67.982],
'C',[1.141,15.333,14.356,1,30.659,1],
'c',[7.437,0,14.244,2.791,19.435,7.33],
'l',[0,0],
'C',[55.296,3.742,62.141,1,69.622,1],
'c',[16.303,0,29.519,14.166,29.519,29.518],
'C',[99.141,60.334,50.141,98.5,50.141,98.5],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_cloud(xr, yr, offset) {
var cmds = ['M',[17.544,99.729],
'c',[0,0,-17.544,6.929,-17.544,-36.699],
'c',[0,-18.698,19.298,-28.047,19.298,-9.35],
'c',[0,0,-3.508,-54.46,26.316,-53.672],
'C',[71.93,0.704,68.421,34.983,68.421,34.983],
'S',[100,25.634,100,72.379],
'c',[0,28.047,-21.053,27.351,-21.053,27.351],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_ellipse(xr, yr, offset) {
svg = "<ellipse cx="+(xr+offset)+" cy="+(yr+offset)+" rx="+xr+" ry="+yr+">";
return svg;
}
function render_vector_speechbubble(xr, yr, offset) {
var cmds = ['M',[100,50],
'c',[0,9.5,-2.7,18,-7.4,26],
'C',[90,80,100,100,100,100],
's',[-23.194,-6.417,-28,-4.162],
'c',[-6.375,3,-13.5,4.7,-21,4.7],
'C',[23,100,0.5,77,0.5,50],
'C',[0.5,23,23,0.5,50,0.5],
'C',[77,0.5,100,23,100,50],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_ngon(edges,xradius,yradius,offset) {
var points = [];
var degrees = 360 / edges;
for (var i=0; i < edges; i++) {
var a = i * degrees - 90;
var x = offset + xradius + xradius * Math.cos(a * Math.PI / 180);
var y = offset + yradius + yradius * Math.sin(a * Math.PI / 180);
points.push(x+","+y);
}
return "<polygon points='"+points.join(" ")+"'/>";
}
function render_vector_rect(xradius,yradius,offset) {
return "<rect x='0' y='0' width='"+xradius*2+"' height='"+xradius*2+"'/>";
}
function render_vector_shape(a) {
var stroke = parseInt(a.style.stroke) + 4;
var offset = stroke / 2;
var xr = (a.board.w-stroke) / 2;
var yr = (a.board.h-stroke) / 2;
var shape_renderers = {
ellipse: function() { return render_vector_ellipse(xr, yr, offset); },
pentagon: function() { return render_vector_ngon(5, xr, yr, offset); },
hexagon: function() { return render_vector_ngon(6, xr, yr, offset); },
octagon: function() { return render_vector_ngon(8, xr, yr, offset); },
diamond: function() { return render_vector_ngon(4, xr, yr, offset); },
square: function() { return "" },
triangle: function() { return render_vector_ngon(3, xr, yr, offset); },
star: function() { return render_vector_star(5, xr, yr, offset); },
burst: function() { return render_vector_star(10, xr, yr, offset); },
speechbubble: function() { return render_vector_speechbubble(xr, yr, offset); },
heart: function() { return render_vector_heart(xr, yr, offset); },
cloud: function() { return render_vector_cloud(xr, yr, offset); },
}
var render_func = shape_renderers[a.style.shape];
if (!render_func) return "";
return render_func();
}
function simplify_scribble_points(control_points) {
var filtered_points = [];
var thresh = 2;
var idx=0;
for (var i=0; i<control_points.length; i++) {
var cp = control_points[i];
var next = control_points[i+1];
if (i>0) {
var prev = control_points[i-1];
}
if (next && prev) {
dprev = vec2_sub(cp, prev);
dnext = vec2_sub(next, cp);
aprev = vec2_angle(dprev);
anext = vec2_angle(dnext);
delta = Math.abs(Math.abs(aprev)-Math.abs(anext));
delta2 = vec2_magn(vec2_sub(cp,prev));
if (delta2>thresh && delta>0.1) {
filtered_points.push(cp);
}
}
else {
filtered_points.push(cp);
}
}
return filtered_points;
}
if (typeof(window) == 'undefined') {
exports.render_vector_shape = render_vector_shape;
exports.render_vector_drawing = render_vector_drawing;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
"use strict";
var config = require('config');
require('../../models/schema');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var async = require('async');
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router();
var userMapping = { '_id': 1, 'nickname': 1, 'email': 1};
var spaceMapping = { '_id': 1, name: 1};
router.get('/:membership_id/accept', function(req, res, next) {
if (req.user) {
Membership.findOne({
_id: req.params.membership_id,
state: "pending",
code: req.query.code,
user: { "$exists": false }
}).populate('space').exec((err,mem) => {
if (err) res.sendStatus(400);
else {
if (mem) {
if(!mem.user) {
mem.code = null;
mem.state = "active";
mem.user = req.user;
mem.save(function(err){
if (err) res.status(400).json(err);
else {
console.log(mem);
res.status(200).json(mem);
}
});
} else {
res.status(400).json({"error": "already_used"});
}
} else {
res.status(404).json({"error": "not found"});
}
}
});
} else {
res.sendStatus(403);
}
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var bcrypt = require('bcrypt');
var crypo = require('crypto');
var express = require('express');
var router = express.Router();
router.post('/', function(req, res) {
var data = req.body;
if (data.email && data.password) {
var email = req.body.email.toLowerCase();
var password = req.body["password"];
User.find({email: email, account_type: "email"}, (function (err, users) {
if (err) {
res.status(400).json({"error":"session.users"});
} else {
if (users.length == 1) {
var user = users[0];
if (bcrypt.compareSync(password, user.password_hash)) {
crypo.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex');
var session = {
token: token,
ip: req.ip,
device: "web",
created_at: new Date()
};
if (!user.sessions)
user.sessions = [];
user.sessions.push(session);
user.save(function(err, result) {
// FIXME
var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging";
var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost";
res.cookie('sdsession', token, { domain: domain, httpOnly: true, secure: secure});
res.status(201).json(session);
});
});
}else{
res.sendStatus(403);
}
} else {
res.sendStatus(404);
}
}
}));
} else {
res.status(400).json({});
}
});
router.delete('/current', function(req, res, next) {
if (req.user) {
var user = req.user;
var newSessions = user.sessions.filter( function(session){
return session.token != req.token;
});
user.sessions = newSessions;
user.save(function(err, result) {
// FIXME
var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost";
res.clearCookie('sdsession', { domain: domain });
res.sendStatus(204);
});
} else {
res.sendStatus(404);
}
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var payloadConverter = require('../../helpers/artifact_converter');
var redis = require('../../helpers/redis');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
// ARTIFACTS
router.get('/', (req, res) => {
Artifact.find({
space_id: req.space._id
}).exec((err, artifacts) => {
async.map(artifacts, (a, cb) => {
a = a.toObject();
if (a.user_id) {
User.findOne({
"_id": a.user_id
}).select({
"_id": 1,
"nickname": 1,
"email": 1
}).exec((err, user) => {
a['user'] = user.toObject();
cb(err, a);
});
} else {
cb(null, a);
}
}, (err, mappedArtifacts) => {
if (err) res.status(400).json(err);
else {
res.status(200).json(mappedArtifacts);
}
});
});
});
router.post('/', function(req, res, next) {
var attrs = req.body;
attrs['space_id'] = req.space._id;
var artifact = new Artifact(attrs);
artifact.created_from_ip = req['real_ip'];
if (req.user) {
artifact.user_id = req.user._id;
artifact.last_update_user_id = req.user._id;
} else {
artifact.last_update_editor_name = req.editor_name;
}
if (req.spaceRole == "editor"  ||  req.spaceRole == "admin") {
artifact.save(function(err) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeCreate("Artifact", artifact);
}
});
} else {
res.status(401).json({
"error": "no access"
});
}
});
router.post('/:artifact_id/payload', function(req, res, next) {
if (req.spaceRole == "editor"  ||  req.spaceRole == "admin") {
var a = req.artifact;
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9_\-\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
var progress_callback = function(progress_msg) {
a.description = progress_msg;
a.save();
redis.sendMessage("update", a, a.toJSON(), req.channelId);
};
stream.on('finish', function() {
payloadConverter.convert(a, fileName, localFilePath, function(error, artifact) {
if (error) res.status(400).json(error);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeUpdate("Artifact", artifact);
}
}, progress_callback);
});
} else {
res.status(401).json({
"error": "no access"
});
}
});
router.put('/:artifact_id', function(req, res, next) {
var a = req.artifact;
var newAttr = req.body;
newAttr.updated_at = new Date();
delete newAttr['_id'];
if (req.user) {
newAttr.last_update_user_id = req.user._id;
} else {
newAttr.last_update_editor_name = req.editor_name;
}
Artifact.findOneAndUpdate({
"_id": a._id
}, {
"$set": newAttr
}, {
"new": true
}, function(err, artifact) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeUpdate("Artifact", artifact);
}
});
});
router.delete('/:artifact_id', function(req, res, next) {
var artifact = req.artifact;
artifact.remove(function(err) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeDelete("Artifact", artifact);
}
});
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
};
router.get('/', function(req, res, next) {
var showActionForSpaces = function(err, spaceIds) {
var userMapping = {
'_id': 1,
'nickname': 1,
'email': 1
};
var spaceMapping = {
'_id': 1,
'name': 1
};
var d = new Date();
d.setDate(d.getDate() - 1);
Action
.find({
created_at: {
"$gt": d
},
space: {
"$in": spaceIds
}
})
.populate('space', spaceMapping)
.populate('user', userMapping)
.exec(function(err, actions) {
var groupedBySpaceEvents = _.groupBy(actions, function(action) {
return action.space._id;
});
var events = _.map(groupedBySpaceEvents, function(value, key) {
var sortByDate = _.sortBy(groupedBySpaceEvents[key], function(o) {
return o.created_at;
});
var lastDate = sortByDate[sortByDate.length - 1].created_at;
var users = groupedBySpaceEvents[key].map(function(action) {
if (action.user) {
if (action.user._id.equals(req.user._id)) {
return "you";
} else {
return action.user.nickname;
}
}
return action.editor_name;
});
var uniqueUsers = _.unique(users);
return {
space: value[0].space,
users: uniqueUsers,
last_action: lastDate
};
});
var sortedEvents = _.sortBy(events, function(o) {
return -o.last_action;
});
res.status(200).json(sortedEvents);
});
};
if (!req.user) {
res.status(403).json({
error: "auth required"
});
} else {
if (!req.space._id.equals(req.user.home_folder_id)) {
Space.getRecursiveSubspacesForSpace(req.space, function(err, spaces) {
showActionForSpaces(err, spaces.map(function(s) {
return s._id;
}));
});
} else {
async.parallel({
bycreator: function(cb) {
Space.find({
creator: req.user._id
}, function(err, spaces) {
cb(null, spaces.map(function(s) {
return s._id;
}));
});
},
bymembership: function(cb) {
Membership.find({
user: req.user,
space: {
"$exists": 1
}
}).populate("space").exec(function(err, memberships) {
async.map(memberships, function(membership, memcb) {
Space.getRecursiveSubspacesForSpace(membership.space, function(err, spaces) {
cb(null, spaces.map(function(s) {
return s._id;
}));
});
}, function(err, spaceArrays) {
cb(null, spaceArrays.map(function(s) {
return s._id;
}));
});
});
}
}, function(err, results) {
var spaceIds = _.unique(results.bycreator.concat(results.bymembership));
showActionForSpaces(err, spaceIds);
});
}
}
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var async = require('async');
var moment = require('moment');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var sanitizeHtml = require('sanitize-html');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
router.get('/png', function(req, res, next) {
var triggered = new Date();
var s3_filename = "s" + req.space._id + "/" + "thumb_" + triggered.getTime() + ".jpg";
if (!req.space.thumbnail_updated_at || req.space.thumbnail_updated_at < req.space.updated_at || !req.space.thumbnail_url) {
Space.update({
"_id": req.space._id
}, {
"$set": {
thumbnail_updated_at: triggered
}
}, function(a, b, c) {});
phantom.takeScreenshot(req.space, "png",
function(local_path) {
var localResizedFilePath = local_path + ".thumb.jpg";
gm(local_path).resize(640, 480).quality(70.0).autoOrient().write(localResizedFilePath, function(err) {
if (err) {
console.error("screenshot resize error: ", err);
res.status(500).send("Error taking screenshot.");
return;
}
uploader.uploadFile(s3_filename, "image/jpeg", localResizedFilePath, function(err, thumbnailUrl) {
if (err) {
console.error("screenshot s3 upload error. filename: " + s3_filename + " details: ", err);
res.status(500).send("Error uploading screenshot.");
return;
}
var oldUrl = req.space.thumbnail_url;
Space.update({
"_id": req.space._id
}, {
"$set": {
thumbnail_url: thumbnailUrl
}
}, function(a, b, c) {
res.redirect(thumbnailUrl);
try {
if (oldUrl) {
var oldPath = url.parse(oldUrl).pathname;
uploader.removeFile(oldPath, function(err, res) {});
}
fs.unlink(local_path);
} catch (e) {
console.error(e);
}
});
try {
fs.unlink(localResizedFilePath);
} catch (e) {
console.error(e);
}
});
});
},
function() {
// on_error
console.error("phantom could not create screenshot for space " + req.space_id);
res.status(404).send("Not found");
});
} else {
res.redirect(req.space.thumbnail_url);
}
});
function make_export_filename(space, extension) {
return space.name.replace(/[^\w]/g, '') + "-" + space._id + "-" + moment().format("YYYYMMDD-HH-mm-ss") + "." + extension;
}
router.get('/list', function(req, res, next) {
if (req.user) {
if (req.spaceRole == "admin" ||  req.spaceRole == "editor") {
if (req.space.space_type == "space") {
Artifact.find({
space_id: req.space._id
}).exec(function(err, artifacts) {
async.map(artifacts, function(a, cb) {
if (a.user_id) {
User.findOne({
"_id": a.user_id
}).exec(function(err, user) {
a.user = user;
if (a.last_update_user_id) {
User.findOne({
"_id": a.last_update_user_id
}).exec(function(err, updateUser) {
a.update_user = updateUser;
cb(null, a);
});
} else {
cb(null, a);
}
});
} else {
cb(null, a);
}
}, function(err, mappedArtifacts) {
req.space.artifacts = mappedArtifacts.map(function(a) {
a.description = sanitizeHtml(a.description, {
allowedTags: [],
allowedAttributes: []
});
if (a.payload_uri) {
var parsed = url.parse(a.payload_uri);
var fileName = path.basename(parsed.pathname) || "file.bin";
a.filename = fileName;
}
return a;
});
res.render('artifact_list', {
space: req.space
});
});
});
} else {
Space.getRecursiveSubspacesForSpace(req.space, (err, subspaces) => {
res.render('space_list', {
subspaces: subspaces.map((s) => {
s.ae_link = config.endpoint + '/s/' + s.edit_hash + (s.edit_slug ? ('-'+s.edit_slug) : '')
return s;
}),
space: req.space
});
});
}
} else {
res.sendStatus(403);
}
} else {
res.sendStatus(403);
}
});
router.get('/pdf', function(req, res, next) {
var s3_filename = make_export_filename(req.space, "pdf");
phantom.takeScreenshot(req.space, "pdf", function(local_path) {
uploader.uploadFile(s3_filename, "application/pdf", local_path, function(err, url) {
res.status(201).json({
url: url
});
fs.unlink(local_path);
});
}, (err) => {
res.status(500).json({
error: "PDF could not created (500)"
});
});
});
router.get('/zip', function(req, res, next) {
Artifact.find({
space_id: req.space._id
}, function(err, artifacts) {
if (!artifacts || !artifacts.length || err) {
res.status(404).json({
"error": "no artifacts"
});
return;
}
var localPath = "/tmp/" + req.space._id;
try {
var files = fs.readdirSync(localPath);
for (var i = 0; i < files.length; i++) {
console.log("[zip export] unlink old file: ", localPath + "/" + files[i]);
if (files[i] != "." && files[i] != "..") {
fs.unlinkSync(localPath + "/" + files[i]);
}
}
fs.rmdirSync(localPath);
} catch (e) {
console.error(e);
}
var used_filenames = {};
fs.mkdir(localPath, function(err, cb) {
async.eachLimit(artifacts, 10, function(artifact, cb) {
try {
if (artifact.payload_uri) {
if (artifact.payload_uri.indexOf("https://") > -1 || artifact.payload_uri.indexOf("http://") > -1) {
var parsed = url.parse(artifact.payload_uri);
var fileName = path.basename(parsed.pathname) || "file.bin";
if (fileName.length > 128) {
fileName = fileName.substr(fileName.length - 128);
}
if (used_filenames[fileName]) {
// if there is a fileName collision, we insert a number before the extension
// to differentiate
var ext = path.extname(fileName);
fileName = path.basename(fileName, ext) + "_" + (parseInt(Math.random() * 10000)) + ext;
}
used_filenames[fileName] = true;
//fix for old artifacts where no .pdf is in the filename
if (artifact.mime == "application/pdf" && fileName.indexOf(".pdf") < 0) {
fileName = fileName + ".pdf";
}
if (artifact.mime == "image/png" && fileName.indexOf(".png") < 0) {
fileName = fileName + ".png";
}
var localFilePath = localPath + '/' + fileName;
request
.get(artifact.payload_uri)
.on('error', function(err) {
console.error(err);
cb(null, artifact.payload_uri);
})
.on('end', function() {
cb(null, artifact.payload_uri);
}).pipe(fs.createWriteStream(localFilePath));
} else {
cb(null, artifact.payload_uri);
}
} else {
cb(null, artifact.payload_uri);
}
} catch (e) {
console.log(e);
cb(null);
}
}, function(err, payloads) {
var outputPath = '/tmp/' + req.space._id + '.zip';
var output = fs.createWriteStream(outputPath);
var archive = archiver('zip');
output.on('close', function() {
var name = make_export_filename(req.space, "zip");
uploader.uploadFile(name, "application/zip", outputPath, function(err, url) {
res.status(201).json({
url: url
});
try {
fs.unlink(outputPath);
} catch (e) {
console.error(e);
}
});
});
archive.on('error', function(err) {
console.error(err);
});
archive.pipe(output);
archive.directory(localPath, false, {
date: new Date()
});
archive.finalize();
});
});
});
});
router.get('/html', function(req, res) {
Artifact.find({
space_id: req.space._id
}, function(err, artifacts) {
var space = req.space;
res.send(space_render.render_space_as_html(space, artifacts));
});
});
router.get('/path', (req, res) => {
// build up a breadcrumb trail (path)
var path = [];
var buildPath = (space) => {
if (space.parent_space_id) {
Space.findOne({
"_id": space.parent_space_id
}, (err, parentSpace) => {
if (space._id == parentSpace._id) {
console.log("error: circular parent reference for space " + space._id);
res.send("error: circular reference");
} else {
path.push(parentSpace);
buildPath(parentSpace);
}
});
} else {
// reached the top
res.json(path.reverse());
}
}
buildPath(req.space);
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
router.get('/', function(req, res, next) {
Membership
.find({
space: req.space._id
})
.populate("user")
.exec(function(err, memberships) {
res.status(200).json(memberships);
});
});
router.post('/', function(req, res, next) {
if (req.spaceRole == "admin") {
var attrs = req.body;
attrs['space'] = req.space._id;
attrs['state'] = "pending";
var membership = new Membership(attrs);
var msg = attrs.personal_message;
if (membership.email_invited != req.user.email) {
User.findOne({
"email": membership.email_invited
}, function(err, user) {
if (user) {
membership.user = user;
membership.state = "active";
} else {
membership.code = crypto.randomBytes(64).toString('hex').substring(0, 12);
}
membership.save(function(err) {
if (err) res.sendStatus(400);
else {
var accept_link = config.endpoint + "/accept/" + membership._id + "?code=" + membership.code;
if (user) {
accept_link = config.endpoint + "/" + req.space.space_type + "s/" + req.space._id;
}
var openText = req.i18n.__("space_invite_membership_action");
if (user) {
req.i18n.__("open");
}
const name = req.user.nickname || req.user.email
const subject = (req.space.space_type == "space") ? req.i18n.__("space_invite_membership_subject", name, req.space.name) : req.i18n.__("folder_invite_membership_subject", req.user.nickname, req.space.name)
const body = (req.space.space_type == "space") ? req.i18n.__("space_invite_membership_body", name, req.space.name) : req.i18n.__("folder_invite_membership_body", req.user.nickname, req.space.name)
mailer.sendMail(
membership.email_invited, subject, body, {
messsage: msg,
action: {
link: accept_link,
name: openText
}
});
res.status(201).json(membership);
}
});
});
} else {
res.status(400).json({
"error": "user already in space"
});
}
} else {
res.status(403).json({
"error": "not_permitted"
});
}
});
router.put('/:membership_id', function(req, res, next) {
if (req.user) {
if (req.spaceRole == "admin") {
Membership.findOne({
_id: req.params.membership_id
}, function(err, mem) {
if (err) res.sendStatus(400);
else {
if (mem) {
var attrs = req.body;
mem.role = attrs.role;
mem.save(function(err) {
if (err) res.sendStatus(400);
else {
res.status(201).json(mem);
}
});
}
}
});
} else {
res.sendStatus(403);
}
} else {
res.sendStatus(403);
}
});
router.delete('/:membership_id', function(req, res, next) {
if (req.user) {
Membership.findOne({
_id: req.params.membership_id
}, function(err, mem) {
if (err) res.sendStatus(400);
else {
mem.remove(function(err) {
if (err) {
res.status(400).json(err);
} else {
// FIXME might need to delete the user?
res.sendStatus(204);
}
});
}
});
} else {
res.sendStatus(403);
}
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
// MESSAGES
router.get('/', function(req, res, next) {
Message.find({
space: req.space._id
}).populate('user', userMapping).exec(function(err, messages) {
res.status(200).json(messages);
});
});
router.post('/', function(req, res, next) {
var attrs = req.body;
attrs.space = req.space;
if (req.user) {
attrs.user = req.user;
} else {
attrs.user = null;
}
var msg = new Message(attrs);
msg.save(function(err) {
if (err) res.status(400).json(erra);
else {
if (msg.message.length <= 1) return;
Membership
.find({
space: req.space,
user: {
"$exists": true
}
})
.populate('user')
.exec(function(err, memberships) {
var users = memberships.map(function(m) {
return m.user;
});
users.forEach((user) => {
if (user.preferences.email_notifications) {
redis.isOnlineInSpace(user, req.space, function(err, online) {
if (!online) {
var nickname = msg.editor_name;
if (req.user) {
nickname = req.user.nickname;
}
mailer.sendMail(
user.email,
req.i18n.__("space_message_subject", req.space.name),
req.i18n.__("space_message_body", nickname, req.space.name), {
message: msg.message,
action: {
link: config.endpoint + "/spaces/" + req.space._id.toString(),
name: req.i18n.__("open")
}
});
} else {
console.log("not sending message to user: is online.");
}
});
} else {
console.log("not sending message to user: is disabled notifications.");
}
});
});
res.distributeCreate("Message", msg);
}
});
});
router.delete('/:message_id', function(req, res, next) {
Message.findOne({
"_id": req.params.message_id
}, function(err, msg) {
if (!msg) {
res.sendStatus(404);
} else {
msg.remove(function(err) {
if (err) res.status(400).json(err);
else {
if (msg) {
res.distributeDelete("Message", msg);
} else {
res.sendStatus(404);
}
}
});
}
});
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var payloadConverter = require('../../helpers/artifact_converter');
var slug = require('slug');
var fs = require('fs');
var async = require('async');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
const exec = require('child_process');
var express = require('express');
var router = express.Router();
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
router.get('/', function(req, res, next) {
if (!req.user) {
res.status(403).json({
error: "auth required"
});
} else {
if (req.query.writablefolders) {
Membership.find({
user: req.user._id
}, (err, memberships) => {
var validMemberships = memberships.filter((m) => {
if (!m.space || (m.space == "undefined"))
return false;
else
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var editorMemberships = validMemberships.filter((m) => {
return (m.role == "editor") || (m.role == "admin")
});
var spaceIds = editorMemberships.map(function(m) {
return new mongoose.Types.ObjectId(m.space);
});
var q = {
"space_type": "folder",
"$or": [{
"creator": req.user._id
}, {
"_id": {
"$in": spaceIds
},
"creator": {
"$ne": req.user._id
}
}]
};
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s.toObject();
return spaceObj;
});
async.map(spaces, (space, cb) => {
Space.getRecursiveSubspacesForSpace(space, (err, spaces) => {
var allSpaces = spaces;
cb(err, allSpaces);
})
}, (err, spaces) => {
var allSpaces = _.flatten(spaces);
var onlyFolders = _.filter(allSpaces, (s) => {
return s.space_type == "folder";
})
var uniqueFolders = _.unique(onlyFolders, (s) => {
return s._id.toString();
})
res.status(200).json(uniqueFolders);
});
});
});
} else if (req.query.search) {
Membership.find({
user: req.user._id
}, function(err, memberships) {
var validMemberships = memberships.filter(function(m) {
if (!m.space || (m.space == "undefined"))
return false;
else
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var spaceIds = validMemberships.map(function(m) {
return new mongoose.Types.ObjectId(m.space);
});
var q = {
"$or": [{"creator": req.user._id},
{"_id": {"$in": spaceIds}},
{"parent_space_id": {"$in": spaceIds}}],
name: new RegExp(req.query.search, "i")
};
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s.toObject();
return spaceObj;
});
res.status(200).json(spaces);
});
});
} else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) {
Space
.findOne({
_id: req.query.parent_space_id
})
.populate('creator', userMapping)
.exec(function(err, space) {
if (space) {
Space.roleInSpace(space, req.user, function(err, role) {
if (role == "none") {
if(space.access_mode == "public") {
role = "viewer";
}
}
if (role != "none") {
Space
.find({
parent_space_id: req.query.parent_space_id
})
.populate('creator', userMapping)
.exec(function(err, spaces) {
res.status(200).json(spaces);
});
} else {
res.status(403).json({"error": "no authorized"});
}
});
} else {
res.status(404).json({"error": "space not found"});
}
});
} else {
Membership.find({
user: req.user._id
}, function(err, memberships) {
var validMemberships = memberships.filter(function(m) {
if (!m.space || (m.space == "undefined"))
return false;
else
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var spaceIds = validMemberships.map(function(m) {
return new mongoose.Types.ObjectId(m.space);
});
var q = {
"$or": [{
"creator": req.user._id,
"parent_space_id": req.user.home_folder_id
}, {
"_id": {
"$in": spaceIds
},
"creator": {
"$ne": req.user._id
}
}]
};
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s.toObject();
return spaceObj;
});
res.status(200).json(spaces);
});
});
}
}
});
router.post('/', function(req, res, next) {
if (req.user) {
var attrs = req.body;
var createSpace = () => {
attrs.creator = req.user;
attrs.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7);
attrs.edit_slug = slug(attrs.name);
var space = new Space(attrs);
space.save(function(err, createdSpace) {
if (err) res.sendStatus(400);
else {
var membership = new Membership({
user: req.user,
space: createdSpace,
role: "admin"
});
membership.save(function(err, createdTeam) {
if (err) {
res.status(400).json(err);
} else {
res.status(201).json(createdSpace);
}
});
}
});
}
if (attrs.parent_space_id) {
Space.findOne({
"_id": attrs.parent_space_id
}).populate('creator', userMapping).exec((err, parentSpace) => {
if (parentSpace) {
Space.roleInSpace(parentSpace, req.user, (err, role) => {
if ((role == "editor") || (role == "admin")) {
createSpace();
} else {
res.status(403).json({
"error": "not editor in parent Space"
});
}
});
} else {
res.status(404).json({
"error": "parent Space not found"
});
}
});
} else {
createSpace();
}
} else {
res.sendStatus(403);
}
});
router.get('/:id', function(req, res, next) {
res.status(200).json(req.space);
});
router.put('/:id', function(req, res) {
var space = req.space;
var newAttr = req.body;
if (req['spaceRole'] != "editor" && req['spaceRole'] != "admin") {
res.sendStatus(403);
return;
}
newAttr.updated_at = new Date();
newAttr.edit_slug = slug(newAttr['name']);
delete newAttr['_id'];
delete newAttr['editor_name'];
delete newAttr['creator'];
Space.findOneAndUpdate({
"_id": space._id
}, {
"$set": newAttr
}, {
"new": true
}, function(err, space) {
if (err) res.status(400).json(err);
else {
res.distributeUpdate("Space", space);
}
});
});
router.post('/:id/background', function(req, res, next) {
var space = req.space;
var newDate = new Date();
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
req.on('end', function() {
uploader.uploadFile("s" + req.space._id + "/bg_" + newDate.getTime() + "_" + fileName, "image/jpeg", localFilePath, function(err, backgroundUrl) {
if (err) res.status(400).json(err);
else {
var adv = space.advanced;
if (adv.background_uri) {
var oldPath = url.parse(req.space.thumbnail_url).pathname;
uploader.removeFile(oldPath, function(err) {
console.log("removed old bg error:", err);
});
}
adv.background_uri = backgroundUrl;
Space.findOneAndUpdate({
"_id": space._id
}, {
"$set": {
advanced: adv
}
}, {
"new": true
}, function(err, space) {
if (err) {
res.sendStatus(400);
} else {
fs.unlink(localFilePath, function(err) {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(200).json(space);
}
});
}
});
}
});
});
});
var handleDuplicateSpaceRequest = function(req, res, parentSpace) {
Space.duplicateSpace(req.space, req.user, 0, (err, newSpace) => {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(201).json(newSpace);
}
}, parentSpace);
}
router.post('/:id/duplicate', (req, res, next) => {
if (req.query.parent_space_id) {
Space.findOne({
_id: req.query.parent_space_id
}).populate('creator', userMapping).exec((err, parentSpace) => {
if (!parentSpace) {
res.status(404).json({
"error": "parent space not found for dupicate"
});
} else {
Space.roleInSpace(parentSpace, req.user, (err, role) => {
if (role == "admin" ||  role == "editor") {
handleDuplicateSpaceRequest(req, res, parentSpace);
} else {
res.status(403).json({
"error": "not authed for parent_space_id"
});
}
});
}
});
} else {
handleDuplicateSpaceRequest(req, res);
}
});
router.delete('/:id', function(req, res, next) {
if (req.user) {
const space = req.space;
if (req.spaceRole == "admin") {
const attrs = req.body;
Space.recursiveDelete(space, function(err) {
if (err) res.status(400).json(err);
else {
res.distributeDelete("Space", space);
}
});
} else {
res.status(403).json({
"error": "requires admin status"
});
}
} else {
res.sendStatus(403);
}
});
router.post('/:id/artifacts-pdf', function(req, res, next) {
if (req.spaceRole == "editor" || req.spaceRole == "admin") {
var withZones = (req.query.zones) ? req.query.zones == "true" : false;
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
req.on('end', function() {
var rawName = fileName.slice(0, fileName.length - 4);
var outputFolder = "/tmp/" + rawName;
var rights = 777;
fs.mkdir(outputFolder, function(db) {
var images = outputFolder + "/" + rawName + "-page-%03d.jpeg";
exec.execFile("gs", ["-sDEVICE=jpeg", "-dDownScaleFactor=4", "-dDOINTERPOLATE", "-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-sOutputFile=" + images, "-r250", "-f", localFilePath], {}, function(error, stdout, stderr) {
if (error === null) {
glob(outputFolder + "/*.jpeg", function(er, files) {
var count = files.length;
var delta = 10;
var limitPerRow = Math.ceil(Math.sqrt(count));
var startX = parseInt(req.query.x, delta);
var startY = parseInt(req.query.y, delta);
async.mapLimit(files, 20, function(localfilePath, cb) {
var fileName = path.basename(localfilePath);
var baseName = path.basename(localfilePath, ".jpeg");
var number = parseInt(baseName.slice(baseName.length - 3, baseName.length), 10);
gm(localFilePath).size(function(err, size) {
var w = 350;
var h = w;
var x = startX + (((number - 1) % limitPerRow) * w);
var y = startY + ((parseInt(((number - 1) / limitPerRow), 10) + 1) * w);
var userId;
if (req.user)
userId = req.user._id;
var a = new Artifact({
mime: "image/jpg",
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: w,
h: h,
x: x,
y: y,
z: (number + (count + 100))
}
});
payloadConverter.convert(a, fileName, localfilePath, (error, artifact) => {
if (error) res.status(400).json(error);
else {
if (withZones) {
var zone = new Artifact({
mime: "x-spacedeck/zone",
description: "Zone " + (number),
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: artifact.board.w + 20,
h: artifact.board.h + 40,
x: x - 10,
y: y - 30,
z: number
},
style: {
order: number,
valign: "middle",
align: "center"
}
});
zone.save((err) => {
redis.sendMessage("create", "Artifact", zone.toJSON(), req.channelId);
cb(null, [artifact, zone]);
});
} else {
cb(null, [artifact]);
}
}
});
});
}, function(err, artifacts) {
exec.execFile("rm", ["-r", outputFolder], function(err) {
res.status(201).json(_.flatten(artifacts));
async.eachLimit(artifacts, 10, (artifact_or_artifacts, cb) => {
if (artifact_or_artifacts instanceof Array) {
_.each(artifact_or_artifacts, (a) => {
redis.sendMessage("create", "Artifact", a.toJSON(), req.channelId);
});
} else  {
redis.sendMessage("create", "Artifact", artifact_or_artifacts.toJSON(), req.channelId);
}
cb(null);
});
});
});
});
} else {
console.error("error:", error);
exec.execFile("rm", ["-r", outputFolder], function(err) {
fs.unlink(localFilePath);
res.status(400).json({});
});
}
});
});
});
} else {
res.status(401).json({
"error": "no access"
});
}
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var fs = require('fs');
var _ = require('underscore');
var crypto = require('crypto');
var bcrypt = require('bcrypt');
var express = require('express');
var router = express.Router();
var userMapping = { '_id': 1, 'nickname': 1, 'email': 1};
router.get('/:id', (req, res) => {
res.status(200).json(req.user.team);
});
router.put('/:id', (req, res) => {
var team = req.user.team;
if (!team) {
res.status(400).json({"error": "user in no team"});
} else {
var newAttr = req.body;
newAttr.updated_at = new Date();
delete newAttr['_id'];
if(newAttr['subdomain']) {
newAttr['subdomain'] = newAttr['subdomain'].toLowerCase();
}
const new_subdomain = newAttr['subdomain'];
var forbidden_subdomains = [];
function updateTeam() {
Team.findOneAndUpdate({"_id": team._id}, {"$set": newAttr}, {"new": true}, (err, team) => {
if (err) res.status(400).json(err);
else {
res.status(200).json(team);
}
});
}
var isForbidden = forbidden_subdomains.indexOf(new_subdomain) > -1;
if (isForbidden) {
res.bad_request("subdomain not valid");
} else {
if (new_subdomain) {
Team.findOne({"domain": new_subdomain}).exec((err, team) => {
if(team) {
res.bad_request("subdomain already used");
} else {
updateTeam()
}
});
} else {
updateTeam()
}
}
}
});
router.get('/:id/memberships', (req, res) => {
User
.find({team: req.user.team})
.populate("team")
.exec(function(err, users){
if (err) res.status(400).json(err);
else {
res.status(200).json(users);
}
});
});
router.post('/:id/memberships', (req, res, next) => {
if (req.body.email) {
const email = req.body.email.toLowerCase();
const team = req.user.team;
User.findOne({"email": email}).populate('team').exec((err, user) => {
if (user) {
const code = crypto.randomBytes(64).toString('hex').substring(0,7);
team.invitation_codes.push(code);
team.save((err) => {
if (err){ res.status(400).json(err); }
else {
mailer.sendMail(email, req.i18n.__("team_invite_membership_subject", team.name), req.i18n.__("team_invite_membership_body", team.name), { action: {
link: config.endpoint + "/teams/" + req.user.team._id + "/join?code=" + code,
name: req.i18n.__("team_invite_membership_action"),
teamname: team.name
}});
res.status(201).json(user);
}
});
} else {
// complete new user
const password = crypto.randomBytes(64).toString('hex').substring(0,7);
const confirmation_token = crypto.randomBytes(64).toString('hex').substring(0,7);
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, (err, hash) => {
crypto.randomBytes(16, (ex, buf) => {
const token = buf.toString('hex');
var u = new User({
email: email,
account_type: "email",
nickname: email,
team: team._id,
password_hash: hash,
payment_plan_key: team.payment_plan_key,
confirmation_token: confirmation_token,
preferences: {
language: req.i18n.locale
}
});
u.save((err) => {
if(err) res.sendStatus(400);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator: u
});
homeSpace.save((err, homeSpace) => {
if (err) res.sendStatus(400);
else {
u.home_folder_id = homeSpace._id;
u.save((err) => {
User.find({"_id": {"$in": team.admins }}).exec((err, admins) => {
admins.forEach((admin) => {
var i18n = req.i18n;
if(admin.preferences && admin.preferences.language){
i18n.setLocale(admin.preferences.language || "en");
}
mailer.sendMail(admin.email, i18n.__("team_invite_membership_subject", team.name), i18n.__("team_invite_admin_body", email, team.name, password), { teamname: team.name });
});
});
mailer.sendMail(email, req.i18n.__("team_invite_membership_subject", team.name), req.i18n.__("team_invite_user_body", team.name, password), { action: {
link: config.endpoint + "/users/byteam/" + req.user.team._id + "/join?confirmation_token=" + confirmation_token,
name: req.i18n.__("team_invite_membership_action")
}, teamname: team.name });
if (err) res.status(400).json(err);
else{
res.status(201).json(u)
}
});
}
});
}
});
});
});
});
}
});
} else {
res.status(400).json({"error": "email missing"});
}
});
router.put('/:id/memberships/:user_id', (req, res) => {
User.findOne({_id: req.params.user_id}, (err,mem) => {
if (err) res.sendStatus(400);
else {
if(user.team._id == req.user.team._id){
user['team'] = req.user.team._id;
user.save((err) => {
res.sendStatus(204);
});
} else {
res.sendStatus(403);
}
}
});
});
router.get('/:id/memberships/:user_id/promote', (req, res) => {
User.findOne({_id: req.params.user_id}, (err,user) => {
if (err) res.sendStatus(400);
else {
if (user.team.toString() == req.user.team._id.toString()) {
var team = req.user.team;
var adminIndex = team.admins.indexOf(user._id);
if (adminIndex == -1) {
team.admins.push(user._id);
team.save((err, team) => {
res.status(204).json(team);
});
} else {
res.status(400).json({"error": "already admin"});
}
} else {
res.status(403).json({"error": "team id not correct"});
}
}
});
});
router.get('/:id/memberships/:user_id/demote', (req, res, next) => {
User.findOne({_id: req.params.user_id}, (err,user) => {
if (err) res.sendStatus(400);
else {
if (user.team.toString() == req.user.team._id.toString()) {
const team = req.user.team;
const adminIndex = team.admins.indexOf(user._id);
if(adminIndex > -1) {
team.admins.splice(adminIndex,1);
team.save((err, team) => {
res.status(204).json(team);
});
} else {
res.sendStatus(404);
}
} else {
res.sendStatus(403);
}
}
});
});
router.delete('/:id/memberships/:user_id', (req, res) => {
User.findOne({_id: req.params.user_id}).populate('team').exec((err,user) => {
if (err) res.sendStatus(400);
else {
const currentUserId = req.user._id.toString();
const team = req.user.team;
const isAdmin = (req.user.team.admins.filter( mem => {
return mem == currentUserId;
}).length == 1)
if (isAdmin) {
user.team = null;
user.payment_plan_key = "free";
user.save( err => {
const adminIndex = team.admins.indexOf(user._id);
if(adminIndex > -1) {
team.admins.splice(adminIndex,1);
team.save((err, team) => {
console.log("admin removed");
});
}
res.sendStatus(204);
});
} else {
res.status(403).json({"error": "not admin"});
}
}
});
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var bcrypt = require('bcrypt');
var crypo = require('crypto');
var swig = require('swig');
var async = require('async');
var _ = require('underscore');
var fs = require('fs');
var request = require('request');
var gm = require('gm');
var validator = require('validator');
var express = require('express');
var router = express.Router();
router.get('/current', function(req, res, next) {
if (req.user) {
console.log(req.user.team);
res.status(200).json(req.user);
} else {
res.status(401).json({"error":"user_not_found"});
}
});
router.post('/', function(req, res) {
if (req.body["email"] && req.body["password"]) {
var email = req.body["email"].toLowerCase();
var nickname = req.body["nickname"];
var password = req.body["password"];
var password_confirmation = req.body["password_confirmation"];
if (password_confirmation == password) {
if (validator.isEmail(email)) {
var createUser = function() {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(password, salt, function(err, hash) {
crypo.randomBytes(16, function(ex, buf) {
var token = buf.toString('hex');
var u = new User({
email: email,
account_type: "email",
nickname: nickname,
password_hash: hash,
preferences: {
language: req.i18n.locale
},
confirmation_token: token
});
u.save(function (err) {
if (err) res.sendStatus(400);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator: u
});
homeSpace.save((err, homeSpace) => {
if (err) res.sendStatus(400);
else {
u.home_folder_id = homeSpace._id;
u.save((err) => {
mailer.sendMail(u.email, req.i18n.__("confirm_subject"), req.i18n.__("confirm_body"), {
action: {
link: config.endpoint + "/confirm/" + u.confirmation_token,
name: req.i18n.__("confirm_action")
}
});
if (err) res.status(400).json(err);
else {
res.status(201).json({});
}
});
}
});
}
});
});
});
});
};
User.find({email: email}, (function (err, users) {
if (err) {
res.status(400).json({"error":"password_confirmation"});
} else {
if (users.length === 0) {
var domain = email.slice(email.lastIndexOf('@')+1);
Domain.findOne({domain: domain}, function(err, domain) {
if(domain){
if(domain.edu) {
createUser();
} else {
res.status(400).json({"error":"domain_blocked"});
}
} else {
createUser();
}
});
} else {
res.status(400).json({"error":"user_email_already_used"});
}
}
}));
} else {
res.status(400).json({"error":"email_invalid"});
}
} else {
res.status(400).json({"error":"password_confirmation"});
}
} else {
res.status(400).json({"error":"email or password missing"});
}
});
router.get('/oauth2callback/url', function(req, res) {
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(
config.google_access,
config.google_secret,
config.endpoint + "/login"
);
var url = oauth2Client.generateAuthUrl({
access_type: 'online',
scope: "email"
});
res.status(200).json({"url":url});
});
router.get('/loginorsignupviagoogle', function(req, res) {
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var plus = google.plus('v1');
var oauth2Client = new OAuth2(
config.google_access,
config.google_secret,
config.endpoint + "/login"
);
var loginUser = function(user, cb) {
crypo.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex');
var session = {
token: token,
created_at: new Date()
};
if(!user.sessions)
user.sessions = [];
user.sessions.push(session);
user.save(function(err, user) {
cb(session);
});
});
};
var code = req.query.code;
oauth2Client.getToken(code, function(err, tokens) {
if (err) res.status(400).json(err);
else {
var apiUrl = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + tokens.access_token;
var finalizeLogin = function(session){
var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging";
res.cookie('sdsession', session.token, { httpOnly: true, secure: secure});
res.status(201).json(session);
};
request.get(apiUrl, function(error, response, body) {
if (error) res.status(400).json(error);
else {
const data = JSON.parse(body);
const email = data.email;
const name = data.name;
User.findOne({email: email}, function (err, user) {
if (user) {
// login new google user
if (user.account_type == "google") {
// just login
loginUser(user, (session) => {
finalizeLogin(session);
});
} else {
res.status(400).json({"error":"user_email_already_used"});
}
} else {
const u = new User({
email: email,
account_type: "google",
nickname: name,
avatar_thumb_uri: body.picture,
preferences: {
language: req.i18n.locale
},
confirmed_at: new Date()
});
u.save(function (err) {
if (err) res.status(400).json(err);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator: u
});
homeSpace.save(function(err, homeSpace) {
if (err) res.status(400).json(err);
else {
u.home_folder_id = homeSpace._id;
u.save(function(err){
if (err) res.sendStatus(400);
else {
mailer.sendMail(u.email, req.i18n.__("welcome_subject"), req.i18n.__("welcome_body"), {});
loginUser(u, function(session) {
finalizeLogin(session);
});
}
});
}
});
}
});
}
});
}
});
}
});
});
router.get('/ ', function(req, res, next) {
if (req.user) {
console.log(req.user.team);
res.status(200).json(req.user);
} else {
res.status(401).json({"error":"user_not_found"});
}
});
router.put('/:id', function(req, res, next) {
var user = req.user;
console.log(req.params.id, user._id);
if (user._id == req.params.id) {
var newAttr = req.body;
newAttr.updated_at = new Date();
delete newAttr['_id'];
User.findOneAndUpdate({"_id": user._id}, {"$set": newAttr}, function(err, updatedUser) {
if (err) {
res.sendStatus(400);
} else {
res.status(200).json(updatedUser);
}
});
} else {
res.sendStatus(403);
}
});
router.post('/:id/password', function(req, res, next) {
var user = req.user;
var old_password = req.body.old_password;
var pass = req.body.new_password;
if (pass.length >= 6) {
if (user._id == req.params.id) {
if (bcrypt.compareSync(old_password, user.password_hash)) {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(pass, salt, function(err, hash) {
user.password_hash = hash;
user.save(function(err){
if(err){
res.status(400).json(err);
}else{
res.sendStatus(204);
}
});
});
});
} else {
res.status(403).json({"error": "old password wrong"});
}
} else {
res.status(403).json({"error": "wrong user"});
}
} else {
res.status(400).json({"error": "password_to_short"});
}
});
router.delete('/:id', (req, res, next) => {
const user = req.user;
if(user._id == req.params.id) {
if (user.account_type == 'email') {
if (bcrypt.compareSync(req.query.password, user.password_hash)) {
user.remove((err) => {
if(err)res.status(400).json(err);
else res.sendStatus(204);
});
} else {
res.bad_request("password_incorrect");
}
} else {
user.remove((err) => {
if(err)res.status(400).json(err);
else res.sendStatus(204);
});
}
}
else res.status(403).json({error: ""});
});
router.put('/:user_id/confirm', (req, res) => {
const token = req.body.token;
const user = req.user;
if (user.confirmation_token === token) {
user.confirmation_token = null;
user.confirmed_at = new Date();
user.save(function(err, updatedUser) {
if(err) {
res.sendStatus(400);
} else {
res.status(200).json(updatedUser);
}
});
} else {
res.sendStatus(400);
}
});
router.post('/:user_id/avatar', (req, res, next) => {
const user = req.user;
const filename = "u"+req.user._id+"_"+(new Date().getTime())+".jpeg"
const localFilePath = "/tmp/"+filename;
const localResizedFilePath = "/tmp/resized_"+filename;
const writeStream = fs.createWriteStream(localFilePath);
const stream = req.pipe(writeStream);
req.on('end', function() {
gm(localFilePath).resize(200, 200).autoOrient().write(localResizedFilePath, (err) => {
if (err) res.status(400).json(err);
else {
uploader.uploadFile(filename, "image/jpeg", localResizedFilePath, (err, url) => {
if (err) res.status(400).json(err);
else {
user.avatar_thumb_uri = url;
user.save((err, updatedUser) => {
if (err) {
res.sendStatus(400);
} else {
fs.unlink(localResizedFilePath, (err) => {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(200).json(updatedUser);
}
});
}
});
}
});
}
});
});
});
router.post('/feedback', function(req, res, next) {
var text = req.body.text;
// FIXME
mailer.sendMail("support@example.org", "Support Request by " + req.user.email, text, {reply_to: req.user.email});
res.sendStatus(201);
});
router.post('/password_reset_requests', (req, res, next) => {
const email = req.query.email;
User.findOne({"email": email}).exec((err, user) => {
if (err) {
res.status(400).json(err);
} else {
if (user) {
if(user.account_type == "email") {
crypo.randomBytes(16, (ex, buf) => {
user.password_reset_token = buf.toString('hex');
user.save((err, updatedUser) => {
if (err) res.status(400).json(err);
else {
mailer.sendMail(email, req.i18n.__("password_reset_subject"), req.i18n.__("password_reset_body"), {action: {
link: config.endpoint + "/password-confirm/" + user.password_reset_token,
name: req.i18n.__("password_reset_action")
}});
res.status(201).json({});
}
});
});
} else {
res.status(404).json({"error": "error_unknown_email"});
}
} else {
res.status(404).json({"error": "error_unknown_email"});
}
}
});
});
router.post('/password_reset_requests/:confirm_token/confirm', function(req, res, next) {
var password = req.body.password;
User
.findOne({"password_reset_token": req.params.confirm_token})
.exec((err, user) => {
if (err) {
res.sendStatus(400);
} else {
if(user) {
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, function(err, hash) {
user.password_hash = hash;
user.password_token = null;
user.save(function(err, updatedUser){
if (err) {
res.sendStatus(400);
} else {
res.sendStatus(201);
}
});
});
});
} else {
res.sendStatus(404);
}
}
});
});
router.post('/:user_id/confirm', function(req, res, next) {
mailer.sendMail(req.user.email, req.i18n.__("confirm_subject"), req.i18n.__("confirm_body"), { action:{
link: config.endpoint + "/confirm/" + req.user.confirmation_token,
name: req.i18n.__("confirm_action")
}});
res.sendStatus(201);
});
module.exports = router;
"use strict";
var config = require('config');
require('../../models/schema');
var fs = require('fs');
var phantom = require('node-phantom-simple');
var md5 = require('md5');
var express = require('express');
var router = express.Router();
function website_to_png(url,on_success,on_error) {
var hash = md5(url);
var export_path = "/tmp/webgrabber-"+hash+".png";
var timeout = 2000;
console.log("[webgrabber] url: "+url);
console.log("[webgrabber] export_path: "+export_path);
var on_success_called = false;
var on_exit = function(exit_code) {
if (exit_code>0) {
console.log("[phantom-webgrabber] abnormal exit for url "+url);
if (!on_success_called && on_error) {
on_error();
}
}
};
fs.stat(export_path, function(err, stat) {
if (!err) {
// file exists
console.log("[webgrabber] serving cached snapshot of url: "+url);
on_success(export_path);
} else {
phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) {
return browser.createPage(function (err, page) {
page.set('settings.resourceTimeout',timeout);
page.set('settings.javascriptEnabled',false);
return page.open(url, function(err, status) {
console.log("[webgrabber] status: "+status);
page.render(export_path, function() {
on_success_called = true;
on_success(export_path);
browser.exit();
});
});
});
}, {
onExit: on_exit
});
}
});
}
router.get('/:id', function (req, res) {
var uri = new Buffer(req.params.id, "base64")+"";
var on_success_called = false;
website_to_png(uri, (image_path) => {
on_success_called = true;
res.sendFile(image_path);
}, () => {
if (!on_success_called) {
res.status(500).send("[webgrabber] Error fetching website.");
}
});
});
module.exports = router;
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment