21 Commits

Author SHA1 Message Date
Lukas F. Hartmann
2974caab1d minor cleanup 2018-07-17 12:16:24 +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
mntmn
c5783feca9 fix some artifact serialization trouble; fix login error 2018-04-15 00:23:52 +02:00
mntmn
f396bc2e40 update README 2018-04-14 22:32:28 +02:00
mntmn
44c84028dc electron windows sqlite3 build instrs 2018-04-14 22:29:25 +02:00
mntmn
54755621a1 fix avatar api; fix edit link api 2018-04-14 22:27:38 +02:00
mntmn
30d2995565 package.json sort 2018-04-14 22:25:58 +02:00
mntmn
f5acdcf614 remove junk from gulpfile and ship compiled CSS 2018-04-14 22:24:16 +02:00
Lukas F. Hartmann
f752ec4219 readme tweaks 2018-04-12 20:27:58 +02:00
Lukas F. Hartmann
4caef3b913 update README 2018-04-12 19:56:06 +02:00
Lukas F. Hartmann
f321d67597 tune README 2018-04-12 18:49:45 +02:00
mntmn
ebac854da8 Port Backend to SQLite/Sequelize (removes MongoDB), Support Electron (#14)
* The MongoDB/Mongoose data storage is removed in favor of Sequelize. This abstracts over SQLite or RDBMs like PostgreSQL and MSSQL. The default is SQLite, which significantly simplifies deployments in end-user environments.

* As Spacedeck now has no more mandatory server dependencies, we can wrap it in Electron and ship it as a desktop application.

* Removes docker-compose.yml

* First version of import UI
2018-04-12 16:40:58 +00:00
26 changed files with 16865 additions and 16292 deletions

3
.gitignore vendored
View File

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

View File

@@ -1,42 +1,6 @@
var gulp = require('gulp');
var sass = require('gulp-sass');
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.src('styles/**/*.scss')
@@ -47,15 +11,3 @@ gulp.task('styles', function() {
.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
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.
Data migration features will be added soon.
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.
**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
- Create virtual whiteboards called *Spaces* with virtually unlimited size
@@ -19,32 +21,28 @@ 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
# Data Import from Spacedeck.com
Spacedeck Open has a data import feature that you can use to migrate your ZIP export from Spacedeck.com.
1. Just copy your downloaded ZIP file into the spacedeck root folder. Don't extract it.
2. Start your local Spacedeck.
3. Navigate to *Account / Profile* (person icon in the top right corner).
4. Click the *Import* button next to the ZIP file name. It is on the bottom of the page.
5. Wait until console output has finished and you're done.
# Requirements, Installation
Spacedeck uses the following major building blocks:
Spacedeck requires:
- Node.js 9.x: Web Server / API
- Vue.js: Frontend UI Framework (included)
- SQLite (included)
- Node.js 9.x: Web Server / API. Download: https://nodejs.org
It also has some optional binary dependencies for advanced media conversion:
To run Spacedeck, you only need Node.JS 9.x.
- ffmpeg and ffprobe (for video/audio conversion)
- audiowaveform (for audio waveform rendering) (https://github.com/bbcrd/audiowaveform)
- ghostscript (gs, for PDF import)
By default, media files are uploaded to the ```storage``` folder.
To use Spacedeck, you only need Node.JS 9.x.
Then, to install all node dependencies, run
To install all node dependencies, run (do this once):
npm install
To rebuild the frontend CSS styles (you need to do this at least once):
gulp styles
# Configuration
See [config/default.json](config/default.json)
@@ -59,6 +57,25 @@ Then open http://localhost:9666 in a web browser.
electron .
# Optional Dependencies
For advanced media conversion:
- ffmpeg and ffprobe for video/audio conversion. Download: https://www.ffmpeg.org/download.html
- audiowaveform for audio waveform rendering. Download: https://github.com/bbcrd/audiowaveform
- ghostscript for PDF import. Download: https://www.ghostscript.com/download/gsdnld.html
# Data Storage
By default, media files are uploaded to the ```storage``` folder.
The database is stored in ```database.sqlite``` by default.
# Hacking
To rebuild the frontend CSS styles:
gulp styles
# License
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",
"audio/amr",
"audio/3ga",
"audio/wav",
"audio/wave",
"audio/3gpp",
"audio/x-wav",
"audio/aiff",
@@ -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);
}
});
@@ -263,6 +263,8 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
a.h = Math.round(size.height*factor);
a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() {
fs.unlink(originalFilePath, function (err) {
if (err){
@@ -278,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);
@@ -309,8 +311,8 @@ 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);
else{
if (err) payloadCallback(err);
else {
console.log(localFilePath);
var stats = fs.statSync(localFilePath);
@@ -328,6 +330,8 @@ module.exports = {
a.h = Math.round(size.height*factor);
a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() {
fs.unlink(localFilePath, function (err) {
if (err){
@@ -353,8 +357,8 @@ module.exports = {
thumbnail: function(callback) {
createThumbnailForVideo(fileName, localFilePath, function(err, created){
console.log("thumbnail created: ", err, created);
if(err) callback(err);
else{
if (err) callback(err);
else {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".jpg" ;
uploader.uploadFile(keyName, "image/jpeg", created, function(err, url){
if (err) callback(err);
@@ -376,7 +380,7 @@ module.exports = {
else callback(null, url);
});
}
}, progress_callback);
}, progressCallback);
}
},
mp4: function(callback) {
@@ -392,7 +396,7 @@ module.exports = {
else callback(null, url);
});
}
}, progress_callback);
}, progressCallback);
}
},
original: function(callback){
@@ -400,7 +404,7 @@ module.exports = {
callback(null, url);
});
}
}, function(err, results){
}, function(err, results) {
console.log(err, results);
if (err) payloadCallback(err, a);
@@ -434,19 +438,16 @@ module.exports = {
db.packArtifact(a);
a.updated_at = new Date();
a.save(function(err) {
if (err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + localFilePath);
payloadCallback(null, a);
}
});
}
a.save().then(function() {
fs.unlink(localFilePath, function (err) {
if (err) {
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + localFilePath);
payloadCallback(null, a);
}
});
});
}
});

