This commit is contained in:
Lukas F. Hartmann 2018-05-01 17:06:29 +02:00
commit fb8d3ac654
16 changed files with 16610 additions and 154 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules node_modules
public/stylesheets/*
javascripts/maps javascripts/maps
javascripts/spacedeck.js javascripts/spacedeck.js
*.swp
*~

View File

@ -1,42 +1,6 @@
var gulp = require('gulp'); var gulp = require('gulp');
var sass = require('gulp-sass'); var sass = require('gulp-sass');
var concat = require('gulp-concat'); var concat = require('gulp-concat');
var server = require('gulp-express');
var nodemon = require('gulp-nodemon');
var revReplace = require("gulp-rev-replace");
var clean = require('gulp-clean');
var child_process = require('child_process');
var path = require('path');
var uglify = require('gulp-uglify');
var fingerprint = require('gulp-fingerprint');
var rev = require('gulp-rev');
var revAll = require('gulp-rev-all');
gulp.task('rev', () => {
return gulp.src(['public/**'])
.pipe(gulp.dest('build/assets'))
.pipe(revAll.revision())
.pipe(gulp.dest('build/assets'))
.pipe(revAll.manifestFile())
.pipe(gulp.dest('build/assets'));
});
gulp.task("all", ["styles", "uglify", "rev", "copylocales"], function(){
var manifest = gulp.src("./build/assets/rev-manifest.json");
return gulp.src("./views/**/*")
.pipe(revReplace({manifest: manifest}))
.pipe(gulp.dest("./build/views"));
});
gulp.task('copylocales', function(){
return gulp.src('./locales/*.js').pipe(gulp.dest('./build/locales'));
});
gulp.task('clean', function () {
return gulp.src('./build').pipe(clean());
});
gulp.task('styles', function() { gulp.task('styles', function() {
gulp.src('styles/**/*.scss') gulp.src('styles/**/*.scss')
@ -47,15 +11,3 @@ gulp.task('styles', function() {
.pipe(concat('style.css')); .pipe(concat('style.css'));
}); });
gulp.task('uglify', () => {
child_process.exec('sed -n \'s/.*script minify src="\\(.*\\)".*/.\\/public\\/\\1/p\' views/spacedeck.html',
function (error, stdout, stderr) {
var scripts = stdout.split('\n').map(function(p){return path.normalize(p)}).filter(function(p){return p!='.'});
console.log("scripts: ",scripts);
gulp.src(scripts)
.pipe(uglify({output:{beautify:true}}))
.pipe(concat('spacedeck.js'))
.pipe(gulp.dest('public/javascripts'));
});
});

View File

@ -1,13 +1,15 @@
# Spacedeck Open # 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. 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.
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.
We appreciate filed issues, pull requests and general discussion. We appreciate filed issues, pull requests and general discussion.
**Windows users:** Try the one-click release at https://github.com/spacedeck/spacedeck-open/releases/tag/v0.9
Desktop releases for Linux and Mac will be published here soon. In the meantime, you have to install Node.JS to run Spacedeck.
# Features # Features
- Create virtual whiteboards called *Spaces* with virtually unlimited size - Create virtual whiteboards called *Spaces* with virtually unlimited size
@ -41,10 +43,6 @@ To install all node dependencies, run (do this once):
npm install npm install
To rebuild the frontend CSS styles (do this at least once, too):
gulp styles
# Configuration # Configuration
See [config/default.json](config/default.json) See [config/default.json](config/default.json)
@ -72,6 +70,12 @@ For advanced media conversion:
By default, media files are uploaded to the ```storage``` folder. By default, media files are uploaded to the ```storage``` folder.
The database is stored in ```database.sqlite``` by default. The database is stored in ```database.sqlite``` by default.
# Hacking
To rebuild the frontend CSS styles:
gulp styles
# License # License
The Spacedeck logo and brand assets are registered trademarks of Spacedeck GmbH. All rights reserved. The Spacedeck logo and brand assets are registered trademarks of Spacedeck GmbH. All rights reserved.

17
electron-windows.md Normal file
View File

@ -0,0 +1,17 @@
# Windows Electron Build
sqlite3 needs to be manually built for the iojs version that electron ships. The following code assumes electron v1.8.4.
````
npm -g install windows-build-tools
cd node_modules\sqlite3
node-gyp configure --module_name=node_sqlite3 --module_path=../lib/binding/electron-v1.8-win32-x64
node-gyp rebuild --target=1.8.4 --target_platform=win32 --dist-url=https://atom.io/download/atom-shell --module_name=node_sqlite3 --module_path=../lib/binding/electron-v1.8-win32-x64 --msvs_version=2015
cd ..\..
````

View File

@ -37,7 +37,7 @@ const convertableAudioTypes = [
"application/ogg", "application/ogg",
"audio/amr", "audio/amr",
"audio/3ga", "audio/3ga",
"audio/wav", "audio/wave",
"audio/3gpp", "audio/3gpp",
"audio/x-wav", "audio/x-wav",
"audio/aiff", "audio/aiff",
@ -263,6 +263,8 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
a.h = Math.round(size.height*factor); a.h = Math.round(size.height*factor);
a.updated_at = new Date(); a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() { a.save().then(function() {
fs.unlink(originalFilePath, function (err) { fs.unlink(originalFilePath, function (err) {
if (err){ if (err){
@ -328,6 +330,8 @@ module.exports = {
a.h = Math.round(size.height*factor); a.h = Math.round(size.height*factor);
a.updated_at = new Date(); a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() { a.save().then(function() {
fs.unlink(localFilePath, function (err) { fs.unlink(localFilePath, function (err) {
if (err){ if (err){

View File

@ -46,7 +46,7 @@
"specify": "Bitte spezifiziere", "specify": "Bitte spezifiziere",
"confirm": "Bitte bestätige", "confirm": "Bitte bestätige",
"signup_google": "Mit Google anmelden", "signup_google": "Mit Google anmelden",
"error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.", "error_unknown_email": "Unbekannte Kombination von Email und Passwort.",
"error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.", "error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.",
"error_domain_blocked": "Diese Domain ist gesperrt.", "error_domain_blocked": "Diese Domain ist gesperrt.",
"error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.", "error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.",

View File

@ -44,8 +44,7 @@
"sure": "Are you sure?", "sure": "Are you sure?",
"specify": "Please Specify", "specify": "Please Specify",
"confirm": "Please Confirm", "confirm": "Please Confirm",
"signup_google": "Sign In with Google", "error_unknown_email": "This email/password combination is unknown.",
"error_unknown_email": "This email/password combination is unknown. Try login with Google.",
"error_password_confirmation": "The entered passwords don't match.", "error_password_confirmation": "The entered passwords don't match.",
"error_domain_blocked": "Your domain is blocked.", "error_domain_blocked": "Your domain is blocked.",
"error_user_email_already_used": "This email address is already in use.", "error_user_email_already_used": "This email address is already in use.",
@ -322,4 +321,4 @@
"mute_present": "Unfollow", "mute_present": "Unfollow",
"follow_present_help": "If someone else is presenting this Space, the other members automatically follow the presentation. Switch following on or off with this button.", "follow_present_help": "If someone else is presenting this Space, the other members automatically follow the presentation. Switch following on or off with this button.",
"export": "export" "export": "export"
} }

View File

@ -46,7 +46,7 @@
"specify": "Veuillez préciser:", "specify": "Veuillez préciser:",
"confirm": "Veuillez confirmer", "confirm": "Veuillez confirmer",
"signup_google": "S'inscrire avec Google", "signup_google": "S'inscrire avec Google",
"error_unknown_email": "Combinaison inconnue de l'email et mot de passe. Ou essayer de signer avec Google.", "error_unknown_email": "Combinaison inconnue de l'email et mot de passe.",
"error_password_confirmation": "Les deux mots de passe ne correspondent pas.", "error_password_confirmation": "Les deux mots de passe ne correspondent pas.",
"error_domain_blocked": "Ce domaine a été désactivé.", "error_domain_blocked": "Ce domaine a été désactivé.",
"error_user_email_already_used": "Cette adresse email est déjà enregistré.", "error_user_email_already_used": "Cette adresse email est déjà enregistré.",
@ -315,4 +315,4 @@
"follow_present": "Suivre", "follow_present": "Suivre",
"mute_present": "Pas suivre", "mute_present": "Pas suivre",
"follow_present_help": "follow_present_help" "follow_present_help": "follow_present_help"
} }

View File

@ -320,26 +320,26 @@ module.exports = {
}, },
unpackArtifact: (a) => { unpackArtifact: (a) => {
if (a.tags) { if (a.tags && (typeof a.tags)=="string") {
a.tags = JSON.parse(a.tags); a.tags = JSON.parse(a.tags);
} }
if (a.control_points) { if (a.control_points && (typeof a.control_points)=="string") {
a.control_points = JSON.parse(a.control_points); a.control_points = JSON.parse(a.control_points);
} }
if (a.payload_alternatives) { if (a.payload_alternatives && (typeof a.payload_alternatives)=="string") {
a.payload_alternatives = JSON.parse(a.payload_alternatives); a.payload_alternatives = JSON.parse(a.payload_alternatives);
} }
return a; return a;
}, },
packArtifact: (a) => { packArtifact: (a) => {
if (a.tags) { if (a.tags && (typeof a.tags)!="string") {
a.tags = JSON.stringify(a.tags); a.tags = JSON.stringify(a.tags);
} }
if (a.control_points) { if (a.control_points && (typeof a.control_points)!="string") {
a.control_points = JSON.stringify(a.control_points); a.control_points = JSON.stringify(a.control_points);
} }
if (a.payload_alternatives) { if (a.payload_alternatives && (typeof a.payload_alternatives)!="string") {
a.payload_alternatives = JSON.stringify(a.payload_alternatives); a.payload_alternatives = JSON.stringify(a.payload_alternatives);
} }
return a; return a;

View File

@ -26,9 +26,9 @@
"helmet": "^3.5.0", "helmet": "^3.5.0",
"i18n-2": "0.6.3", "i18n-2": "0.6.3",
"log-timestamp": "latest", "log-timestamp": "latest",
"morgan": "1.8.1",
"mock-aws-s3": "^2.6.0", "mock-aws-s3": "^2.6.0",
"moment": "^2.19.3", "moment": "^2.19.3",
"morgan": "1.8.1",
"node-phantom-simple": "2.2.4", "node-phantom-simple": "2.2.4",
"phantomjs-prebuilt": "2.1.14", "phantomjs-prebuilt": "2.1.14",
"read-chunk": "^2.1.0", "read-chunk": "^2.1.0",

View File

@ -41,7 +41,7 @@ var SpacedeckBoardArtifacts = {
if ("medium_for_object" in this) { if ("medium_for_object" in this) {
var medium = this.medium_for_object[a._id]; var medium = this.medium_for_object[a._id];
if (medium && a._id != this.editing_artifact_id) { if (medium && a._id != this.editing_artifact_id) {
medium.value(a.description); medium.value(a.description.toString());
} }
} }
}, },
@ -88,10 +88,11 @@ var SpacedeckBoardArtifacts = {
}, },
artifact_is_text_blank: function(a) { artifact_is_text_blank: function(a) {
if(a.description){ if (a.description) {
var filtered = a.description.replace(/<[^>]+>/g,"").replace(/\s/g,""); desc = a.description.toString();
var filtered = desc.replace(/<[^>]+>/g,"").replace(/\s/g,"");
return (filtered.length<1); return (filtered.length<1);
}else{ } else {
return false; return false;
} }
}, },

16525
public/stylesheets/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,10 @@ router.post('/', function(req, res) {
res.sendStatus(404); res.sendStatus(404);
}) })
.then(user => { .then(user => {
if (bcrypt.compareSync(password, user.password_hash)) { if (!user) {
res.sendStatus(404);
}
else if (bcrypt.compareSync(password, user.password_hash)) {
crypto.randomBytes(48, function(ex, buf) { crypto.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex'); var token = buf.toString('hex');

View File

@ -53,15 +53,8 @@ router.get('/', (req, res) => {
space_id: req.space._id space_id: req.space._id
}}).then(artifacts => { }}).then(artifacts => {
async.map(artifacts, (a, cb) => { async.map(artifacts, (a, cb) => {
//a = a.toObject(); TODO db.unpackArtifact(a);
if (a.control_points) {
a.control_points = JSON.parse(a.control_points);
}
if (a.payload_alternatives) {
a.payload_alternatives = JSON.parse(a.payload_alternatives);
}
if (a.user_id) { if (a.user_id) {
// FIXME JOIN // FIXME JOIN
/*User.findOne({where: { /*User.findOne({where: {
@ -131,7 +124,8 @@ router.post('/:artifact_id/payload', function(req, res, next) {
var stream = req.pipe(writeStream); var stream = req.pipe(writeStream);
var progress_callback = function(progress_msg) { var progress_callback = function(progress_msg) {
a.description = progress_msg; a.description = progress_msg.toString();
db.packArtifact(a);
a.save(); a.save();
redis.sendMessage("update", a, JSON.stringify(a), req.channelId); redis.sendMessage("update", a, JSON.stringify(a), req.channelId);
}; };

View File

@ -3,6 +3,7 @@
var config = require('config'); var config = require('config');
const db = require('../../models/db'); const db = require('../../models/db');
const uuidv4 = require('uuid/v4'); const uuidv4 = require('uuid/v4');
const os = require('os');
var mailer = require('../../helpers/mailer'); var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader'); var uploader = require('../../helpers/uploader');
@ -222,8 +223,8 @@ router.post('/:user_id/avatar', (req, res, next) => {
const user = req.user; const user = req.user;
const filename = "u"+req.user._id+"_"+(new Date().getTime())+".jpeg" const filename = "u"+req.user._id+"_"+(new Date().getTime())+".jpeg"
const localFilePath = "/tmp/"+filename; const localFilePath = os.tmpdir()+"/"+filename;
const localResizedFilePath = "/tmp/resized_"+filename; const localResizedFilePath = os.tmpdir()+"/resized_"+filename;
const writeStream = fs.createWriteStream(localFilePath); const writeStream = fs.createWriteStream(localFilePath);
const stream = req.pipe(writeStream); const stream = req.pipe(writeStream);

View File

@ -1,7 +1,6 @@
"use strict"; "use strict";
const config = require('config'); const config = require('config');
require('../models/db');
const redis = require('../helpers/redis'); const redis = require('../helpers/redis');
const express = require('express'); const express = require('express');
@ -10,6 +9,11 @@ const router = express.Router();
const mailer = require('../helpers/mailer'); const mailer = require('../helpers/mailer');
const _ = require('underscore'); const _ = require('underscore');
const db = require('../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.render('index', { title: 'Spaces' }); res.render('index', { title: 'Spaces' });
}); });
@ -120,79 +124,30 @@ router.get('/t/:id', (req, res) => {
}); });
router.get('/s/:token', (req, res) => { router.get('/s/:token', (req, res) => {
redis.rateLimit(req.real_ip, "token", function(ok) { var token = req.params.token;
if (ok) { if (token.split("-").length > 0) {
var token = req.params.token; token = token.split("-")[0];
if (token.split("-").length > 0) { }
token = token.split("-")[0];
}
Space.findOne({"edit_hash": token}).exec(function (err, space) { db.Space.findOne({where: {"edit_hash": token}}).then(function (space) {
if (err) { if (space) {
res.status(404).render('not_found', { title: 'Page Not Found.' }); if (req.accepts('text/html')){
} else { res.redirect("/spaces/"+space._id + "?spaceAuth=" + token);
if (space) { } else {
if(req.accepts('text/html')){ res.status(200).json(space);
res.redirect("/spaces/"+space._id + "?spaceAuth=" + token); }
}else{
res.status(200).json(space);
}
} else {
if(req.accepts('text/html')){
res.status(404).render('not_found', { title: 'Page Not Found.' });
} else {
res.status(404).json({});
}
}
}
});
} else { } else {
res.status(429).json({"error": "Too Many Requests"}); if (req.accepts('text/html')) {
res.status(404).render('not_found', { title: 'Page Not Found.' });
} else {
res.status(404).json({});
}
} }
}); });
}); });
router.get('/spaces/:id', (req, res) => { router.get('/spaces/:id', (req, res) => {
if (req.headers['user-agent']) { res.render('spacedeck', { title: 'Space' });
if (req.headers['user-agent'].match(/facebook/)) {
Space.findOne({"_id": req.params.id }).exec(function (err, space) {
if (err) {
res.status(400).json(err);
} else {
if (space) {
if (space.access_mode == "public") {
Artifact.find({"space_id": req.params.id }).populate("creator").exec(function(err, artifacts) {
space.artifacts = artifacts;
res.render('facebook', { space: space });
});
} else {
res.redirect("/?error=space_not_accessible");
}
} else {
res.render('not_found', { title: 'Spaces' });
}
}
});
} else {
// not facebook, render javascript
res.render('spacedeck', { title: 'Space' });
}
} else res.render('spacedeck', { title: 'Space' });
});
router.get('/qrcode/:id', function(req, res) {
Space.findOne({"_id": req.params.id}).exec(function(err, space) {
if (space) {
const url = config.get("endpoint") + "/s/"+space.edit_hash;
const code = qr.image(url, { type: 'svg' });
res.type('svg');
code.pipe(res);
} else {
res.status(404).json({
"error": "not_found"
});
}
});
}); });
module.exports = router; module.exports = router;