2 Commits

Author SHA1 Message Date
greenkeeper[bot]
f7df0b7f43 docs(readme): add Greenkeeper badge
https://greenkeeper.io/
2017-04-07 07:55:54 +00:00
greenkeeper[bot]
d11eab4038 chore(package): update dependencies
https://greenkeeper.io/
2017-04-07 07:55:51 +00:00
12 changed files with 58 additions and 172 deletions

View File

@@ -1,5 +1,7 @@
# Spacedeck Open # Spacedeck Open
[![Greenkeeper badge](https://badges.greenkeeper.io/spacedeck/spacedeck-open.svg)](https://greenkeeper.io/)
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). All icons and large parts of the CSS were designed by Thomas Helbig (dergraph).
As we plan to retire the subscription based service at spacedeck.com in late 2017, we decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted version. As we plan to retire the subscription based service at spacedeck.com in late 2017, we decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted version.
@@ -23,16 +25,16 @@ We appreciate filed issues, pull requests and general discussion.
Spacedeck uses the following major building blocks: Spacedeck uses the following major building blocks:
- Node.js 7.x (Backend / API) - Node.js 4.x (Backend / API)
- MongoDB 3.x (Datastore) - MongoDB 3.x (Datastore)
- Redis 3.x (Datastore for realtime channels) - Redis 3.x (Datastore for realtime channels)
- Vue.js (Frontend) - Vue.js (Frontend)
It also has some binary dependencies for media conversion and PDF export: It also has some binary dependencies for media conversion and PDF export:
- imagemagick, graphicsmagick, libav(+codecs, ffmpeg replacement), audiowaveform (https://github.com/bbcrd/audiowaveform), phantomjs (http://phantomjs.org/) - imagemagick
Currently, media files are stored in Amazon S3, so you need an Amazon AWS account and have the ```AWS_ACCESS_KEY_ID``` and ```AWS_SECRET_ACCESS_KEY``` environment variables defined. For sending emails in production, Amazon SES is required. Currently, media files are stored in Amazon S3, so you need an Amazon AWS account and have the ```AWS_ACCESS_KEY_ID``` and ```AWS_SECRET_ACCESS_KEY``` environment variables defined. For sending emails, Amazon SES is required.
To install Spacedeck, you need node.js 4.x and a running MongoDB instance. Then, to install all node dependencies, run To install Spacedeck, you need node.js 4.x and a running MongoDB instance. Then, to install all node dependencies, run
@@ -42,17 +44,11 @@ To rebuild the frontend CSS styles (you need to do this at least once):
gulp styles gulp styles
# Configuration
see: config/config.json
# Run # Run
export NODE_ENV=development export NODE_ENV=development
npm start npm start
open http://localhost:9000
# License # License
Spacedeck Open is released under the GNU Affero General Public License Version 3 (GNU AGPLv3). Spacedeck Open is released under the GNU Affero General Public License Version 3 (GNU AGPLv3).

12
app.js
View File

@@ -15,16 +15,13 @@ const favicon = require('serve-favicon');
const logger = require('morgan'); const logger = require('morgan');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const swig = require('swig'); const swig = require('swig');
const i18n = require('i18n-2'); const i18n = require('i18n-2');
const helmet = require('helmet'); const helmet = require('helmet');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const serveStatic = require('serve-static');
const isProduction = app.get('env') === 'production'; const isProduction = app.get('env') === 'production';
@@ -70,6 +67,7 @@ app.use(bodyParser.urlencoded({
})); }));
app.use(cookieParser()); app.use(cookieParser());
app.use(helmet.noCache())
app.use(helmet.frameguard()) app.use(helmet.frameguard())
app.use(helmet.xssFilter()) app.use(helmet.xssFilter())
app.use(helmet.hsts({ app.use(helmet.hsts({
@@ -111,12 +109,6 @@ app.use('/api/teams', require('./routes/api/teams'));
app.use('/api/webgrabber', require('./routes/api/webgrabber')); app.use('/api/webgrabber', require('./routes/api/webgrabber'));
app.use('/', require('./routes/root')); app.use('/', require('./routes/root'));
if (config.get('storage_local_path')) {
app.use('/storage', serveStatic(config.get('storage_local_path')+"/"+config.get('storage_bucket'), {
maxAge: 24*3600
}));
}
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(require('./middlewares/404')); app.use(require('./middlewares/404'));
if (app.get('env') == 'development') { if (app.get('env') == 'development') {
@@ -136,7 +128,7 @@ mongoose.connect('mongodb://' + mongoHost + '/spacedeck');
const port = 9000; const port = 9000;
const server = http.Server(app).listen(port, () => { const server = http.Server(app).listen(port, () => {
if ("send" in process) { if ("send" in process) {
process.send('online'); process.send('online');
} }

View File

@@ -1,9 +1,7 @@
{ {
"endpoint": "http://localhost:9000", "endpoint": "http://localhost:9000",
"storage_region": "eu-central-1", "storage_bucket": "my_spacedeck_s3_bucket",
"storage_bucket": "my_spacedeck_bucket", "storage_cdn": "xyz.cloudfront.net",
"storage_cdn": "/storage",
"storage_local_path": "./storage",
"google_access" : "", "google_access" : "",
"google_secret" : "", "google_secret" : "",
"admin_pass": "very_secret_admin_password", "admin_pass": "very_secret_admin_password",

View File

@@ -7,7 +7,6 @@ const fs = require('fs');
const Models = require('../models/schema'); const Models = require('../models/schema');
const uploader = require('../helpers/uploader'); const uploader = require('../helpers/uploader');
const path = require('path'); const path = require('path');
const os = require('os');
const fileExtensionMap = { const fileExtensionMap = {
".amr" : "audio/AMR", ".amr" : "audio/AMR",
@@ -246,12 +245,13 @@ function resizeAndUpload(a, size, max, fileName, localFilePath, callback) {
if (max>320 || size.width > max || size.height > max) { if (max>320 || size.width > max || size.height > max) {
var resizedFileName = max + "_"+fileName; var resizedFileName = max + "_"+fileName;
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName; var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName;
var localResizedFilePath = os.tmpdir()+"/"+resizedFileName; var localResizedFilePath = "/tmp/"+resizedFileName;
gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) { gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) {
if(!err) { if(!err) {
uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) { uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) {
if (err) callback(err); if (err) callback(err);
else{ else{
console.log(localResizedFilePath);
fs.unlink(localResizedFilePath, function (err) { fs.unlink(localResizedFilePath, function (err) {
if (err) { if (err) {
console.error(err); console.error(err);

View File

@@ -1,104 +1,14 @@
'use strict'; 'use strict';
// this is a mock version of the Redis API, const RedisConnection = require('ioredis');
// emulating Redis if it is not available locally const websockets = require('./websockets');
var notRedis = {
state: {},
topics: {},
publish: function(topic, msg, cb) {
if (!this.topics[topic]) {
this.topics[topic] = {
subscribers: []
};
}
var t=this.topics[topic];
for (var i=0; i<t.subscribers.length; i++) {
var s=t.subscribers[i];
if (s.handler) {
s.handler(topic, msg);
}
}
if (cb) cb(null);
},
subscribe: function(topics, cb) {
var handle = {
handler: null,
on: function(evt, cb) {
if (evt == "message") {
this.handler = cb;
}
}
};
for (var i=0; i<topics.length; i++) {
var topic = topics[i];
if (!this.topics[topic]) {
this.topics[topic] = {
subscribers: []
};
}
var t=this.topics[topic];
t.subscribers.push(handle);
}
cb(null, topics.length);
return handle;
},
get: function(key, cb) {
cb(null, this.state[key]);
return this.state[key];
},
set: function(key, val, cb) {
this.state[key] = val;
cb();
},
del: function(key, cb) {
delete this.state[key];
cb(null);
},
sadd: function(key, skey, cb) {
if (!this.state[key]) this.state[key] = {};
this.state[key][skey] = true;
cb(null);
},
srem: function(key, skey, cb) {
if (this.state[key]) {
delete this.state[key][skey];
}
cb(null);
},
smembers: function(key, cb) {
cb(null, Object.keys(this.state[key]));
},
incr: function(key, cb) {
if (!this.state[key]) this.state[key] = 0;
this.state[key]++;
cb(null, this.state[key]);
},
expire: function() {
},
}
module.exports = { module.exports = {
connectRedis: function() { connectRedis(){
this.connection = notRedis; const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost';
this.connection = new RedisConnection(6379, redisHost);
}, },
getConnection: function() { sendMessage(action, model, attributes, channelId) {
this.connectRedis();
return this.connection;
},
sendMessage: function(action, model, attributes, channelId) {
const data = JSON.stringify({ const data = JSON.stringify({
channel_id: channelId, channel_id: channelId,
action: action, action: action,
@@ -107,12 +17,12 @@ module.exports = {
}); });
this.connection.publish('updates', data); this.connection.publish('updates', data);
}, },
logIp: function(ip, cb) { logIp(ip, cb) {
this.connection.incr("ip_"+ ip, (err, socketCounter) => { this.connection.incr("ip_"+ ip, (err, socketCounter) => {
cb(); cb();
}); });
}, },
rateLimit: function(namespace, ip, cb) { rateLimit(namespace, ip, cb) {
const key = "limit_"+ namespace + "_"+ ip; const key = "limit_"+ namespace + "_"+ ip;
const redis = this.connection; const redis = this.connection;
@@ -137,7 +47,7 @@ module.exports = {
} }
}); });
}, },
isOnlineInSpace: function(user, space, cb) { isOnlineInSpace(user, space, cb) {
this.connection.smembers("space_" + space._id.toString(), function(err, list) { this.connection.smembers("space_" + space._id.toString(), function(err, list) {
if (err) cb(err); if (err) cb(err);
else { else {
@@ -149,6 +59,3 @@ module.exports = {
}); });
} }
}; };
return module.exports;

View File

@@ -1,21 +1,15 @@
'use strict'; 'use strict';
var AWS = require('aws-sdk');
AWS.config.region = 'eu-central-1';
var fs = require('fs'); var fs = require('fs');
var config = require('config'); var config = require('config');
// use AWS S3 or local folder depending on config
if (config.get("storage_local_path")) {
var AWS = require('mock-aws-s3');
AWS.config.basePath = config.get("storage_local_path");
} else {
var AWS = require('aws-sdk');
AWS.config.region = config.get("storage_region");
}
module.exports = { module.exports = {
removeFile: (path, callback) => { removeFile: (path, callback) => {
const s3 = new AWS.S3({ const s3 = new AWS.S3({
region: config.get("storage_region") region: 'eu-central-1'
}); });
const bucket = config.get("storage_bucket"); const bucket = config.get("storage_bucket");
s3.deleteObject({ s3.deleteObject({
@@ -45,8 +39,9 @@ module.exports = {
} }
}); });
fileStream.on('open', function () { fileStream.on('open', function () {
// FIXME
var s3 = new AWS.S3({ var s3 = new AWS.S3({
region: config.get("storage_region") region: 'eu-central-1'
}); });
s3.putObject({ s3.putObject({
@@ -59,7 +54,7 @@ module.exports = {
console.error(err); console.error(err);
callback(err); callback(err);
}else { }else {
const url = config.get("storage_cdn") + "/" + fileName; const url = "https://"+ config.get("storage_cdn") + "/" + fileName;
console.log("[s3]" + localFilePath + " to " + url); console.log("[s3]" + localFilePath + " to " + url);
callback(null, url); callback(null, url);
} }

View File

@@ -3,19 +3,18 @@ require('../models/schema');
const WebSocketServer = require('ws').Server; const WebSocketServer = require('ws').Server;
const Redis = require('ioredis');
const async = require('async'); const async = require('async');
const _ = require("underscore"); const _ = require("underscore");
const mongoose = require("mongoose"); const mongoose = require("mongoose");
const crypto = require('crypto'); const crypto = require('crypto');
var redis = require("./redis.js");
module.exports = { module.exports = {
startWebsockets: function(server) { startWebsockets: function(server){
this.setupSubscription(); this.setupSubscription();
this.state = redis.getConnection(); this.state = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost');
if(!this.current_websockets) { if(!this.current_websockets){
this.current_websockets = []; this.current_websockets = [];
} }
@@ -118,7 +117,8 @@ module.exports = {
}, },
setupSubscription: function() { setupSubscription: function() {
this.cursorSubscriber = redis.getConnection().subscribe(['cursors', 'users', 'updates'], function (err, count) { this.cursorSubscriber = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost');
this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) {
console.log("[redis] websockets to " + count + " topics." ); console.log("[redis] websockets to " + count + " topics." );
}); });
this.cursorSubscriber.on('message', function (channel, rawMessage) { this.cursorSubscriber.on('message', function (channel, rawMessage) {
@@ -206,7 +206,7 @@ module.exports = {
console.log("websocket not found to remove"); console.log("websocket not found to remove");
} }
this.state.del(ws.id+"", function(err, res) { this.state.del(ws.id, function(err, res) {
if (err) console.error(err, res); if (err) console.error(err, res);
else { else {
this.removeUserInSpace(ws.space_id, ws, (err) => { this.removeUserInSpace(ws.space_id, ws, (err) => {
@@ -221,8 +221,7 @@ module.exports = {
addUserInSpace: function(username, space, ws, cb) { addUserInSpace: function(username, space, ws, cb) {
console.log("[websockets] user "+username+" in "+space.access_mode +" space " + space._id + " with socket " + ws.id); console.log("[websockets] user "+username+" in "+space.access_mode +" space " + space._id + " with socket " + ws.id);
this.state.set(ws.id, username, function(err, res) {
this.state.set(ws.id+"", username+"", function(err, res) {
if(err) console.error(err, res); if(err) console.error(err, res);
else { else {
this.state.sadd("space_" + space._id, ws.id, function(err, res) { this.state.sadd("space_" + space._id, ws.id, function(err, res) {
@@ -239,7 +238,7 @@ module.exports = {
}.bind(this)); }.bind(this));
}, },
removeUserInSpace: function(spaceId, ws, cb) { removeUserInSpace: function(spaceId, ws, cb) {
this.state.srem("space_" + spaceId, ws.id+"", function(err, res) { this.state.srem("space_" + spaceId, ws.id, function(err, res) {
if (err) cb(err); if (err) cb(err);
else { else {
console.log("[websockets] socket "+ ws.id + " went offline in space " + spaceId); console.log("[websockets] socket "+ ws.id + " went offline in space " + spaceId);

View File

@@ -3,7 +3,7 @@
var mongoose = require('mongoose'); var mongoose = require('mongoose');
var Schema = mongoose.Schema; var Schema = mongoose.Schema;
module.exports.artifactSchema = Schema({ module.exports.artifactSchema = mongoose.model('Artifact', {
mime: String, mime: String,
thumbnail_uri: String, thumbnail_uri: String,
space_id: Schema.Types.ObjectId, space_id: Schema.Types.ObjectId,

View File

@@ -6,15 +6,12 @@
"start": "nodemon -e .js,.html bin/www", "start": "nodemon -e .js,.html bin/www",
"test": "mocha" "test": "mocha"
}, },
"engines": {
"node": ">=7.8.0"
},
"dependencies": { "dependencies": {
"archiver": "1.3.0", "archiver": "1.3.0",
"async": "2.3.0", "async": "2.3.0",
"aws-sdk": "2.39.0", "aws-sdk": "2.39.0",
"basic-auth": "1.1.0", "basic-auth": "1.1.0",
"bcryptjs": "2.4.3", "bcrypt": "*",
"body-parser": "~1.17.1", "body-parser": "~1.17.1",
"cheerio": "0.22.0", "cheerio": "0.22.0",
"config": "1.25.1", "config": "1.25.1",
@@ -22,16 +19,17 @@
"csurf": "1.9.0", "csurf": "1.9.0",
"debug": "~2.6.3", "debug": "~2.6.3",
"execSync": "latest", "execSync": "latest",
"express": "~4.13.0", "express": "~4.15.2",
"glob": "7.1.1", "glob": "7.1.1",
"gm": "1.23.0", "gm": "1.23.0",
"googleapis": "18.0.0", "googleapis": "18.0.0",
"gulp": "^3.9.1", "gulp": "^3.9.0",
"gulp-concat": "2.6.0", "gulp-concat": "2.6.1",
"gulp-express": "0.3.0", "gulp-eslint": "*",
"gulp-express": "0.3.5",
"gulp-nodemon": "*", "gulp-nodemon": "*",
"gulp-sass": "^2.0.3", "gulp-sass": "^3.1.0",
"gulp-uglify": "^1.5.1", "gulp-uglify": "^2.1.2",
"gulp-util": "^3.0.6", "gulp-util": "^3.0.6",
"helmet": "^3.5.0", "helmet": "^3.5.0",
"i18n-2": "0.6.3", "i18n-2": "0.6.3",
@@ -39,13 +37,13 @@
"lodash": "^4.3.0", "lodash": "^4.3.0",
"log-timestamp": "latest", "log-timestamp": "latest",
"md5": "2.2.1", "md5": "2.2.1",
"mock-aws-s3": "^2.6.0",
"moment": "2.18.1", "moment": "2.18.1",
"mongoose": "4.9.3", "mongoose": "4.9.3",
"morgan": "1.8.1", "morgan": "1.8.1",
"node-phantom-simple": "2.2.4",
"node-sass-middleware": "0.11.0", "node-sass-middleware": "0.11.0",
"pdfkit": "0.8.0", "pdfkit": "0.8.0",
"validator": "7.0.0",
"node-phantom-simple": "2.2.4",
"phantomjs-prebuilt": "2.1.14", "phantomjs-prebuilt": "2.1.14",
"pm2": "latest", "pm2": "latest",
"qr-image": "3.2.0", "qr-image": "3.2.0",
@@ -53,19 +51,18 @@
"request": "2.81.0", "request": "2.81.0",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"serve-favicon": "~2.4.2", "serve-favicon": "~2.4.2",
"serve-static": "^1.13.1",
"slug": "0.9.1",
"swig": "1.4.2", "swig": "1.4.2",
"slug": "0.9.1",
"underscore": "1.8.3", "underscore": "1.8.3",
"validator": "7.0.0",
"weak": "1.0.1", "weak": "1.0.1",
"ws": "2.2.3" "ws": "2.2.3"
}, },
"devDependencies": { "devDependencies": {
"express": "^4.13.3", "express": "^4.13.3",
"gulp": "^3.9.1", "gulp": "^3.9.0",
"gulp-clean": "^0.3.2", "gulp-clean": "^0.3.2",
"gulp-concat": "^2.6.0", "gulp-concat": "^2.6.0",
"gulp-eslint": "^3.0.1",
"gulp-express": "^0.3.0", "gulp-express": "^0.3.0",
"gulp-fingerprint": "^0.3.2", "gulp-fingerprint": "^0.3.2",
"gulp-nodemon": "^2.0.4", "gulp-nodemon": "^2.0.4",
@@ -74,14 +71,16 @@
"gulp-rev-replace": "^0.4.3", "gulp-rev-replace": "^0.4.3",
"gulp-sass": "^3.1.0", "gulp-sass": "^3.1.0",
"gulp-uglify": "^2.1.2", "gulp-uglify": "^2.1.2",
"nodemon": "1.11.0", "mocha": "*",
"nodemon": "*",
"should": "^11.2.1", "should": "^11.2.1",
"supertest": "^3.0.0", "supertest": "^3.0.0",
"winston": "^2.3.1" "winston": "^2.3.1"
}, },
"description": "", "description": "",
"main": "Gulpfile.js", "main": "Gulpfile.js",
"directories": {}, "directories": {
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/spacedeck/spacedeck-open.git" "url": "https://github.com/spacedeck/spacedeck-open.git"

View File

@@ -3,7 +3,7 @@
var config = require('config'); var config = require('config');
require('../../models/schema'); require('../../models/schema');
var bcrypt = require('bcryptjs'); var bcrypt = require('bcrypt');
var crypo = require('crypto'); var crypo = require('crypto');
var express = require('express'); var express = require('express');

View File

@@ -9,7 +9,7 @@ var mailer = require('../../helpers/mailer');
var fs = require('fs'); var fs = require('fs');
var _ = require('underscore'); var _ = require('underscore');
var crypto = require('crypto'); var crypto = require('crypto');
var bcrypt = require('bcryptjs'); var bcrypt = require('bcrypt');
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();

View File

@@ -6,7 +6,7 @@ require('../../models/schema');
var mailer = require('../../helpers/mailer'); var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader'); var uploader = require('../../helpers/uploader');
var bcrypt = require('bcryptjs'); var bcrypt = require('bcrypt');
var crypo = require('crypto'); var crypo = require('crypto');
var swig = require('swig'); var swig = require('swig');
var async = require('async'); var async = require('async');