16 Commits
v0.9 ... master

Author SHA1 Message Date
mntmn
2fc14e1efe Update README.md 2018-07-25 11:05:35 +02:00
mntmn
79a3d39015 Remove windows prerelease from README
Releases should be tested and built via CI/CD.
2018-07-17 12:21:32 +02:00
Lukas F. Hartmann
628ceb6c3a Merge branch 'master' of https://github.com/spacedeck/spacedeck-open 2018-07-17 12:18:14 +02:00
Lukas F. Hartmann
9889aaa34c Merge branch 'kaleidos-issues/23/deleting-space-user-is-not-possible' 2018-07-17 12:17:56 +02:00
Lukas F. Hartmann
2974caab1d minor cleanup 2018-07-17 12:16:24 +02:00
Hirunatan
ea4f40b628 Implement SMTP email service (#28) 2018-07-17 12:05:27 +02:00
Alejandro Alonso
f71081b15b Adding migration support and delete cascade migration for space references 2018-07-17 08:39:50 +02:00
Lukas F. Hartmann
c075c562d6 fix mp4 upload and video conversion progress display; fixes #21 2018-05-07 20:19:07 +02:00
Lukas F. Hartmann
dc986dcc7e remove traces of team from guest login, fixes #20 2018-05-07 20:00:02 +02:00
Lukas F. Hartmann
fc653e5ce5 fix pdf grid import with zones #19 2018-05-07 19:56:26 +02:00
Lukas F. Hartmann
4c5e6ea286 fix auto layout button 2018-05-03 15:37:59 +02:00
Lukas F. Hartmann
5f4d41f3a4 fix PDF import 2018-05-03 15:35:51 +02:00
Lukas F. Hartmann
fb8d3ac654 Merge branch 'master' of https://github.com/spacedeck/spacedeck-open 2018-05-01 17:06:29 +02:00
Lukas F. Hartmann
c19f00b316 fix session token/cookie handling for arbitrary server IPs; fix realtime update distribution via websockets 2018-05-01 17:04:08 +02:00
mntmn
60ccedd840 Update README.md 2018-05-01 01:25:07 +02:00
mntmn
d2f07a73cf Update README.md 2018-04-14 18:54:16 +02:00
20 changed files with 294 additions and 16140 deletions

View File

@@ -1,10 +1,8 @@
# Spacedeck Open
This is the free and open source version of Spacedeck, a web based, real time, collaborative whiteboard application with rich media support. Spacedeck was developed in 6 major releases during Autumn 2011 until the end of 2016 and was originally a commercial SaaS. The developers were Lukas F. Hartmann (mntmn) and Martin Güther (magegu). All icons and large parts of the CSS were designed by Thomas Helbig (dergraph).
This is the free and open source version of Spacedeck, a web based, real time, collaborative whiteboard application with rich media support. Spacedeck was developed in 6 major releases during Autumn 2011 until the end of 2016 and was originally a commercial SaaS. The developers were Lukas F. Hartmann (mntmn) and Martin Güther (magegu).
As we plan to retire the subscription based service at spacedeck.com in May 2018, we decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted or local version.
Easy to use desktop releases with binaries for Linux, Mac and Windows will be published here soon. In the meantime, you have to install Node.JS.
The spacedeck.com online service was shut down on May 1st 2018. We decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted or local version.
We appreciate filed issues, pull requests and general discussion.
@@ -19,6 +17,12 @@ We appreciate filed issues, pull requests and general discussion.
- Share Spaces on the web or via email
- Export your work as printable PDF or ZIP
# Use Cases
- Education: Virtual classwork with multimedia
- Creative: Mood boards, Brainstorming, Design Thinking
- Visual note taking and planning
# Data Import from Spacedeck.com
Spacedeck Open has a data import feature that you can use to migrate your ZIP export from Spacedeck.com.

View File

@@ -1,8 +1,10 @@
{
//"endpoint": "http://localhost:9000",
"endpoint": "http://localhost:9666",
"storage_region": "eu-central-1",
"team_name": "My Open Spacedeck",
"contact_email": "support@example.org",
"endpoint": "http://localhost:9666",
"storage_region": "eu-central-1",
//"storage_bucket": "sdeck-development",
//"storage_cdn": "http://localhost:9123/sdeck-development",
//"storage_endpoint": "http://storage:9000",
@@ -18,5 +20,14 @@
"google_access" : "",
"google_secret" : "",
"admin_pass": "very_secret_admin_password",
"phantom_api_secret": "very_secret_phantom_password"
"phantom_api_secret": "very_secret_phantom_password",
// Choose "console" or "smtp"
"mail_provider": "smtp",
"mail_smtp_host": "your.smtp.host",
"mail_smtp_port": 465,
"mail_smtp_secure": true,
"mail_smtp_require_tls": true,
"mail_smtp_user": "your.smtp.user",
"mail_smtp_pass": "your.secret.smtp.password"
}

View File

@@ -92,14 +92,14 @@ function createWaveform(fileName, localFilePath, callback){
});
}
function convertVideo(fileName, filePath, codec, callback, progress_callback) {
function convertVideo(fileName, filePath, codec, callback, progressCallback) {
var ext = path.extname(fileName);
var presetMime = mime.lookup(fileName);
var newExt = codec == "mp4" ? "mp4" : "ogv";
var convertedPath = filePath + "." + newExt;
console.log("converting", filePath, "to", convertedPath, "progress_cb:",progress_callback);
console.log("converting", filePath, "to", convertedPath);
var convertArgs = (codec == "mp4") ? [
"-i", filePath,
@@ -134,8 +134,8 @@ function convertVideo(fileName, filePath, codec, callback, progress_callback) {
ff.stderr.on('data', function (data) {
console.log('[ffmpeg-video] stderr: ' + data);
if (progress_callback) {
progress_callback(data);
if (progressCallback) {
progressCallback(data);
}
});
@@ -280,7 +280,7 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
};
module.exports = {
convert: function(a, fileName, localFilePath, payloadCallback, progress_callback) {
convert: function(a, fileName, localFilePath, payloadCallback, progressCallback) {
getMime(fileName, localFilePath, function(err, mimeType){
console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mimeType:", mimeType);
@@ -311,7 +311,7 @@ module.exports = {
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) {
if(err)callback(err);
if (err) payloadCallback(err);
else {
console.log(localFilePath);
var stats = fs.statSync(localFilePath);
@@ -380,7 +380,7 @@ module.exports = {
else callback(null, url);
});
}
}, progress_callback);
}, progressCallback);
}
},
mp4: function(callback) {
@@ -396,7 +396,7 @@ module.exports = {
else callback(null, url);
});
}
}, progress_callback);
}, progressCallback);
}
},
original: function(callback){
@@ -438,9 +438,7 @@ module.exports = {
db.packArtifact(a);
a.updated_at = new Date();
a.save(function(err) {
if (err) payloadCallback(err, null);
else {
a.save().then(function() {
fs.unlink(localFilePath, function (err) {
if (err) {
console.error(err);
@@ -450,7 +448,6 @@ module.exports = {
payloadCallback(null, a);
}
});
}
});
}
});

View File

@@ -1,18 +1,18 @@
'use strict';
var swig = require('swig');
const config = require('config');
const nodemailer = require('nodemailer');
const swig = require('swig');
//var AWS = require('aws-sdk');
module.exports = {
sendMail: (to_email, subject, body, options) => {
if (!options) {
options = {};
}
// FIXME
const teamname = options.teamname || "My Open Spacedeck"
const from = teamname + ' <support@example.org>';
const teamname = options.teamname || config.get('team_name');
const from = teamname + ' <' + config.get('contact_email') + '>';
let reply_to = [from];
if (options.reply_to) {
@@ -29,9 +29,40 @@ module.exports = {
options: options
});
//if (process.env.NODE_ENV === 'development') {
if (config.get('mail_provider') === 'console') {
console.log("Email: to " + to_email + " in production.\nreply_to: " + reply_to + "\nsubject: " + subject + "\nbody: \n" + htmlText + "\n\n plaintext:\n" + plaintext);
/*} else {
} else if (config.get('mail_provider') === 'smtp') {
const transporter = nodemailer.createTransport({
host: config.get('mail_smtp_host'),
port: config.get('mail_smtp_port'),
secure: config.get('mail_smtp_secure'),
requireTLS: config.get('mail_smtp_require_tls'),
auth: {
user: config.get('mail_smtp_user'),
pass: config.get('mail_smtp_pass'),
}
});
transporter.sendMail({
from: from,
replyTo: reply_to,
to: to_email,
subject: subject,
text: plaintext,
html: htmlText,
}, function(err, info) {
if (err) {
console.error("Error sending email:", err);
} else {
console.log("Email sent.");
}
});
} else if (config.get('mail_provider') === 'aws') {
/*
AWS.config.update({region: 'eu-west-1'});
var ses = new AWS.SES();
@@ -56,6 +87,7 @@ module.exports = {
if (err) console.error("Error sending email:", err);
else console.log("Email sent.");
});
}*/
*/
}
}
};

View File

@@ -16,7 +16,8 @@ module.exports = (req, res, next) => {
else db.User.findOne({where: {_id: session.user_id}})
.then(user => {
if (!user) {
res.clearCookie('sdsession');
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : req.headers.hostname;
res.clearCookie('sdsession', { domain: domain });
if (req.accepts("text/html")) {
res.send("Please clear your cookies and try again.");

View File

@@ -1,7 +1,4 @@
//'use strict';
//var mongoose = require('mongoose');
//const sqlite3 = require('sqlite3').verbose();
const Umzug = require('umzug');
function sequel_log(a,b,c) {
console.log(a);
@@ -199,7 +196,7 @@ module.exports = {
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW}
}),
init: function() {
init: async function() {
User = this.User;
Session = this.Session;
Space = this.Space;
@@ -256,7 +253,27 @@ module.exports = {
as: 'space'
});
sequelize.sync();
await sequelize.sync();
var umzug = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: sequelize
},
migrations: {
params: [
sequelize.getQueryInterface(),
Sequelize
],
path: './models/migrations',
pattern: /\.js$/
}
});
umzug.up().then(function(migrations) {
console.log('Migration complete up!');
});
},
getUserRoleInSpace: (originalSpace, user, cb) => {

View File

@@ -0,0 +1,80 @@
'use strict';
module.exports = {
up: function(migration, DataTypes) {
return [
migration.changeColumn('memberships', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
),
migration.changeColumn('artifacts', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
),
migration.changeColumn('messages', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
)
]
},
down: function(migration, DataTypes) {
return [
migration.changeColumn('memberships', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'NO ACTION'
}
),
,
migration.changeColumn('artifacts', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'NO ACTION'
}
),
migration.changeColumn('messages', 'space_id',
{
type: DataTypes.STRING,
references: {
model: 'spaces',
key: '_id'
},
onDelete: 'CASCADE',
onUpdate: 'NO ACTION'
}
)
]
}
};

View File

@@ -30,6 +30,7 @@
"moment": "^2.19.3",
"morgan": "1.8.1",
"node-phantom-simple": "2.2.4",
"nodemailer": "^4.6.7",
"phantomjs-prebuilt": "2.1.14",
"read-chunk": "^2.1.0",
"request": "2.81.0",
@@ -40,6 +41,7 @@
"slug": "0.9.1",
"sqlite3": "^4.0.0",
"swig": "1.4.2",
"umzug": "^2.1.0",
"underscore": "1.8.3",
"uuid": "^3.2.1",
"validator": "7.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -587,7 +587,7 @@ var SpacedeckBoardArtifacts = {
var selected = this.selected_artifacts();
if (selected.length<2) return;
var sorted = _.sortBy(selected, function(a) { return a.x+a.y*this.active_space.advanced.width }.bind(this));
var sorted = _.sortBy(selected, function(a) { return a.x+a.y*this.active_space.width }.bind(this));
var minx = sorted[0].x;
var miny = sorted[0].y;
@@ -596,11 +596,13 @@ var SpacedeckBoardArtifacts = {
var blocks = [];
var padding = 10;
for (var i=0; i<sorted.length; i++) {
var a = sorted[i];
blocks.push({
w: a.w,
h: a.h,
w: a.w+padding,
h: a.h+padding,
a: a
});
}

View File

@@ -252,8 +252,6 @@ var SpacedeckRoutes = {
// #hash
if (event.currentTarget.hash && event.currentTarget.hash.length>1) return;
console.log("clicked", event.currentTarget.pathname);
// external link?
if (event.currentTarget.host != location.host) return;
@@ -269,35 +267,6 @@ var SpacedeckRoutes = {
event.preventDefault();
}.bind(this));
if (location.host!=ENV.webHost) {
if (!subdomainTeam) {
location.href = ENV.webEndpoint;
return;
} else {
if(subdomainTeam.subdomain) {
var realHost = (subdomainTeam.subdomain + "." + ENV.webHost);
if (location.host != realHost) {
location.href = realHost;
return;
}
} else {
location.href = ENV.webEndpoint;
return;
}
}
}
if (this.logged_in) {
if (this.user.team) {
if (this.user.team.subdomain && this.user.team.subdomain.length > 0) {
var realHost = (this.user.team.subdomain + "." + ENV.webHost);
if (location.host != realHost) {
location.href = location.protocol + "//" + realHost + location.pathname;
return;
}
}
}
}
this.internal_route(location.pathname);
},

View File

@@ -71,9 +71,7 @@ var SpacedeckSpaces = {
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));
},
@@ -85,14 +83,7 @@ var SpacedeckSpaces = {
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) {
smoke.prompt(__('what_is_your_name', "Spacedeck") , function(content) {
if (!content || (content.length === 0)) {
this.ask_guestname(dft, cb);
} else {
@@ -172,7 +163,6 @@ var SpacedeckSpaces = {
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));

View File

@@ -48,10 +48,6 @@ SpacedeckUsers = {
},
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){

View File

@@ -101,11 +101,13 @@ SpacedeckWebsockets = {
}
if (this.websocket && this.websocket.readyState==1) {
var token = "";
if (this.user) token = this.user.token;
var auth_params = {
action: "auth",
editor_auth: space_auth,
editor_name: this.guest_nickname,
auth_token: window.socket_auth,
auth_token: token,
space_id: space._id
};
console.log("[websocket] auth space");

View File

@@ -23,7 +23,6 @@ router.post('/', function(req, res) {
db.User.findOne({where: {email: email}})
.error(err => {
res.sendStatus(404);
//res.status(400).json({"error":"session.users"});
})
.then(user => {
if (!user) {
@@ -32,7 +31,6 @@ router.post('/', function(req, res) {
else if (bcrypt.compareSync(password, user.password_hash)) {
crypto.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex');
console.log("!!! token: ",token);
var session = {
user_id: user._id,
@@ -48,7 +46,7 @@ router.post('/', function(req, res) {
res.sendStatus(500);
})
.then(() => {
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : "localhost";
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : req.headers.hostname;
res.cookie('sdsession', token, { domain: domain, httpOnly: true });
res.status(201).json(session);
});
@@ -61,16 +59,14 @@ router.post('/', function(req, res) {
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) {
var domain = new URL(config.get('endpoint')).hostname;
var token = req.cookies['sdsession'];
db.Session.findOne({where: {token: token}})
.then(session => {
session.destroy();
});
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : req.headers.hostname;
res.clearCookie('sdsession', { domain: domain });
res.sendStatus(204);
//});
} else {
res.sendStatus(404);
}

View File

@@ -123,11 +123,11 @@ router.post('/:artifact_id/payload', function(req, res, next) {
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
var progress_callback = function(progress_msg) {
a.description = progress_msg.toString();
var progressCallback = function(progressMsg) {
a.description = progressMsg.toString();
db.packArtifact(a);
a.save();
redis.sendMessage("update", a, a.toJSON(), req.channelId);
redis.sendMessage("update", "Artifact", a, req.channelId);
};
stream.on('finish', function() {
@@ -137,7 +137,7 @@ router.post('/:artifact_id/payload', function(req, res, next) {
db.Space.update({ updated_at: new Date() }, {where: {_id: req.space._id}});
res.distributeUpdate("Artifact", artifact);
}
}, progress_callback);
}, progressCallback);
});
} else {
res.status(401).json({
@@ -165,6 +165,7 @@ router.put('/:artifact_id', function(req, res, next) {
}}).then(rows => {
db.unpackArtifact(newAttr);
db.Space.update({ updated_at: new Date() }, {where: {_id: req.space._id} });
newAttr._id = a._id;
res.distributeUpdate("Artifact", newAttr);
});
});

View File

@@ -240,7 +240,6 @@ router.get('/zip', function(req, res, next) {
});
router.get('/html', function(req, res) {
console.log("!!!!! hello ");
db.Artifact.findAll({where: {
space_id: req.space._id
}}).then(function(artifacts) {

View File

@@ -1,5 +1,6 @@
"use strict";
var config = require('config');
const os = require('os');
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
@@ -422,17 +423,16 @@ router.post('/:id/artifacts-pdf', function(req, res, next) {
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 localFilePath = os.tmpdir() + "/" + 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;
var outputFolder = os.tmpdir() + "/" + rawName;
fs.mkdir(outputFolder, function(db) {
fs.mkdir(outputFolder, function(err) {
var images = outputFolder + "/" + rawName + "-page-%03d.jpeg";
// FIXME not portable
@@ -455,7 +455,7 @@ router.post('/:id/artifacts-pdf', function(req, res, next) {
var number = parseInt(baseName.slice(baseName.length - 3, baseName.length), 10);
gm(localFilePath).size(function(err, size) {
gm(localFilePath).size((err, size) => {
var w = 350;
var h = w;
@@ -463,49 +463,43 @@ router.post('/:id/artifacts-pdf', function(req, res, next) {
var y = startY + ((parseInt(((number - 1) / limitPerRow), 10) + 1) * w);
var userId;
if (req.user)
userId = req.user._id;
if (req.user) userId = req.user._id;
var a = new Artifact({
var a = db.Artifact.create({
_id: uuidv4(),
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))
}
});
}).then(a => {
payloadConverter.convert(a, fileName, localfilePath, (error, artifact) => {
if (error) res.status(400).json(error);
else {
if (withZones) {
var zone = new Artifact({
var zone = {
_id: uuidv4(),
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,
w: artifact.w + 20,
h: artifact.h + 40,
x: x - 10,
y: y - 30,
z: number
},
style: {
z: number,
order: number,
valign: "middle",
align: "center"
}
});
};
zone.save((err) => {
redis.sendMessage("create", "Artifact", zone.toJSON(), req.channelId);
db.Artifact.create(zone).then((z) => {
redis.sendMessage("create", "Artifact", z.toJSON(), req.channelId);
cb(null, [artifact, zone]);
});
@@ -514,6 +508,7 @@ router.post('/:id/artifacts-pdf', function(req, res, next) {
}
}
});
});
});
@@ -527,10 +522,10 @@ router.post('/:id/artifacts-pdf', function(req, res, next) {
if (artifact_or_artifacts instanceof Array) {
_.each(artifact_or_artifacts, (a) => {
redis.sendMessage("create", "Artifact", a.toJSON(), req.channelId);
redis.sendMessage("create", "Artifact", JSON.stringify(a), req.channelId);
});
} else  {
redis.sendMessage("create", "Artifact", artifact_or_artifacts.toJSON(), req.channelId);
redis.sendMessage("create", "Artifact", JSON.stringify(artifact_or_artifacts), req.channelId);
}
cb(null);
});

View File

@@ -26,8 +26,15 @@ var glob = require('glob');
router.get('/current', function(req, res, next) {
if (req.user) {
console.log(req.user.team);
res.status(200).json(req.user);
var u = _.clone(req.user.dataValues);
delete u.password_hash;
delete u.password_reset_token;
delete u.confirmation_token;
u.token = req.cookies['sdsession'];
console.log(u);
res.status(200).json(u);
} else {
res.status(401).json({"error":"user_not_found"});
}
@@ -276,7 +283,7 @@ router.post('/password_reset_requests', (req, res, next) => {
router.post('/password_reset_requests/:confirm_token/confirm', function(req, res, next) {
var password = req.body.password;
User
db.User
.findOne({where: {"password_reset_token": req.params.confirm_token}})
.then((user) => {
if (user) {

View File

@@ -22,29 +22,13 @@
window.browser_lang = '[[locale]]';
window.csrf_token = '[[csrf_token]]';
{% if process.env.NODE_ENV != "production" %}
var ENV = {
name: 'development',
webHost: "localhost:9666",
webEndpoint:"http://localhost:9666",
apiEndpoint: "http://localhost:9666",
websocketsEndpoint: "ws://localhost:9666"
};
{% else %}
var ENV = {
name: 'production',
webHost: location.host,
webEndpoint: location.origin,
apiEndpoint: location.origin,
websocketsEndpoint: location.origin.replace("https:","wss:").replace("http:","ws:")
};
{% endif %}
{% if subdomain_team %}
var subdomainTeam = [[ subdomain_team | json | safe ]];
{% else %}
var subdomainTeam = null;
{% endif %}
</script>
{% if process.env.NODE_ENV == "production" %}