View File

@@ -46,7 +46,7 @@
"specify": "Bitte spezifiziere",
"confirm": "Bitte bestätige",
"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_domain_blocked": "Diese Domain ist gesperrt.",
"error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.",

View File

@@ -44,8 +44,7 @@
"sure": "Are you sure?",
"specify": "Please Specify",
"confirm": "Please Confirm",
"signup_google": "Sign In with Google",
"error_unknown_email": "This email/password combination is unknown. Try login with Google.",
"error_unknown_email": "This email/password combination is unknown.",
"error_password_confirmation": "The entered passwords don't match.",
"error_domain_blocked": "Your domain is blocked.",
"error_user_email_already_used": "This email address is already in use.",
@@ -322,4 +321,4 @@
"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.",
"export": "export"
}
}

View File

@@ -46,7 +46,7 @@
"specify": "Veuillez préciser:",
"confirm": "Veuillez confirmer",
"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_domain_blocked": "Ce domaine a été désactivé.",
"error_user_email_already_used": "Cette adresse email est déjà enregistré.",
@@ -315,4 +315,4 @@
"follow_present": "Suivre",
"mute_present": "Pas suivre",
"follow_present_help": "follow_present_help"
}
}

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);
@@ -81,13 +78,13 @@ module.exports = {
height: Sequelize.INTEGER,
background_color: Sequelize.STRING,
background_uri: Sequelize.STRING,
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
thumbnail_url: Sequelize.STRING,
thumbnail_updated_at: {type: Sequelize.DATE}
}),
Membership: sequelize.define('membership', {
_id: {type: Sequelize.STRING, primaryKey: true},
space_id: Sequelize.STRING,
@@ -98,7 +95,7 @@ module.exports = {
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW}
}),
Message: sequelize.define('message', {
_id: {type: Sequelize.STRING, primaryKey: true},
space_id: Sequelize.STRING,
@@ -108,7 +105,7 @@ module.exports = {
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW}
}),
Artifact: sequelize.define('artifact', {
_id: {type: Sequelize.STRING, primaryKey: true},
space_id: Sequelize.STRING,
@@ -121,7 +118,7 @@ module.exports = {
last_update_editor_name: Sequelize.STRING,
description: Sequelize.TEXT,
state: {type: Sequelize.STRING, default: "idle"},
//linked_to: Sequelize.STRING,
title: Sequelize.STRING,
tags: Sequelize.TEXT,
@@ -142,16 +139,16 @@ module.exports = {
//}],
control_points: Sequelize.TEXT,
group: Sequelize.STRING,
locked: {type: Sequelize.BOOLEAN, default: false},
payload_uri: Sequelize.STRING,
payload_thumbnail_web_uri: Sequelize.STRING,
payload_thumbnail_medium_uri: Sequelize.STRING,
payload_thumbnail_big_uri: Sequelize.STRING,
payload_size: Sequelize.INTEGER, // file size in bytes
fill_color: {type: Sequelize.STRING, default: "transparent"},
stroke_color: {type: Sequelize.STRING, default: "#000000"},
text_color: Sequelize.STRING,
@@ -176,7 +173,7 @@ module.exports = {
border_radius: Sequelize.INTEGER,
align: {type: Sequelize.STRING, default: "left"},
valign: {type: Sequelize.STRING, default: "top"},
brightness: Sequelize.DECIMAL,
contrast: Sequelize.DECIMAL,
saturation: Sequelize.DECIMAL,
@@ -185,7 +182,7 @@ module.exports = {
opacity: Sequelize.DECIMAL,
payload_alternatives: Sequelize.TEXT,
/*payload_alternatives: [{
mime: String,
payload_uri: String,
@@ -194,12 +191,12 @@ module.exports = {
payload_thumbnail_big_uri: String,
payload_size: Number
}],*/
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW}
}),
init: function() {
init: async function() {
User = this.User;
Session = this.Session;
Space = this.Space;
@@ -220,49 +217,69 @@ module.exports = {
},
as: 'user'
});
Membership.belongsTo(Space, {
foreignKey: {
name: 'space_id'
},
as: 'space'
});
Artifact.belongsTo(User, {
foreignKey: {
name: 'user_id'
},
as: 'user'
});
Artifact.belongsTo(Space, {
foreignKey: {
name: 'space_id'
},
as: 'space'
});
Message.belongsTo(User, {
foreignKey: {
name: 'user_id'
},
as: 'user'
});
Message.belongsTo(Space, {
foreignKey: {
name: 'space_id'
},
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) => {
originalSpace.path = [];
console.log("getUserRoleInSpace",originalSpace._id,user._id,user.home_folder_id);
if (originalSpace._id == user.home_folder_id || (originalSpace.creator_id && originalSpace.creator_id == user._id)) {
cb("admin");
} else {
@@ -320,26 +337,26 @@ module.exports = {
},
unpackArtifact: (a) => {
if (a.tags) {
if (a.tags && (typeof a.tags)=="string") {
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);
}
if (a.payload_alternatives) {
if (a.payload_alternatives && (typeof a.payload_alternatives)=="string") {
a.payload_alternatives = JSON.parse(a.payload_alternatives);
}
return a;
},
packArtifact: (a) => {
if (a.tags) {
if (a.tags && (typeof a.tags)!="string") {
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);
}
if (a.payload_alternatives) {
if (a.payload_alternatives && (typeof a.payload_alternatives)!="string") {
a.payload_alternatives = JSON.stringify(a.payload_alternatives);
}
return a;

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

@@ -26,9 +26,9 @@
"helmet": "^3.5.0",
"i18n-2": "0.6.3",
"log-timestamp": "latest",
"morgan": "1.8.1",
"mock-aws-s3": "^2.6.0",
"moment": "^2.19.3",
"morgan": "1.8.1",
"node-phantom-simple": "2.2.4",
"phantomjs-prebuilt": "2.1.14",
"read-chunk": "^2.1.0",
@@ -40,6 +40,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

@@ -41,7 +41,7 @@ var SpacedeckBoardArtifacts = {
if ("medium_for_object" in this) {
var medium = this.medium_for_object[a._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) {
if(a.description){
var filtered = a.description.replace(/<[^>]+>/g,"").replace(/\s/g,"");
if (a.description) {
desc = a.description.toString();
var filtered = desc.replace(/<[^>]+>/g,"").replace(/\s/g,"");
return (filtered.length<1);
}else{
} else {
return false;
}
},
@@ -586,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;
@@ -595,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 {
@@ -101,7 +92,7 @@ var SpacedeckSpaces = {
if ("localStorage" in window && localStorage) {
try {
localStorage['guest_nickname'] = this.guest_nickname;
}catch(e) {
} catch(e) {
console.error(e);
}
}
@@ -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

@@ -59,13 +59,13 @@ SpacedeckWebsockets = {
else if (msg.action == "delete" && msg.object) {
if (this.active_space) {
var o = msg.object;
if(o._id){
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");
} else console.error("object without _id");
}
}
}
@@ -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");

16525
public/stylesheets/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,15 +23,14 @@ 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 => {
console.log("!!! user: ",user.password_hash);
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) {
var token = buf.toString('hex');
console.log("!!! token: ",token);
var session = {
user_id: user._id,
@@ -47,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);
});
@@ -60,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;
res.clearCookie('sdsession', { domain: domain });
res.sendStatus(204);
//});
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

@@ -53,15 +53,8 @@ router.get('/', (req, res) => {
space_id: req.space._id
}}).then(artifacts => {
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) {
// FIXME JOIN
/*User.findOne({where: {
@@ -130,10 +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;
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() {
@@ -143,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({
@@ -171,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;
fs.mkdir(outputFolder, function(db) {
var outputFolder = os.tmpdir() + "/" + rawName;
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,56 +463,51 @@ 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))
}
});
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,
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 = {
_id: uuidv4(),
mime: "x-spacedeck/zone",
description: "Zone " + (number),
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
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);
cb(null, [artifact, zone]);
});
db.Artifact.create(zone).then((z) => {
redis.sendMessage("create", "Artifact", z.toJSON(), req.channelId);
cb(null, [artifact, zone]);
});
} else {
cb(null, [artifact]);
} else {
cb(null, [artifact]);
}
}
}
});
});
});
@@ -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

@@ -3,6 +3,7 @@
var config = require('config');
const db = require('../../models/db');
const uuidv4 = require('uuid/v4');
const os = require('os');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
@@ -25,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"});
}
@@ -215,8 +223,8 @@ 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 localFilePath = os.tmpdir()+"/"+filename;
const localResizedFilePath = os.tmpdir()+"/resized_"+filename;
const writeStream = fs.createWriteStream(localFilePath);
const stream = req.pipe(writeStream);

View File

@@ -1,7 +1,6 @@
"use strict";
const config = require('config');
require('../models/db');
const redis = require('../helpers/redis');
const express = require('express');
@@ -10,6 +9,11 @@ const router = express.Router();
const mailer = require('../helpers/mailer');
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) => {
res.render('index', { title: 'Spaces' });
});
@@ -120,79 +124,30 @@ router.get('/t/:id', (req, res) => {
});
router.get('/s/:token', (req, res) => {
redis.rateLimit(req.real_ip, "token", function(ok) {
if (ok) {
var token = req.params.token;
if (token.split("-").length > 0) {
token = token.split("-")[0];
}
var token = req.params.token;
if (token.split("-").length > 0) {
token = token.split("-")[0];
}
Space.findOne({"edit_hash": token}).exec(function (err, space) {
if (err) {
res.status(404).render('not_found', { title: 'Page Not Found.' });
} else {
if (space) {
if(req.accepts('text/html')){
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({});
}
}
}
});
db.Space.findOne({where: {"edit_hash": token}}).then(function (space) {
if (space) {
if (req.accepts('text/html')){
res.redirect("/spaces/"+space._id + "?spaceAuth=" + token);
} else {
res.status(200).json(space);
}
} 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) => {
if (req.headers['user-agent']) {
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"
});
}
});
res.render('spacedeck', { title: 'Space' });
});
module.exports = router;

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 %}
var ENV = {
name: 'development',
webHost: location.host,
webEndpoint: location.origin,
apiEndpoint: location.origin,
websocketsEndpoint: location.origin.replace("https:","wss:").replace("http:","ws:")
};
</script>
{% if process.env.NODE_ENV == "production" %}