Compare commits

..

No commits in common. "whiteboard" and "importer" have entirely different histories.

113 changed files with 21036 additions and 19039 deletions

5
.gitignore vendored
View File

@ -1,8 +1,5 @@
node_modules
public/stylesheets/*
javascripts/maps
javascripts/spacedeck.js
public/stylesheets/*.css
database.sqlite
*.swp
*~

View File

@ -1,13 +1,61 @@
const gulp = require('gulp')
const sass = require('gulp-sass')
const concat = require('gulp-concat')
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');
gulp.task('styles', function(done) {
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')
.pipe(sass({
errLogToConsole: true
}))
.pipe(gulp.dest('./public/stylesheets/'))
.pipe(concat('style.css'))
done()
})
.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,82 +1,70 @@
# Spacedeck Open
![Spacedeck 6.0 Screenshot](/public/images/sd6-screenshot.png)
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 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.
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.
[MNT Research GmbH](https://mntre.com) has restarted development of Spacedeck Open in 2020.
Data migration features will be added soon.
We appreciate filed issues, pull requests and general discussion.
# Features
- Create virtual whiteboards called *Spaces* with virtually unlimited size
- Create virtual whiteboards called "Spaces" with virtually unlimited size
- Drag & drop images, videos and audio from your computer or the web
- Write and format text with full control over fonts, colors and style
- Draw, annotate and highlight with included graphical shapes
- Turn your Space into a zooming presentation
- Collaborate in realtime with teammates, students or friends
- Collaborate and chat in realtime with teammates, students or friends
- Share Spaces on the web or via email
- Export your work as printable PDF or ZIP (currently being fixed, stay tuned)
# Use Cases
- Education: Virtual classwork with multimedia
- Creative: Mood boards, Brainstorming, Design Thinking
- Visual note taking and planning
- Export your work as printable PDF or ZIP
# Requirements, Installation
Spacedeck requires:
Spacedeck uses the following major building blocks:
- Node.js 10.x: Web Server / API. Download: https://nodejs.org
- Vue.js (Frontend)
- Node.js 7.x (Backend / API)
- MongoDB 3.x (Datastore)
- Redis 3.x (Datastore for realtime channels)
To run Spacedeck, you only need Node.JS 10.x.
It also has some binary dependencies for media conversion and PDF export:
To install all node dependencies, run (do this once):
- imagemagick, graphicsmagick, libav(+codecs, ffmpeg replacement), audiowaveform (https://github.com/bbcrd/audiowaveform), phantomjs (http://phantomjs.org/)
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.
To install Spacedeck, you need node.js 4.x and a running MongoDB instance. Then, to install all node dependencies, run
npm install
# Configuration
See [config/default.json](config/default.json)
# Run (web server)
node spacedeck.js
Then open http://localhost:9666 in a web browser.
# 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:
To rebuild the frontend CSS styles (you need to do this at least once):
gulp styles
# Configuration
see: config/config.json
# Run
export NODE_ENV=development
npm start
open http://localhost:9666
# experimental docker(compose) support
We have a docker base image at https://github.com/spacedeck/docker-baseimage that includes all required binaries. Based on this image we can use Docker Compose to bootstrap a Spacedeck including data storage.
docker-compose build
docker-compose run -e ENV=development -p 9666:9666 -e NODE_ENV=development spacedeck-open
# License
The Spacedeck logo and brand assets are registered trademarks of Spacedeck GmbH. All rights reserved.
Spacedeck Open source code 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).
Spacedeck Open - Web-based Collaborative Whiteboard For Rich Media
Copyright (C) 2011-2018 Lukas F. Hartmann, Martin GĂĽther
Icons and original CSS design copyright by Thomas Helbig
Copyright (C) 2011-2017 Lukas F. Hartmann, Martin GĂĽther, Thomas Helbig
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as

199
app.js
View File

@ -1,33 +1,180 @@
const spacedeck = require('./spacedeck')
"use strict";
const electron = require('electron')
const electronApp = electron.app
const BrowserWindow = electron.BrowserWindow
let mainWindow
require('./models/schema');
require("log-timestamp");
function createWindow () {
mainWindow = new BrowserWindow({width: 1200, height: 700})
mainWindow.loadURL("http://localhost:9666")
mainWindow.on('closed', function () {
mainWindow = null
})
const config = require('config');
const redis = require('./helpers/redis');
const websockets = require('./helpers/websockets');
const http = require('http');
const path = require('path');
const _ = require('underscore');
const favicon = require('serve-favicon');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const swig = require('swig');
const i18n = require('i18n-2');
const helmet = require('helmet');
const express = require('express');
const app = express();
const serveStatic = require('serve-static');
const isProduction = app.get('env') === 'production';
console.log("Booting Spacedeck Open… (environment: " + app.get('env') + ")");
app.use(logger(isProduction ? 'combined' : 'dev'));
i18n.expressBind(app, {
locales: ["en", "de", "fr"],
defaultLocale: "en",
cookieName: "spacedeck_locale",
devMode: (app.get('env') == 'development')
});
swig.setDefaults({
varControls: ["[[", "]]"] // otherwise it's not compatible with vue.js
});
swig.setFilter('cdn', function(input, idx) {
return input;
});
app.engine('html', swig.renderFile);
app.set('view engine', 'html');
if (isProduction) {
app.set('views', path.join(__dirname, 'build', 'views'));
app.use(favicon(path.join(__dirname, 'build', 'assets', 'images', 'favicon.png')));
app.use(express.static(path.join(__dirname, 'build', 'assets')));
} else {
app.set('views', path.join(__dirname, 'views'));
app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.png')));
app.use(express.static(path.join(__dirname, 'public')));
}
electronApp.on('ready', createWindow)
app.use(bodyParser.json({
limit: '50mb'
}));
// Quit when all windows are closed.
electronApp.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
electronApp.quit()
}
})
app.use(bodyParser.urlencoded({
extended: false,
limit: '50mb'
}));
electronApp.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
app.use(cookieParser());
app.use(helmet.frameguard())
app.use(helmet.xssFilter())
app.use(helmet.hsts({
maxAge: 7776000000,
includeSubdomains: true
}))
app.disable('x-powered-by');
app.use(helmet.noSniff())
// CUSTOM MIDDLEWARES
app.use(require("./middlewares/templates"));
app.use(require("./middlewares/error_helpers"));
app.use(require("./middlewares/setuser"));
app.use(require("./middlewares/cors"));
app.use(require("./middlewares/i18n"));
app.use("/api", require("./middlewares/api_helpers"));
app.use('/api/spaces/:id', require("./middlewares/space_helpers"));
app.use('/api/spaces/:id/artifacts/:artifact_id', require("./middlewares/artifact_helpers"));
app.use('/api/teams', require("./middlewares/team_helpers"));
// REAL ROUTES
app.use('/api/users', require('./routes/api/users'));
app.use('/api/memberships', require('./routes/api/memberships'));
const spaceRouter = require('./routes/api/spaces');
app.use('/api/spaces', spaceRouter);
spaceRouter.use('/:id/artifacts', require('./routes/api/space_artifacts'));
spaceRouter.use('/:id/memberships', require('./routes/api/space_memberships'));
spaceRouter.use('/:id/messages', require('./routes/api/space_messages'));
spaceRouter.use('/:id/digest', require('./routes/api/space_digest'));
spaceRouter.use('/:id', require('./routes/api/space_exports'));
app.use('/api/sessions', require('./routes/api/sessions'));
app.use('/api/teams', require('./routes/api/teams'));
app.use('/api/webgrabber', require('./routes/api/webgrabber'));
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
app.use(require('./middlewares/404'));
if (app.get('env') == 'development') {
app.set('view cache', false);
swig.setDefaults({cache: false});
} else {
app.use(require('./middlewares/500'));
}
module.exports = app;
// CONNECT TO DATABASE
const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || config.get('mongodb_host');
mongoose.connect('mongodb://' + mongoHost + '/spacedeck');
// START WEBSERVER
const port = 9666;
const server = http.Server(app).listen(port, () => {
if ("send" in process) {
process.send('online');
}
})
}).on('listening', () => {
const host = server.address().address;
const port = server.address().port;
console.log('Spacedeck Open listening at http://%s:%s', host, port);
}).on('error', (error) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
//WEBSOCKETS & WORKER
websockets.startWebsockets(server);
redis.connectRedis();
process.on('message', (message) => {
console.log("Process message:", message);
if (message === 'shutdown') {
console.log("Exiting spacedeck.");
process.exit(0);
}
});

0
bin/www Normal file → Executable file
View File

View File

@ -1,11 +1,8 @@
{
"team_name": "My Open Spacedeck",
"contact_email": "support@example.org",
//"endpoint": "http://localhost:9000",
"endpoint": "http://localhost:9666",
"invite_code": "", //disabled invite code by default
"storage_region": "eu-central-1",
//"storage_bucket": "sdeck-development",
//"storage_cdn": "http://localhost:9123/sdeck-development",
//"storage_endpoint": "http://storage:9000",
@ -21,14 +18,5 @@
"google_access" : "",
"google_secret" : "",
"admin_pass": "very_secret_admin_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"
"phantom_api_secret": "very_secret_phantom_password"
}

34
docker-compose.yml Normal file
View File

@ -0,0 +1,34 @@
version: '2'
services:
sync:
image: redis
storage:
image: minio/minio
environment:
- MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
- MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
ports:
- 9123:9000
command: server /export
db:
image: mongo
spacedeck-open:
environment:
- env=development
- MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
- MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
build: .
volumes:
# - ./:/usr/src/app
- /usr/src/app/node_modules
command: npm start
ports:
- 9666:9666
depends_on:
- db
- sync
- storage
links:
- storage
- db
- sync

View File

@ -1,17 +0,0 @@
# 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

@ -4,18 +4,52 @@ const exec = require('child_process');
const gm = require('gm');
const async = require('async');
const fs = require('fs');
const Models = require('../models/db');
const Models = require('../models/schema');
const uploader = require('../helpers/uploader');
const path = require('path');
const os = require('os');
const db = require('../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const mime = require('mime-types');
const fileType = require('file-type');
const readChunk = require('read-chunk');
const fileExtensionMap = {
".amr" : "audio/AMR",
".ogg" : "audio/ogg",
".aac" : "audio/aac",
".mp3" : "audio/mpeg",
".mpg" : "video/mpeg",
".3ga" : "audio/3ga",
".mp4" : "video/mp4",
".wav" : "audio/wav",
".mov" : "video/quicktime",
".doc" : "application/msword",
".dot" : "application/msword",
".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".dotx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
".docm" : "application/vnd.ms-word.document.macroEnabled.12",
".dotm" : "application/vnd.ms-word.template.macroEnabled.12",
".xls" : "application/vnd.ms-excel",
".xlt" : "application/vnd.ms-excel",
".xla" : "application/vnd.ms-excel",
".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xltx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
".xlsm" : "application/vnd.ms-excel.sheet.macroEnabled.12",
".xltm" : "application/vnd.ms-excel.template.macroEnabled.12",
".xlam" : "application/vnd.ms-excel.addin.macroEnabled.12",
".xlsb" : "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
".ppt" : "application/vnd.ms-powerpoint",
".pot" : "application/vnd.ms-powerpoint",
".pps" : "application/vnd.ms-powerpoint",
".ppa" : "application/vnd.ms-powerpoint",
".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".potx" : "application/vnd.openxmlformats-officedocument.presentationml.template",
".ppsx" : "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
".ppam" : "application/vnd.ms-powerpoint.addin.macroEnabled.12",
".pptm" : "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
".potm" : "application/vnd.ms-powerpoint.template.macroEnabled.12",
".ppsm" : "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
".key" : "application/x-iwork-keynote-sffkey",
".pages" : "application/x-iwork-pages-sffpages",
".numbers" : "application/x-iwork-numbers-sffnumbers",
".ttf" : "application/x-font-ttf"
};
const convertableImageTypes = [
"image/png",
@ -35,9 +69,9 @@ const convertableVideoTypes = [
const convertableAudioTypes = [
"application/ogg",
"audio/amr",
"audio/AMR",
"audio/3ga",
"audio/wave",
"audio/wav",
"audio/3gpp",
"audio/x-wav",
"audio/aiff",
@ -92,14 +126,14 @@ function createWaveform(fileName, localFilePath, callback){
});
}
function convertVideo(fileName, filePath, codec, callback, progressCallback) {
function convertVideo(fileName, filePath, codec, callback, progress_callback) {
var ext = path.extname(fileName);
var presetMime = mime.lookup(fileName);
var presetMime = fileExtensionMap[ext];
var newExt = codec == "mp4" ? "mp4" : "ogv";
var convertedPath = filePath + "." + newExt;
console.log("converting", filePath, "to", convertedPath);
console.log("converting", filePath, "to", convertedPath, "progress_cb:",progress_callback);
var convertArgs = (codec == "mp4") ? [
"-i", filePath,
@ -134,8 +168,8 @@ function convertVideo(fileName, filePath, codec, callback, progressCallback) {
ff.stderr.on('data', function (data) {
console.log('[ffmpeg-video] stderr: ' + data);
if (progressCallback) {
progressCallback(data);
if (progress_callback) {
progress_callback(data);
}
});
@ -152,7 +186,7 @@ function convertVideo(fileName, filePath, codec, callback, progressCallback) {
function convertAudio(fileName, filePath, codec, callback) {
var ext = path.extname(fileName);
var presetMime = mime.lookup(fileName);
var presetMime = fileExtensionMap[ext];
var newExt = codec == "mp3" ? "mp3" : "ogg";
var convertedPath = filePath + "." + newExt;
@ -189,14 +223,22 @@ function createThumbnailForVideo(fileName, filePath, callback) {
function getMime(fileName, filePath, callback) {
var ext = path.extname(fileName);
var presetMime = mime.lookup(fileName);
var presetMime = fileExtensionMap[ext];
if (presetMime) {
callback(null, presetMime);
} else {
const buffer = readChunk.sync(filePath, 0, 4100);
var mimeType = fileType(buffer);
callback(null, mimeType);
exec.execFile("file", ["-b","--mime-type", filePath], {}, function(error, stdout, stderr) {
console.log("file stdout: ",stdout);
if (stderr === '' && error == null) {
//filter special chars from commandline
var mime = stdout.replace(/[^a-zA-Z0-9\/\-]/g,'');
callback(null, mime);
} else {
console.log("getMime file error: ", error);
callback(error, null);
}
});
}
}
@ -230,7 +272,7 @@ function resizeAndUpload(a, size, max, fileName, localFilePath, callback) {
}
var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) {
var resizeAndUploadImage = function(a, mime, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) {
async.parallel({
small: function(callback){
resizeAndUpload(a, size, 320, fileName, imageFilePath, callback);
@ -243,13 +285,13 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
},
original: function(callback){
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id + "/" + fileNameOrg;
uploader.uploadFile(s3Key, mimeType, originalFilePath, function(err, url){
uploader.uploadFile(s3Key, mime, originalFilePath, function(err, url){
callback(null, url);
});
}
}, function(err, results) {
a.state = "idle";
a.mime = mimeType;
a.mime = mime;
var stats = fs.statSync(originalFilePath);
a.payload_size = stats["size"];
@ -259,13 +301,15 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
a.payload_uri = results.original;
var factor = 320/size.width;
a.w = Math.round(size.width*factor);
a.h = Math.round(size.height*factor);
var newBoardSpecs = a.board;
newBoardSpecs.w = Math.round(size.width*factor);
newBoardSpecs.h = Math.round(size.height*factor);
a.board = newBoardSpecs;
a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() {
a.save(function(err) {
if(err) payloadCallback(err, null);
else {
fs.unlink(originalFilePath, function (err) {
if (err){
console.error(err);
@ -275,27 +319,28 @@ var resizeAndUploadImage = function(a, mimeType, size, fileName, fileNameOrg, im
payloadCallback(null, a);
}
});
}
});
});
};
module.exports = {
convert: function(a, fileName, localFilePath, payloadCallback, progressCallback) {
getMime(fileName, localFilePath, function(err, mimeType){
console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mimeType:", mimeType);
convert: function(a, fileName, localFilePath, payloadCallback, progress_callback) {
getMime(fileName, localFilePath, function(err, mime){
console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mime:", mime);
if (!err) {
if (convertableImageTypes.indexOf(mimeType) > -1) {
if (convertableImageTypes.indexOf(mime) > -1) {
gm(localFilePath).size(function (err, size) {
console.log("[convert] gm:", err, size);
if (!err) {
if(mimeType == "application/pdf") {
if(mime == "application/pdf") {
var firstImagePath = localFilePath + ".jpeg";
exec.execFile("gs", ["-sDEVICE=jpeg","-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-dFirstPage=1", "-dLastPage=1", "-sOutputFile=" + firstImagePath, "-r90", "-f", localFilePath], {}, function(error, stdout, stderr) {
if(error === null) {
resizeAndUploadImage(a, mimeType, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) {
resizeAndUploadImage(a, mime, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) {
fs.unlink(firstImagePath, function (err) {
payloadCallback(err, a);
});
@ -305,19 +350,19 @@ module.exports = {
}
});
} else if(mimeType == "image/gif") {
} else if(mime == "image/gif") {
//gifs are buggy after convertion, so we should not convert them
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) {
if (err) payloadCallback(err);
else {
if(err)callback(err);
else{
console.log(localFilePath);
var stats = fs.statSync(localFilePath);
a.state = "idle";
a.mime = mimeType;
a.mime = mime;
a.payload_size = stats["size"];
a.payload_thumbnail_web_uri = url;
@ -326,13 +371,15 @@ module.exports = {
a.payload_uri = url;
var factor = 320/size.width;
a.w = Math.round(size.width*factor);
a.h = Math.round(size.height*factor);
var newBoardSpecs = a.board;
newBoardSpecs.w = Math.round(size.width*factor);
newBoardSpecs.h = Math.round(size.height*factor);
a.board = newBoardSpecs;
a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function() {
a.save(function(err){
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
@ -342,23 +389,24 @@ module.exports = {
payloadCallback(null, a);
}
});
}
});
}
});
} else {
resizeAndUploadImage(a, mimeType, size, fileName, fileName, localFilePath, localFilePath, payloadCallback);
resizeAndUploadImage(a, mime, size, fileName, fileName, localFilePath, localFilePath, payloadCallback);
}
} else payloadCallback(err);
});
} else if (convertableVideoTypes.indexOf(mimeType) > -1) {
} else if (convertableVideoTypes.indexOf(mime) > -1) {
async.parallel({
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);
@ -368,7 +416,7 @@ module.exports = {
});
},
ogg: function(callback) {
if (mimeType == "video/ogg") {
if (mime == "video/ogg") {
callback(null, "org");
} else {
convertVideo(fileName, localFilePath, "ogg", function(err, file) {
@ -380,11 +428,11 @@ module.exports = {
else callback(null, url);
});
}
}, progressCallback);
}, progress_callback);
}
},
mp4: function(callback) {
if (mimeType == "video/mp4") {
if (mime == "video/mp4") {
callback(null, "org");
} else {
convertVideo(fileName, localFilePath, "mp4", function(err, file) {
@ -396,21 +444,21 @@ module.exports = {
else callback(null, url);
});
}
}, progressCallback);
}, progress_callback);
}
},
original: function(callback){
uploader.uploadFile(fileName, mimeType, localFilePath, function(err, url){
uploader.uploadFile(fileName, mime, localFilePath, function(err, url){
callback(null, url);
});
}
}, function(err, results) {
}, function(err, results){
console.log(err, results);
if (err) payloadCallback(err, a);
else {
a.state = "idle";
a.mime = mimeType;
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
@ -419,7 +467,7 @@ module.exports = {
a.payload_thumbnail_big_uri = results.thumbnail;
a.payload_uri = results.original;
if (mimeType == "video/mp4") {
if (mime == "video/mp4") {
a.payload_alternatives = [
{
mime: "video/ogg",
@ -435,12 +483,12 @@ module.exports = {
];
}
db.packArtifact(a);
a.updated_at = new Date();
a.save().then(function() {
a.save(function(err) {
if (err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
@ -448,11 +496,12 @@ module.exports = {
payloadCallback(null, a);
}
});
}
});
}
});
} else if (convertableAudioTypes.indexOf(mimeType) > -1) {
} else if (convertableAudioTypes.indexOf(mime) > -1) {
async.parallel({
ogg: function(callback) {
@ -490,7 +539,7 @@ module.exports = {
},
original: function(callback) {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(keyName, mimeType, localFilePath, function(err, url){
uploader.uploadFile(keyName, mime, localFilePath, function(err, url){
callback(null, url);
});
}
@ -501,7 +550,7 @@ module.exports = {
else {
a.state = "idle";
a.mime = mimeType;
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
@ -515,10 +564,9 @@ module.exports = {
];
a.updated_at = new Date();
db.packArtifact(a);
a.save().then(function(){
a.save(function(err){
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
@ -528,27 +576,31 @@ module.exports = {
payloadCallback(null, a);
}
});
}
});
}
});
} else {
console.log("mimeType not matched for conversion, storing file");
console.log("mime not matched for conversion, storing file");
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(keyName, mimeType, localFilePath, function(err, url) {
uploader.uploadFile(keyName, mime, localFilePath, function(err, url) {
a.state = "idle";
a.mime = mimeType;
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
a.payload_uri = url;
a.updated_at = new Date();
a.save().then(function() {
a.save(function(err) {
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
payloadCallback(null, a);
});
}
});
});
}

View File

@ -5,12 +5,7 @@ const config = require('config')
const fs = require('fs')
const path = require('path')
const db = require('../models/db')
const Sequelize = require('sequelize')
const Op = Sequelize.Op
const uuidv4 = require('uuid/v4')
require('../models/db')
require('../models/schema')
module.exports = {
importZIP: function(user, zipPath) {
@ -59,8 +54,8 @@ module.exports = {
let artifacts = JSON.parse(fs.readFileSync(importDir+'/'+space._id+'_artifacts.json'))
console.log('[import] space',space._id,'artifacts:',artifacts.length)
//let q = {where: {_id: space._id}}
space.creator_id = user._id
let q = {_id: space._id}
space.creator = user._id
delete space.__v
// transplant homefolder
@ -69,36 +64,18 @@ module.exports = {
space.parent_space_id = user.home_folder_id
}
// move nested attrs
console.log(space)
for (k in space.advanced) {
space[k] = space.advanced[k]
}
db.Space.create(space)
.error((err) => {
console.error("[import] space upsert err:",err)
Space.findOneAndUpdate(q, space, {upsert: true}, function(err,res) {
if (err) console.log("[import] space upsert err:",err)
})
for (var j=0; j<artifacts.length; j++) {
let a = artifacts[j]
let q = {_id: a._id}
a.user_id = user._id
a.creator = user._id
delete a.__v
delete a.payload_thumbnail_big_uri
// move nested attrs
for (k in a.style) {
a[k] = a.style[k]
}
for (k in a.meta) {
a[k] = a.meta[k]
}
for (k in a.board) {
a[k] = a.board[k]
}
let prefix = "/storage/"+relativeImportDir+"/"+space._id+"_files/"
if (a.thumbnail_uri && a.thumbnail_uri[0]!='/') a.thumbnail_uri = prefix + a.thumbnail_uri
if (a.payload_uri && a.payload_uri[0]!='/') a.payload_uri = prefix + a.payload_uri
@ -115,10 +92,8 @@ module.exports = {
}
}
db.packArtifact(a)
db.Artifact.create(a).error(function(err) {
console.error("[import] artifact upsert err:",err)
Artifact.findOneAndUpdate(q, a, {upsert: true}, function(err,res) {
if (err) console.log("[import] artifact upsert err:",err)
})
}
}

View File

@ -1,18 +1,18 @@
'use strict';
const config = require('config');
const nodemailer = require('nodemailer');
const swig = require('swig');
//var AWS = require('aws-sdk');
var swig = require('swig');
var AWS = require('aws-sdk');
module.exports = {
sendMail: (to_email, subject, body, options) => {
if (!options) {
options = {};
}
const teamname = options.teamname || config.get('team_name');
const from = teamname + ' <' + config.get('contact_email') + '>';
// FIXME
const teamname = options.teamname || "My Open Spacedeck"
const from = teamname + ' <support@example.org>';
let reply_to = [from];
if (options.reply_to) {
@ -29,38 +29,33 @@ module.exports = {
options: options
});
if (config.get('mail_provider') === 'console') {
if (process.env.NODE_ENV === 'development') {
console.log("Email: to " + to_email + " in production.\nreply_to: " + reply_to + "\nsubject: " + subject + "\nbody: \n" + htmlText + "\n\n plaintext:\n" + plaintext);
} 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.");
}
});
AWS.config.update({region: 'eu-west-1'});
var ses = new AWS.SES();
ses.sendEmail( {
Source: from,
Destination: { ToAddresses: [to_email] },
ReplyToAddresses: reply_to,
Message: {
Subject: {
Data: subject
},
Body: {
Text: {
Data: plaintext,
},
Html: {
Data: htmlText
}
}
}
}, function(err, data) {
if (err) console.error("Error sending email:", err);
else console.log("Email sent.");
});
}
}
};

View File

@ -1,9 +1,8 @@
'use strict';
const db = require('../models/db');
const config = require('config');
const phantom = require('node-phantom-simple');
const os = require('os');
require('../models/schema');
var config = require('config');
var phantom = require('node-phantom-simple');
module.exports = {
// type = "pdf" or "png"
@ -11,7 +10,7 @@ module.exports = {
var spaceId = space._id;
var space_url = config.get("endpoint")+"/api/spaces/"+spaceId+"/html";
var export_path = os.tmpdir()+"/"+spaceId+"."+type;
var export_path = "/tmp/"+spaceId+"."+type;
var timeout = 5000;
if (type=="pdf") timeout = 30000;
@ -25,7 +24,7 @@ module.exports = {
var on_exit = function(exit_code) {
if (exit_code>0) {
console.error("phantom abnormal exit for url "+space_url);
console.log("phantom abnormal exit for url "+space_url);
if (!on_success_called && on_error) {
on_error();
}
@ -33,16 +32,16 @@ module.exports = {
};
phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) {
if (err) {
console.error(err);
} else {
if(err){
console.log(err);
}else{
return browser.createPage(function (err, page) {
console.log("page created, opening ",space_url);
if (type=="pdf") {
var psz = {
width: space.width+"px",
height: space.height+"px"
width: space.advanced.width+"px",
height: space.advanced.height+"px"
};
page.set('paperSize', psz);
}

View File

@ -1,16 +1,14 @@
'use strict';
const db = require('../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
require('../models/schema');
const config = require('config');
const WebSocketServer = require('ws').Server;
//const RedisConnection = require('ioredis');
const RedisConnection = require('ioredis');
const async = require('async');
const _ = require("underscore");
const mongoose = require("mongoose");
const crypto = require('crypto');
const redisMock = require("./redis.js");
@ -47,11 +45,11 @@ module.exports = {
const editorAuth = msg.editor_auth;
const spaceId = msg.space_id;
db.Space.findOne({where: {"_id": spaceId}}).then(space => {
Space.findOne({"_id": spaceId}).populate('creator').exec((err, space) => {
if (space) {
const upgradeSocket = function() {
if (token) {
db.findUserBySessionToken(token, function(err, user) {
User.findBySessionToken(token, function(err, user) {
if (err) {
console.error(err, user);
} else {
@ -270,10 +268,10 @@ module.exports = {
},
distributeUsers: function(spaceId) {
if (!spaceId)
if(!spaceId)
return;
/*this.state.smembers("space_" + spaceId, function(err, list) {
this.state.smembers("space_" + spaceId, function(err, list) {
async.map(list, function(item, callback) {
this.state.get(item, function(err, userId) {
console.log(item, "->", userId);
@ -294,14 +292,16 @@ module.exports = {
return {nickname: realNickname, email: null, avatar_thumbnail_uri: null };
});
db.User.findAll({where: {
"_id" : { "$in" : validUserIds }}, attributes: ["nickname","email","avatar_thumbnail_uri"]})
.then(users) {
User.find({"_id" : { "$in" : validUserIds }}, { "nickname" : 1 , "email" : 1, "avatar_thumbnail_uri": 1 }, function(err, users) {
if (err)
console.error(err);
else {
const allUsers = users.concat(anonymousUsers);
const strUsers = JSON.stringify({users: allUsers, space_id: spaceId});
this.state.publish("users", strUsers);
}
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));*/
}
};

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.",
"error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.",
"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,7 +44,8 @@
"sure": "Are you sure?",
"specify": "Please Specify",
"confirm": "Please Confirm",
"error_unknown_email": "This email/password combination is unknown.",
"signup_google": "Sign In with Google",
"error_unknown_email": "This email/password combination is unknown. Try login with Google.",
"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.",

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.",
"error_unknown_email": "Combinaison inconnue de l'email et mot de passe. Ou essayer de signer avec Google.",
"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é.",

View File

@ -1,6 +1,6 @@
'use strict';
require('../models/db');
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {

View File

@ -1,11 +1,9 @@
'use strict';
require('../models/db');
require('../models/schema');
var config = require('config');
const redis = require('../helpers/redis');
// FIXME TODO object.toJSON()
var saveAction = (actionKey, object) => {
if (object.constructor.modelName == "Space")
return;
@ -15,14 +13,14 @@ var saveAction = (actionKey, object) => {
space: object.space_id || object.space,
user: object.user_id || object.user,
editor_name: object.editor_name,
object: object
object: object.toJSON()
};
/*let action = new Action(attr);
let action = new Action(attr);
action.save(function(err) {
if (err)
console.error("saved create action err:", err);
});*/
});
};
module.exports = (req, res, next) => {
@ -34,21 +32,21 @@ module.exports = (req, res, next) => {
res['distributeCreate'] = function(model, object) {
if (!object) return;
redis.sendMessage("create", model, object, req.channelId);
this.status(201).json(object);
redis.sendMessage("create", model, object.toJSON(), req.channelId);
this.status(201).json(object.toJSON());
saveAction("create", object);
};
res['distributeUpdate'] = function(model, object) {
if (!object) return;
redis.sendMessage("update", model, object, req.channelId);
this.status(200).json(object);
redis.sendMessage("update", model, object.toJSON(), req.channelId);
this.status(200).json(object.toJSON());
saveAction("update", object);
};
res['distributeDelete'] = function(model, object) {
if (!object) return;
redis.sendMessage("delete", model, object, req.channelId);
redis.sendMessage("delete", model, object.toJSON(), req.channelId);
this.sendStatus(204);
saveAction("delete", object);
};

View File

@ -1,20 +1,22 @@
'use strict';
const db = require('../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
var artifactId = req.params.artifact_id;
db.Artifact.findOne({where: {
Artifact.findOne({
"_id": artifactId
}}).then(artifact => {
}, (err, artifact) => {
if (err) {
res.status(400).json(err);
} else {
if (artifact) {
req['artifact'] = artifact;
next();
} else {
res.sendStatus(404);
}
}
});
};

View File

@ -1,6 +1,6 @@
'use strict';
require('../models/db');
require('../models/schema');
const config = require('config');
const url = require('url');
@ -33,13 +33,13 @@ module.exports = (req, res, next) => {
respond(origin, req, res, next);
} else {
//Team.getTeamForHost(parsedUrl.hostname, (err, team, subdomain) => {
//if (team) {
Team.getTeamForHost(parsedUrl.hostname, (err, team, subdomain) => {
if (team) {
respond(origin, req, res, next);
//} else {
} else {
next();
//}
//});
}
});
}
} else {

View File

@ -1,6 +1,6 @@
'use strict';
require('../models/db');
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
@ -10,8 +10,8 @@ module.exports = (req, res, next) => {
req.i18n.setLocaleFromCookie();
}
if (req.user && req.user.prefs_language) {
req.i18n.setLocale(req.user.prefs_language);
if (req.user && req.user.preferences.language) {
req.i18n.setLocale(req.user.preferences.language);
}
next();
}

View File

@ -1,47 +0,0 @@
'use strict';
const db = require('../models/db');
var config = require('config');
module.exports = (req, res, next) => {
const token = req.cookies["sdsession"];
if (token && token != "null" && token != null) {
db.Session.findOne({where: {token: token}})
.then(session => {
if (!session) {
// session not found
next();
}
else db.User.findOne({where: {_id: session.user_id}})
.then(user => {
if (!user) {
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.");
} else if (req.accepts('application/json')) {
res.status(403).json({
"error": "token_not_found"
});
} else {
res.send("Please clear your cookies and try again.");
}
} else {
req["token"] = token;
req["user"] = user;
next();
}
});
})
.error(err => {
console.error("Session resolve error",err);
next();
});
} else {
next();
}
}

35
middlewares/setuser.js Normal file
View File

@ -0,0 +1,35 @@
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
const token = req.cookies["sdsession"];
if (token && token != "null" && token !== null) {
User.findOne({
"sessions.token": token
}).populate('team').exec((err, user) => {
if (err) console.error("session.token lookup error:",err);
if (!user) {
res.clearCookie('sdsession');
if (req.accepts("text/html")) {
res.send("Please clear your cookies and try again.");
} else if (req.accepts('application/json')) {
res.status(403).json({
"error": "token_not_found"
});
} else {
res.send("Please clear your cookies and try again.");
}
} else {
req["token"] = token;
req["user"] = user;
next();
}
});
} else {
next();
}
}

View File

@ -1,6 +1,6 @@
'use strict';
const db = require('../models/db');
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
@ -19,6 +19,50 @@ module.exports = (req, res, next) => {
}
};
var rolePerUser = (originalSpace, user, cb) => {
originalSpace.path = [];
if (originalSpace._id.equals(req.user.home_folder_id) || (originalSpace.creator && originalSpace.creator._id.equals(req.user._id))) {
cb("admin");
} else {
var findMembershipsForSpace = function(space, allMemberships, prevRole) {
Membership.find({
"space": space._id
}, function(err, parentMemberships) {
var currentMemberships = parentMemberships.concat(allMemberships);
if (space.parent_space_id) {
Space.findOne({
"_id": space.parent_space_id
}, function(err, parentSpace) {
findMembershipsForSpace(parentSpace, currentMemberships, prevRole);
});
} else {
// reached the top
var role = prevRole;
space.memberships = currentMemberships;
if(role == "none"){
if(originalSpace.access_mode == "public") {
role = "viewer";
}
}
currentMemberships.forEach(function(m, i) {
if (m.user && m.user.equals(user._id)) {
role = m.role;
}
});
cb(role);
}
});
};
findMembershipsForSpace(originalSpace, [], "none");
}
};
var finalizeAnonymousLogin = function(space, spaceAuth) {
var role = "none";
@ -33,7 +77,7 @@ module.exports = (req, res, next) => {
}
if (req.user) {
db.getUserRoleInSpace(space, req.user, function(newRole) {
rolePerUser(space, req.user, function(newRole) {
if (newRole == "admin" && (role == "editor" || role == "viewer")) {
finalizeReq(space, newRole);
} else if (newRole == "editor" && (role == "viewer")) {
@ -53,12 +97,16 @@ module.exports = (req, res, next) => {
'email': 1
};
db.Space.findOne({where: {
Space.findOne({
"_id": spaceId
}}).then(function(space) {
}).populate("creator", userMapping).exec(function(err, space) {
if (err) {
res.status(400).json(err);
} else {
if (space) {
if (space.access_mode == "public") {
if (space.password) {
if (req.spacePassword) {
if (req.spacePassword === space.password) {
@ -78,8 +126,6 @@ module.exports = (req, res, next) => {
}
} else {
// space is private
// special permission for screenshot/pdf export from backend
if (req.query['api_token'] && req.query['api_token'] == config.get('phantom_api_secret')) {
finalizeReq(space, "viewer");
@ -87,7 +133,7 @@ module.exports = (req, res, next) => {
}
if (req.user) {
db.getUserRoleInSpace(space, req.user, function(role) {
rolePerUser(space, req.user, function(role) {
if (role == "none") {
finalizeAnonymousLogin(space, req["spaceAuth"]);
} else {
@ -109,5 +155,6 @@ module.exports = (req, res, next) => {
"error": "space_not_found"
});
}
}
});
}

33
middlewares/subdomain.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
let host = req.headers.host;
Team.getTeamForHost(host, (err, team, subdomain) => {
if (subdomain) {
if (!err && team) {
req.subdomainTeam = team;
req.subdomain = subdomain;
next()
} else {
if (req.accepts('text/html')) {
res.status(404).render('not_found', {
title: 'Page Not Found.'
});
} else if (req.accepts('application/json')) {
res.status(404).json({
"error": "not_found"
});
} else {
res.status(404).render('not_found', {
title: 'Page Not Found.'
});
}
}
} else {
next();
}
});
}

View File

@ -0,0 +1,23 @@
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
if (req.user) {
var isAdmin = req.user.team.admins.indexOf(req.user._id) >= 0;
var correctMethod = req.method == "GET" || (req.method == "DELETE" || req.method == "PUT" || req.method == "POST");
if (correctMethod && isAdmin) {
next();
} else {
res.status(403, {
"error": "not authorized"
});
}
} else {
res.status(403, {
"error": "not logged in"
});
}
}

31
middlewares/templates.js Normal file
View File

@ -0,0 +1,31 @@
'use strict';
require('../models/schema');
var config = require('config');
var _ = require('underscore');
module.exports = (req, res, next) => {
res.oldRender = res.render;
res.render = function(template, params) {
var team = req.subdomainTeam;
if (team) {
team = _.pick(team.toObject(), ['_id', 'name', 'subdomain', 'avatar_original_uri']);
} else {
team = null;
}
const addParams = {
locale: req.i18n.locale,
config: config,
subdomain_team: team,
user: req.user,
csrf_token: "",
socket_auth: req.token
};
const all = _.extend(params, addParams);
res.oldRender(template, all);
};
next();
}

View File

@ -1,7 +1,5 @@
'use strict';
// FIXME port this last model
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

88
models/artifact.js Normal file
View File

@ -0,0 +1,88 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.artifactSchema = Schema({
mime: String,
thumbnail_uri: String,
space_id: Schema.Types.ObjectId,
user_id: {type: Schema.Types.ObjectId, ref: 'User' },
last_update_user_id: {type: Schema.Types.ObjectId, ref: 'User' },
editor_name: String,
last_update_editor_name: String,
description: String,
state: {type: String, default: "idle"},
meta: {
linked_to: [String],
title: String,
tags: [String],
search_text: String,
link_uri: String,
play_from: Number,
play_to: Number,
},
board: {
x: {type: Number, default: 0.0},
y: {type: Number, default: 0.0},
z: {type: Number, default: 0.0},
r: {type: Number, default: 0.0},
w: {type: Number, default: 100},
h: {type: Number, default: 100},
},
control_points: [{
dx: Number, dy: Number
}],
group:{type: String, default: ""},
locked: {type: Boolean, default: false},
payload_uri: String,
payload_thumbnail_web_uri: String,
payload_thumbnail_medium_uri: String,
payload_thumbnail_big_uri: String,
payload_size: Number, // file size in bytes
style: {
fill_color: {type: String, default: "transparent"},
stroke_color:{type: String, default: "#000000"},
text_color: String,
stroke: {type: Number, default: 0.0},
stroke_style: {type: String, default: "solid"},
alpha: {type: Number, default: 1.0},
order: {type: Number, default: 0},
crop: {
x: Number,
y: Number,
w: Number,
h: Number
},
shape: String,
shape_svg: String,
padding_left: Number,
padding_right: Number,
padding_top: Number,
padding_bottom: Number,
margin_left: Number,
margin_right: Number,
margin_top: Number,
margin_bottom: Number,
border_radius: Number,
align: {type: String, default: "left"},
valign: {type: String, default: "top"},
brightness: Number,
contrast: Number,
saturation: Number,
blur: Number,
hue: Number,
opacity: Number
},
payload_alternatives: [{
mime: String,
payload_uri: String,
payload_thumbnail_web_uri: String,
payload_thumbnail_medium_uri: String,
payload_thumbnail_big_uri: String,
payload_size: Number
}],
created_at: {type: Date, default: Date.now},
created_from_ip: {type: String},
updated_at: {type: Date, default: Date.now}
});

View File

@ -1,364 +0,0 @@
const Umzug = require('umzug');
function sequel_log(a,b,c) {
console.log(a);
}
const Sequelize = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'sqlite',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
// SQLite only
storage: 'database.sqlite',
logging: sequel_log,
// http://docs.sequelizejs.com/manual/tutorial/querying.html#operators
operatorsAliases: false
});
var User;
var Session;
var Space;
var Membership;
var Artifact;
var Message;
var Action;
module.exports = {
User: sequelize.define('user', {
_id: {type: Sequelize.STRING, primaryKey: true},
email: Sequelize.STRING,
password_hash: Sequelize.STRING,
nickname: Sequelize.STRING,
avatar_original_uri: Sequelize.STRING,
avatar_thumb_uri: Sequelize.STRING,
confirmation_token: Sequelize.STRING,
password_reset_token: Sequelize.STRING,
home_folder_id: Sequelize.STRING,
prefs_language: Sequelize.STRING,
prefs_email_notifications: Sequelize.STRING,
prefs_email_digest: Sequelize.STRING,
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
updated_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW}
}),
Session: sequelize.define('session', {
token: {type: Sequelize.STRING, primaryKey: true},
user_id: Sequelize.STRING,
expires: Sequelize.DATE,
created_at: {type: Sequelize.DATE, defaultValue: Sequelize.NOW},
device: Sequelize.STRING,
ip: Sequelize.STRING
}),
Space: sequelize.define('space', {
_id: {type: Sequelize.STRING, primaryKey: true},
name: {type: Sequelize.STRING, default: "New Space"},
space_type: {type: Sequelize.STRING, defaultValue: "space"},
creator_id: Sequelize.STRING,
parent_space_id: Sequelize.STRING,
access_mode: {type: Sequelize.STRING, default: "private"}, // "public" || "private"
password: Sequelize.STRING,
edit_hash: Sequelize.STRING,
edit_slug: Sequelize.STRING,
editors_locking: Sequelize.BOOLEAN,
thumbnail_uri: Sequelize.STRING,
width: Sequelize.INTEGER,
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,
user_id: Sequelize.STRING,
role: Sequelize.STRING,
code: Sequelize.STRING,
state: {type: Sequelize.STRING, defaultValue: "pending"}, // valid: "pending", "active"
email_invited: Sequelize.STRING,
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,
user_id: Sequelize.STRING,
editor_name: Sequelize.STRING,
message: Sequelize.TEXT,
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,
user_id: Sequelize.STRING,
mime: Sequelize.STRING,
thumbnail_uri: Sequelize.STRING,
last_update_user_id: Sequelize.STRING,
editor_name: Sequelize.STRING,
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,
search_text: Sequelize.STRING,
link_uri: Sequelize.STRING,
play_from: Sequelize.DECIMAL,
play_to: Sequelize.DECIMAL,
x: {type: Sequelize.DECIMAL, default: 0.0},
y: {type: Sequelize.DECIMAL, default: 0.0},
z: {type: Sequelize.DECIMAL, default: 0.0},
r: {type: Sequelize.DECIMAL, default: 0.0},
w: {type: Sequelize.DECIMAL, default: 100},
h: {type: Sequelize.DECIMAL, default: 100},
//control_points: [{
// dx: Number, dy: Number
//}],
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,
stroke: {type: Sequelize.DECIMAL, default: 0.0},
stroke_style: {type: Sequelize.STRING, default: "solid"},
alpha: {type: Sequelize.DECIMAL, default: 1.0},
order: {type: Sequelize.INTEGER, default: 0},
crop_x: Sequelize.INTEGER,
crop_y: Sequelize.INTEGER,
crop_w: Sequelize.INTEGER,
crop_h: Sequelize.INTEGER,
shape: Sequelize.STRING,
shape_svg: Sequelize.STRING,
padding_left: Sequelize.INTEGER,
padding_right: Sequelize.INTEGER,
padding_top: Sequelize.INTEGER,
padding_bottom: Sequelize.INTEGER,
margin_left: Sequelize.INTEGER,
margin_right: Sequelize.INTEGER,
margin_top: Sequelize.INTEGER,
margin_bottom: Sequelize.INTEGER,
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,
blur: Sequelize.DECIMAL,
hue: Sequelize.DECIMAL,
opacity: Sequelize.DECIMAL,
payload_alternatives: Sequelize.TEXT,
/*payload_alternatives: [{
mime: String,
payload_uri: String,
payload_thumbnail_web_uri: String,
payload_thumbnail_medium_uri: String,
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: async function() {
User = this.User;
Session = this.Session;
Space = this.Space;
Artifact = this.Artifact;
Message = this.Message;
Membership = this.Membership;
Space.belongsTo(User, {
foreignKey: {
name: 'creator_id'
},
as: 'creator'
});
Membership.belongsTo(User, {
foreignKey: {
name: 'user_id'
},
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'
});
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 = [];
if (originalSpace._id == user.home_folder_id || (originalSpace.creator_id && originalSpace.creator_id == user._id)) {
cb("admin");
} else {
var findMembershipsForSpace = function(space, allMemberships, prevRole) {
Membership.findAll({ where: {
"space_id": space._id
}}).then(function(parentMemberships) {
var currentMemberships = parentMemberships.concat(allMemberships);
if (space.parent_space_id) {
Space.findOne({ where: {
"_id": space.parent_space_id
}}).then(function(parentSpace) {
findMembershipsForSpace(parentSpace, currentMemberships, prevRole);
});
} else {
// reached the top
var role = prevRole;
space.memberships = currentMemberships;
if (role == "none") {
if (originalSpace.access_mode == "public") {
role = "viewer";
}
}
currentMemberships.forEach(function(m, i) {
if (m.user_id && m.user_id == user._id) {
role = m.role;
}
});
cb(role);
}
});
};
findMembershipsForSpace(originalSpace, [], "none");
}
},
spaceToObject: (space) => {
// FIXME TODO
return space;
},
findUserBySessionToken: (token, cb) => {
Session.findOne({where: {token: token}})
.then(session => {
if (!session) cb(null, null)
else User.findOne({where: {_id: session.user_id}})
.then(user => {
cb(null, user)
})
})
},
unpackArtifact: (a) => {
if (a.tags && (typeof a.tags)=="string") {
a.tags = JSON.parse(a.tags);
}
if (a.control_points && (typeof a.control_points)=="string") {
a.control_points = JSON.parse(a.control_points);
}
if (a.payload_alternatives && (typeof a.payload_alternatives)=="string") {
a.payload_alternatives = JSON.parse(a.payload_alternatives);
}
return a;
},
packArtifact: (a) => {
if (a.tags && (typeof a.tags)!="string") {
a.tags = JSON.stringify(a.tags);
}
if (a.control_points && (typeof a.control_points)!="string") {
a.control_points = JSON.stringify(a.control_points);
}
if (a.payload_alternatives && (typeof a.payload_alternatives)!="string") {
a.payload_alternatives = JSON.stringify(a.payload_alternatives);
}
return a;
}
}

21
models/domain.js Normal file
View File

@ -0,0 +1,21 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.domainSchema = mongoose.Schema({
domain: String,
edu: Boolean,
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
module.exports.domainSchema.index({
domain: 1
});

45
models/membership.js Normal file
View File

@ -0,0 +1,45 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.membershipSchema = mongoose.Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
space: {
type: Schema.Types.ObjectId,
ref: 'Space'
},
team: {
type: Schema.Types.ObjectId,
ref: 'Team'
},
role: {
type: String,
default: "viewer"
},
state: {
type: String,
default: "active"
},
email_invited: String,
code: String,
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
module.exports.membershipSchema.index({
user: 1,
space: 1,
team: 1,
code: 1
});

31
models/message.js Normal file
View File

@ -0,0 +1,31 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.messageSchema = mongoose.Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
editor_name: String,
space: {
type: Schema.Types.ObjectId,
ref: 'Space'
},
message: String,
created_from_ip: {type: String},
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
module.exports.messageSchema.index({
space: 1,
user: 1
});

View File

@ -1,79 +0,0 @@
'use strict';
module.exports = {
up: function(migration, DataTypes) {
return Promise.all([
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 Promise.all([
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'
}
)
])
}
}

44
models/plan.js Normal file
View File

@ -0,0 +1,44 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
Plan = mongoose.model('Plan', {
key: String,
description: String,
limit_folders: {
type: Number,
default: 200
},
limit_spaces: {
type: Number,
default: 500
},
limit_storage_bytes: {
type: Number,
default: 10737418240
},
plan_type: {
type: String,
default: "org"
},
price: Number,
public: Boolean,
recurring: {
type: String,
default: "month"
},
title: String,
trial_days: Number,
voucher_code: String,
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
exports.planModel = Plan;

12
models/schema.js Normal file
View File

@ -0,0 +1,12 @@
//'use strict';
var mongoose = require('mongoose');
User = mongoose.model('User', require('./user').userSchema);
Action = mongoose.model('Action', require('./action').actionSchema);
Space = mongoose.model('Space', require('./space').spaceSchema);
Artifact = mongoose.model('Artifact', require('./artifact').artifactSchema);
Team = mongoose.model('Team', require('./team').teamSchema);
Message = mongoose.model('Message', require('./message').messageSchema);
Membership = mongoose.model('Membership', require('./membership').membershipSchema);
Domain = mongoose.model('Domain', require('./domain').domainSchema);

273
models/space.js Normal file
View File

@ -0,0 +1,273 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var async = require('async');
var _ = require("underscore");
var crypto = require('crypto');
module.exports.spaceSchema = Schema({
name: {type: String, default: "New Space"},
space_type: {type: String, default: "space"},
creator : { type: Schema.Types.ObjectId, ref: 'User' },
parent_space_id: Schema.Types.ObjectId,
access_mode: {type: String, default: "private"}, // "public" || "private"
password: String,
edit_hash: String,
edit_slug: String,
editors_locking: Boolean,
thumbnail_uri: String,
stats: {
num_children: Number,
total_spaces: Number,
total_folders: Number,
storage_bytes: Number,
},
advanced: {
type: {
width: Number,
height: Number,
margin: Number,
background_color: String,
background_uri: String,
background_repeat: Boolean,
grid_size: Number,
grid_divisions: Number,
gutter: Number,
columns: Number,
column_max_width: Number,
columns_responsive: Number,
row_max_height: Number,
padding_horz: Number,
padding_vert: Number
},
default: {
width: 200,
height: 400,
margin: 0,
background_color: "rgba(255,255,255,1)"
}
},
blocked_at: {type: Date, default: Date.now},
created_at: {type: Date, default: Date.now},
updated_at: {type: Date, default: Date.now},
thumbnail_updated_at: {type: Date},
thumbnail_url: String
});
module.exports.spaceSchema.index({ creator: 1, parent_space_id: 1, created_at: 1, updated_at: 1, edit_hash: 1});
module.exports.spaceSchema.statics.allForUser = function (user, callback) {
return this.find({user_id: user_id}, callback);
};
module.exports.spaceSchema.statics.getMemberships = function (err, callback) {
callback(null, {});
};
var getRecursiveSubspacesForSpace = (parentSpace, cb) => {
if (parentSpace.space_type == "folder") {
Space.find({
"parent_space_id": parentSpace._id
}).exec((err, subspaces) => {
async.map(subspaces, (space, innerCb) => {
getRecursiveSubspacesForSpace(space, (err, spaces) => {
innerCb(err, spaces);
});
}, (err, subspaces) => {
var flattenSubspaces = _.flatten(subspaces);
flattenSubspaces.push(parentSpace);
cb(null, flattenSubspaces);
});
});
} else {
cb(null, [parentSpace]);
}
};
module.exports.spaceSchema.statics.getRecursiveSubspacesForSpace = getRecursiveSubspacesForSpace;
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
module.exports.spaceSchema.statics.roleInSpace = (originalSpace, user, cb) => {
if (user.home_folder_id.toString() === originalSpace._id.toString()) {
cb(null, "admin");
return;
}
if (originalSpace.creator) {
if (originalSpace.creator._id.toString() === user._id.toString()) {
cb(null, "admin");
return;
}
}
var findMembershipsForSpace = function(space, allMemberships, prevRole) {
Membership.find({
"space": space._id
}, (err, parentMemberships) => {
var currentMemberships = parentMemberships.concat(allMemberships);
if (space.parent_space_id) {
Space.findOne({
"_id": space.parent_space_id
}, function(err, parentSpace) {
var role = prevRole;
if(role == "none"){
if(originalSpace.access_mode == "public") {
role = "viewer";
}
}
findMembershipsForSpace(parentSpace, currentMemberships, role);
});
} else {
// reached the top
var role = prevRole;
space.memberships = currentMemberships;
currentMemberships.forEach(function(m, i) {
if (m.user && m.user.equals(user._id)) {
if (m.role != null) {
if (roleMapping[m.role] > roleMapping[role]) {
role = m.role;
}
}
}
});
cb(err, role);
}
});
};
findMembershipsForSpace(originalSpace, [], "none");
}
module.exports.spaceSchema.statics.recursiveDelete = (space, cb) => {
space.remove(function(err) {
Action.remove({
space: space
}, function(err) {
if (err)
console.error("removed actions for space: ", err);
});
Membership.remove({
space: space
}, function(err) {
if (err)
console.error("removed memberships for space: ", err);
});
if (space.space_type === "folder") {
Space
.find({
parent_space_id: space._id
})
.exec(function(err, spaces) {
async.eachLimit(spaces, 10, function(subSpace, innerCb) {
module.exports.spaceSchema.statics.recursiveDelete(subSpace, function(err) {
innerCb(err);
});
}, function(err) {
cb(err);
});
});
} else {
Artifact.find({
space_id: space._id
}, function(err, artifacts) {
if (err) cb(err);
else {
async.eachLimit(artifacts, 20, function(a, innerCb) {
a.remove(function(err) {
innerCb(null, a);
});
}, function(err) {
cb(err);
});
}
});
}
});
};
var duplicateRecursiveSpace = (space, user, depth, cb, newParentSpace) => {
var newSpace = new Space(space);
newSpace._id = mongoose.Types.ObjectId();
if (newParentSpace) {
newSpace.parent_space_id = newParentSpace._id;
} else {
newSpace.name = newSpace.name + " (b)";
}
newSpace.creator = user;
newSpace.created_at = new Date();
newSpace.updated_at = new Date();
if (newSpace.space_type === "space") {
newSpace.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7);
}
newSpace.save(function(err) {
if (newSpace.space_type === "folder" && depth < 10) {
Space
.find({
parent_space_id: space._id
})
.exec(function(err, spaces) {
async.eachLimit(spaces, 10, function(subSpace, innerCb) {
duplicateRecursiveSpace(subSpace, user, ++depth, function(err, newSubSpace) {
innerCb(err, newSubSpace);
}, newSpace);
}, function(err, allNewSubspaces) {
cb(err, newSpace);
});
});
} else {
Artifact.find({
space_id: space._id
}, function(err, artifacts) {
if (err) innerCb(err);
else {
async.eachLimit(artifacts, 20, function(a, innerCb) {
var newArtifact = new Artifact(a);
newArtifact._id = mongoose.Types.ObjectId();
newArtifact.space_id = newSpace._id;
newArtifact.created_at = new Date();
newArtifact.updated_at = new Date();
newArtifact.save(function(err) {
innerCb(null, newArtifact);
});
}, function(err, allNewArtifacts) {
cb(err, newSpace);
});
}
});
}
});
};
module.exports.spaceSchema.statics.duplicateSpace = duplicateRecursiveSpace;

70
models/team.js Normal file
View File

@ -0,0 +1,70 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.teamSchema = mongoose.Schema({
name: String,
subdomain: String,
creator: {
type: Schema.Types.ObjectId,
ref: 'User'
},
admins: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
invitation_codes: [String],
avatar_thumb_uri: String,
avatar_uri: String,
payment_type: {
type: String,
default: "auto"
},
payment_plan_key: String,
payment_subscription_id: String,
blocked_at: {
type: Date
},
upgraded_at: {
type: Date
},
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
module.exports.teamSchema.index({
creator: 1
});
module.exports.teamSchema.statics.getTeamForHost = (host, cb) => {
if (host != "127.0.0.1:9666") { //phantomjs check
let subDomainParts = host.split('.');
if (subDomainParts.length > 2) {
const subdomain = subDomainParts[0];
if (subdomain != "www") {
Team.findOne({
subdomain: subdomain
}).exec((err, team) => {
cb(err, team, subdomain)
});
} else {
cb(null, null)
}
} else {
cb(null, null);
}
} else {
cb(null, null);
}
}

53
models/user.js Normal file
View File

@ -0,0 +1,53 @@
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.userSchema = mongoose.Schema({
email: String,
password_hash: String,
nickname: String,
account_type: {type: String, default: "email"},
created_at: {type: Date, default: Date.now},
updated_at: {type: Date, default: Date.now},
avatar_original_uri: String,
avatar_thumb_uri: String,
src: String,
confirmation_token: String,
confirmed_at: Date,
password_reset_token: String,
home_folder_id: Schema.Types.ObjectId,
team : { type: Schema.Types.ObjectId, ref: 'Team' },
preferences: {
language: String,
email_notifications: {type: Boolean, default: true},
daily_digest_last_send: Date,
daily_digest: {type: Boolean, default: true}
},
sessions: [
{
token: String,
expires: Date,
device: String,
ip: String,
created_at: Date
}
],
payment_info: String,
payment_plan_key: {type: String, default: "free"},
payment_customer_id: String,
payment_subscription_id: String,
payment_notification_state: Number
});
module.exports.userSchema.index({
email: 1,
"sessions.token": 1,
team: 1,
created_at: 1,
home_folder_id: 1
});
module.exports.userSchema.statics.findBySessionToken = function (token, cb) {
return this.findOne({ "sessions.token": token}, cb);
};

View File

@ -3,55 +3,85 @@
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node spacedeck.js"
"start": "nodemon -e .js,.html bin/www",
"test": "mocha"
},
"engines": {
"node": ">=10.0.0"
"node": ">=7.8.0"
},
"dependencies": {
"archiver": "1.3.0",
"async": "2.3.0",
"aws-sdk": "2.39.0",
"basic-auth": "1.1.0",
"bcryptjs": "2.4.3",
"body-parser": "^1.19.0",
"body-parser": "~1.17.1",
"cheerio": "0.22.0",
"config": "1.25.1",
"cookie-parser": "~1.4.3",
"csurf": "1.9.0",
"debug": "~2.6.3",
"execSync": "latest",
"express": "^4.16.4",
"file-type": "^7.6.0",
"express": "~4.13.0",
"extract-zip": "^1.6.6",
"glob": "7.1.1",
"gm": "^1.23.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-sass": "^4.0.2",
"gm": "1.23.0",
"googleapis": "18.0.0",
"gulp": "^3.9.1",
"gulp-concat": "2.6.0",
"gulp-express": "0.3.0",
"gulp-nodemon": "*",
"gulp-sass": "^2.0.3",
"gulp-uglify": "^1.5.1",
"gulp-util": "^3.0.6",
"helmet": "^3.5.0",
"i18n-2": "0.6.3",
"ioredis": "2.5.0",
"lodash": "^4.3.0",
"log-timestamp": "latest",
"md5": "2.2.1",
"mock-aws-s3": "^2.6.0",
"moment": "^2.19.3",
"morgan": "^1.9.1",
"mongoose": "4.9.3",
"morgan": "1.8.1",
"node-phantom-simple": "2.2.4",
"node-server-screenshot": "^0.2.1",
"nodemailer": "^4.6.7",
"phantomjs-prebuilt": "^2.1.16",
"read-chunk": "^2.1.0",
"request": "^2.88.0",
"node-sass-middleware": "0.11.0",
"pdfkit": "0.8.0",
"phantomjs-prebuilt": "2.1.14",
"pm2": "latest",
"qr-image": "3.2.0",
"raven": "1.2.0",
"request": "2.81.0",
"sanitize-html": "^1.11.1",
"sequelize": "^4.37.6",
"serve-favicon": "~2.4.2",
"serve-static": "^1.13.1",
"slug": "^1.1.0",
"sqlite3": "^4.0.0",
"slug": "0.9.1",
"swig": "1.4.2",
"umzug": "^2.1.0",
"underscore": "1.8.3",
"uuid": "^3.2.1",
"validator": "7.0.0",
"ws": "3.3.1"
"weak": "1.0.1",
"ws": "2.2.3"
},
"devDependencies": {
"express": "^4.13.3",
"gulp": "^3.9.1",
"gulp-clean": "^0.3.2",
"gulp-concat": "^2.6.0",
"gulp-express": "^0.3.0",
"gulp-fingerprint": "^0.3.2",
"gulp-nodemon": "^2.0.4",
"gulp-rev": "^7.1.2",
"gulp-rev-all": "^0.9.7",
"gulp-rev-replace": "^0.4.3",
"gulp-sass": "^3.1.0",
"gulp-uglify": "^2.1.2",
"nodemon": "1.11.0",
"should": "^11.2.1",
"supertest": "^3.0.0",
"winston": "^2.3.1"
},
"main": "app.js",
"description": "",
"main": "Gulpfile.js",
"directories": {},
"repository": {
"type": "git",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="17.370329mm"
height="17.370247mm"
viewBox="0 0 17.370329 17.370247"
version="1.1"
id="svg3417"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="sd6-icon-white.svg"
inkscape:export-filename="/home/mntmn/code/spacedeck-open/public/images/favicon.png"
inkscape:export-xdpi="93.585312"
inkscape:export-ydpi="93.585312">
<defs
id="defs3411" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="68.901329"
inkscape:cy="26.613846"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1376"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata3414">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-61.618407,-79.672019)">
<path
inkscape:connector-curvature="0"
id="path1681-6-5-3-7-4-9-2-0-2-9-7"
d="m 69.103371,79.69206 c -0.792105,0.07526 -1.553632,0.368078 -2.179688,0.99414 -0.967242,0.967233 -1.023215,2.24006 -0.822265,3.46875 -1.228429,-0.200703 -2.499819,-0.144769 -3.466797,0.822266 -1.252082,1.252133 -1.178244,3.043412 -0.677734,4.544922 0.500509,1.50151 1.477937,2.995513 2.832031,4.349611 1.354102,1.3541 2.848091,2.33152 4.349609,2.83203 1.501518,0.50051 3.292795,0.57437 4.544922,-0.67773 0.9673,-0.96727 1.023249,-2.24001 0.822266,-3.468755 1.228416,0.200714 2.499803,0.146691 3.466796,-0.820313 1.252124,-1.252112 1.17824,-3.045353 0.677735,-4.546874 -0.500505,-1.501522 -1.477926,-2.995502 -2.832031,-4.34961 -1.354109,-1.354105 -2.848087,-2.329573 -4.34961,-2.830078 -0.750761,-0.250253 -1.57313,-0.393617 -2.365234,-0.318359 z m 0.251953,3.427734 c -0.06232,0.06232 0.187775,-0.12686 1.025391,0.152344 0.837615,0.279204 1.980359,0.976455 3.005859,2.001953 1.025498,1.0255 1.720796,2.16629 2,3.003906 0.279204,0.837616 0.09198,1.087707 0.154297,1.025391 0.06232,-0.06232 -0.187775,0.124907 -1.025391,-0.154297 -0.817005,-0.272334 -1.926016,-0.966798 -2.93164,-1.951172 -0.02107,-0.02133 -0.03343,-0.04515 -0.05469,-0.06641 -0.02194,-0.02194 -0.04635,-0.0349 -0.06836,-0.05664 -0.984356,-1.005615 -1.678841,-2.112692 -1.951172,-2.929687 -0.279204,-0.837616 -0.09198,-1.087708 -0.154297,-1.025391 z m -4.289063,4.289063 c -0.06231,0.06232 0.187774,-0.124903 1.025391,0.154296 0.81575,0.271911 1.923337,0.965368 2.927735,1.947266 0.02276,0.02306 0.03561,0.04929 0.05859,0.07227 0.023,0.023 0.04918,0.03581 0.07227,0.05859 0.981898,1.004395 1.67535,2.111982 1.947265,2.927735 0.279205,0.837619 0.09198,1.087705 0.154297,1.025385 0.06232,-0.0623 -0.187772,0.12492 -1.02539,-0.154291 -0.837619,-0.27921 -1.980364,-0.974504 -3.00586,-2 -1.025488,-1.025491 -1.720791,-2.168245 -2,-3.00586 -0.279208,-0.837615 -0.09198,-1.087708 -0.154297,-1.02539 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.4395833;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:37.79527664;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="17.370329mm"
height="17.370247mm"
viewBox="0 0 17.370329 17.370247"
version="1.1"
id="svg3417"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="sd6-icon.svg">
<defs
id="defs3411" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="68.901329"
inkscape:cy="26.613846"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1376"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata3414">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-61.618407,-79.672019)">
<path
inkscape:connector-curvature="0"
id="path1681-6-5-3-7-4-9-2-0-2-9-7"
d="m 69.103371,79.69206 c -0.792105,0.07526 -1.553632,0.368078 -2.179688,0.99414 -0.967242,0.967233 -1.023215,2.24006 -0.822265,3.46875 -1.228429,-0.200703 -2.499819,-0.144769 -3.466797,0.822266 -1.252082,1.252133 -1.178244,3.043412 -0.677734,4.544922 0.500509,1.50151 1.477937,2.995513 2.832031,4.349611 1.354102,1.3541 2.848091,2.33152 4.349609,2.83203 1.501518,0.50051 3.292795,0.57437 4.544922,-0.67773 0.9673,-0.96727 1.023249,-2.24001 0.822266,-3.468755 1.228416,0.200714 2.499803,0.146691 3.466796,-0.820313 1.252124,-1.252112 1.17824,-3.045353 0.677735,-4.546874 -0.500505,-1.501522 -1.477926,-2.995502 -2.832031,-4.34961 -1.354109,-1.354105 -2.848087,-2.329573 -4.34961,-2.830078 -0.750761,-0.250253 -1.57313,-0.393617 -2.365234,-0.318359 z m 0.251953,3.427734 c -0.06232,0.06232 0.187775,-0.12686 1.025391,0.152344 0.837615,0.279204 1.980359,0.976455 3.005859,2.001953 1.025498,1.0255 1.720796,2.16629 2,3.003906 0.279204,0.837616 0.09198,1.087707 0.154297,1.025391 0.06232,-0.06232 -0.187775,0.124907 -1.025391,-0.154297 -0.817005,-0.272334 -1.926016,-0.966798 -2.93164,-1.951172 -0.02107,-0.02133 -0.03343,-0.04515 -0.05469,-0.06641 -0.02194,-0.02194 -0.04635,-0.0349 -0.06836,-0.05664 -0.984356,-1.005615 -1.678841,-2.112692 -1.951172,-2.929687 -0.279204,-0.837616 -0.09198,-1.087708 -0.154297,-1.025391 z m -4.289063,4.289063 c -0.06231,0.06232 0.187774,-0.124903 1.025391,0.154296 0.81575,0.271911 1.923337,0.965368 2.927735,1.947266 0.02276,0.02306 0.03561,0.04929 0.05859,0.07227 0.023,0.023 0.04918,0.03581 0.07227,0.05859 0.981898,1.004395 1.67535,2.111982 1.947265,2.927735 0.279205,0.837619 0.09198,1.087705 0.154297,1.025385 0.06232,-0.0623 -0.187772,0.12492 -1.02539,-0.154291 -0.837619,-0.27921 -1.980364,-0.974504 -3.00586,-2 -1.025488,-1.025491 -1.720791,-2.168245 -2,-3.00586 -0.279208,-0.837615 -0.09198,-1.087708 -0.154297,-1.02539 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.4395833;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:37.79527664;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="81.731232mm"
height="17.370247mm"
viewBox="0 0 81.731232 17.370247"
version="1.1"
id="svg2651"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="sd6-logo-black.svg">
<defs
id="defs2645" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8"
inkscape:cx="80.852573"
inkscape:cy="-16.110417"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="2560"
inkscape:window-height="1376"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata2648">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-29.059958,-86.19285)">
<g
id="g3248">
<path
inkscape:connector-curvature="0"
id="path1681-6-5-3-7-4-9-2-0-2-9-7"
d="m 36.544922,86.212891 c -0.792105,0.07526 -1.553632,0.368078 -2.179688,0.99414 -0.967242,0.967233 -1.023215,2.24006 -0.822265,3.46875 -1.228429,-0.200703 -2.499819,-0.144769 -3.466797,0.822266 -1.252082,1.252133 -1.178244,3.043412 -0.677734,4.544922 0.500509,1.50151 1.477937,2.995513 2.832031,4.349611 1.354102,1.3541 2.848091,2.33152 4.349609,2.83203 1.501518,0.50051 3.292795,0.57437 4.544922,-0.67773 0.9673,-0.96727 1.023249,-2.24001 0.822266,-3.468755 1.228416,0.200714 2.499803,0.146691 3.466796,-0.820313 1.252124,-1.252112 1.17824,-3.045353 0.677735,-4.546874 -0.500505,-1.501522 -1.477926,-2.995502 -2.832031,-4.34961 -1.354109,-1.354105 -2.848087,-2.329573 -4.34961,-2.830078 -0.750761,-0.250253 -1.57313,-0.393617 -2.365234,-0.318359 z m 0.251953,3.427734 c -0.06232,0.06232 0.187775,-0.12686 1.025391,0.152344 0.837615,0.279204 1.980359,0.976455 3.005859,2.001953 1.025498,1.0255 1.720796,2.16629 2,3.003906 0.279204,0.837616 0.09198,1.087707 0.154297,1.025391 0.06232,-0.06232 -0.187775,0.124907 -1.025391,-0.154297 -0.817005,-0.272334 -1.926016,-0.966798 -2.93164,-1.951172 -0.02107,-0.02133 -0.03343,-0.04515 -0.05469,-0.06641 -0.02194,-0.02194 -0.04635,-0.0349 -0.06836,-0.05664 -0.984356,-1.005615 -1.678841,-2.112692 -1.951172,-2.929687 -0.279204,-0.837616 -0.09198,-1.087708 -0.154297,-1.025391 z m -4.289063,4.289063 c -0.06231,0.06232 0.187774,-0.124903 1.025391,0.154296 0.81575,0.271911 1.923337,0.965368 2.927735,1.947266 0.02276,0.02306 0.03561,0.04929 0.05859,0.07227 0.023,0.023 0.04918,0.03581 0.07227,0.05859 0.981898,1.004395 1.67535,2.111982 1.947265,2.927735 0.279205,0.837619 0.09198,1.087705 0.154297,1.025385 0.06232,-0.0623 -0.187772,0.12492 -1.02539,-0.154291 -0.837619,-0.27921 -1.980364,-0.974504 -3.00586,-2 -1.025488,-1.025491 -1.720791,-2.168245 -2,-3.00586 -0.279208,-0.837615 -0.09198,-1.087708 -0.154297,-1.02539 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.4395833;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:37.79527664;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<g
id="g2614"
transform="matrix(0.26458333,0,0,0.26458333,-523.78744,61.714265)">
<g
id="flowRoot1610-0-6-8-1-1"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;line-height:1.25;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.37800002;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:37.79527664;stroke-opacity:1"
transform="matrix(2.6369365,0,0,2.6369365,2045.0224,86.079903)"
aria-label="Spacedeck">
<path
id="path3214"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 52.795627,11.002199 h 2.693247 C 55.466146,8.82601 53.73315,7.3543837 51.028539,7.3543837 c -2.659155,0 -4.573974,1.4488985 -4.556928,3.6137233 -0.0057,1.767088 1.232985,2.76143 3.244397,3.221669 l 1.215938,0.284097 c 1.27844,0.295462 1.852317,0.642061 1.863681,1.295486 -0.01136,0.710244 -0.676152,1.204575 -1.806861,1.204575 -1.244349,0 -2.06255,-0.57956 -2.125052,-1.698905 h -2.693246 c 0.03409,2.721656 1.926182,4.022824 4.852389,4.022824 2.897797,0 4.613748,-1.312532 4.625112,-3.522812 -0.01136,-1.857999 -1.267076,-2.99439 -3.562586,-3.500084 l -1.000024,-0.227278 c -1.056844,-0.227279 -1.727315,-0.57956 -1.704587,-1.272758 0.0057,-0.636379 0.55115,-1.0966177 1.642085,-1.0966177 1.096618,0 1.698905,0.4943297 1.77277,1.3238957 z"
inkscape:connector-curvature="0" />
<path
id="path3216"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 57.068458,22.422928 h 2.778476 v -4.687613 h 0.05682 c 0.352281,0.806838 1.136391,1.53981 2.454605,1.53981 1.931864,0 3.48872,-1.5114 3.48872,-4.483062 0,-3.07962 -1.647767,-4.483063 -3.471674,-4.483063 -1.380715,0 -2.136415,0.806838 -2.471651,1.607994 h -0.08523 v -1.494355 h -2.750066 z m 2.721656,-7.636547 c 0,-1.426171 0.590923,-2.306874 1.607993,-2.306874 1.028434,0 1.59663,0.903431 1.59663,2.306874 0,1.409125 -0.568196,2.323919 -1.59663,2.323919 -1.01707,0 -1.607993,-0.909112 -1.607993,-2.323919 z"
inkscape:connector-curvature="0" />
<path
id="path3218"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 69.778991,19.297853 c 1.215939,0 2.056868,-0.471602 2.534152,-1.363669 h 0.06818 v 1.215938 h 2.613699 v -5.931961 c 0,-1.846635 -1.642085,-2.909161 -3.86373,-2.909161 -2.346647,0 -3.676224,1.181847 -3.897821,2.772794 l 2.562562,0.09091 c 0.119321,-0.556832 0.579559,-0.897749 1.312532,-0.897749 0.681834,0 1.113663,0.329553 1.113663,0.914794 v 0.02841 c 0,0.534104 -0.57956,0.647743 -2.068232,0.778428 -1.767088,0.147731 -3.244396,0.801156 -3.244396,2.73302 0,1.727315 1.198892,2.568244 2.869387,2.568244 z m 0.857975,-1.818226 c -0.642061,0 -1.096617,-0.306825 -1.096617,-0.886384 0,-0.562514 0.443193,-0.903431 1.232984,-1.022752 0.517058,-0.07387 1.153437,-0.187505 1.465945,-0.352282 v 0.829566 c 0,0.852293 -0.715927,1.431852 -1.602312,1.431852 z"
inkscape:connector-curvature="0" />
<path
id="path3220"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 80.767893,19.314899 c 2.454605,0 3.977369,-1.426171 4.051234,-3.585314 h -2.596653 c -0.102275,0.926159 -0.659107,1.431853 -1.420489,1.431853 -0.977296,0 -1.613675,-0.823883 -1.613675,-2.375057 0,-1.53981 0.642061,-2.363693 1.613675,-2.363693 0.795474,0 1.312532,0.539785 1.420489,1.431852 h 2.596653 c -0.0625,-2.147779 -1.630721,-3.54554 -4.056916,-3.54554 -2.744384,0 -4.403515,1.82959 -4.403515,4.505791 0,2.664836 1.647767,4.500108 4.409197,4.500108 z"
inkscape:connector-curvature="0" />
<path
id="path3222"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 90.336304,19.314899 c 2.289828,0 3.795546,-1.107981 4.113736,-2.823932 l -2.551198,-0.07387 c -0.215914,0.579559 -0.78411,0.892067 -1.5114,0.892067 -1.068208,0 -1.727315,-0.710245 -1.727315,-1.778452 v -0.07386 h 5.818322 v -0.693199 c 0,-2.875069 -1.750042,-4.454653 -4.227374,-4.454653 -2.636427,0 -4.32965,1.806862 -4.32965,4.511473 0,2.795521 1.670495,4.494426 4.414879,4.494426 z m -1.676177,-5.471723 c 0.03977,-0.869339 0.727291,-1.528446 1.647767,-1.528446 0.914795,0 1.573902,0.636379 1.585266,1.528446 z"
inkscape:connector-curvature="0" />
<path
id="path3224"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 99.10072,19.275125 c 1.31821,0 2.10232,-0.732972 2.4546,-1.53981 h 0.0852 v 1.414807 h 2.75007 V 7.5134784 h -2.77848 v 4.4035156 h -0.0568 C 101.22577,11.115838 100.46439,10.309 99.089356,10.309 c -1.823907,0 -3.477356,1.403443 -3.477356,4.483063 0,2.971662 1.562537,4.483062 3.48872,4.483062 z m 0.96593,-2.164825 c -1.028432,0 -1.602309,-0.914794 -1.602309,-2.323919 0,-1.403443 0.568196,-2.306874 1.602309,-2.306874 1.01707,0 1.608,0.880703 1.608,2.306874 0,1.414807 -0.59661,2.323919 -1.608,2.323919 z"
inkscape:connector-curvature="0" />
<path
id="path3226"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 110.24303,19.314899 c 2.28983,0 3.79555,-1.107981 4.11374,-2.823932 l -2.5512,-0.07387 c -0.21591,0.579559 -0.78411,0.892067 -1.5114,0.892067 -1.06821,0 -1.72731,-0.710245 -1.72731,-1.778452 v -0.07386 h 5.81832 v -0.693199 c 0,-2.875069 -1.75004,-4.454653 -4.22737,-4.454653 -2.63643,0 -4.32965,1.806862 -4.32965,4.511473 0,2.795521 1.67049,4.494426 4.41487,4.494426 z m -1.67617,-5.471723 c 0.0398,-0.869339 0.72729,-1.528446 1.64777,-1.528446 0.91479,0 1.5739,0.636379 1.58526,1.528446 z"
inkscape:connector-curvature="0" />
<path
id="path3228"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 119.89384,19.314899 c 2.4546,0 3.97736,-1.426171 4.05123,-3.585314 h -2.59665 c -0.10228,0.926159 -0.65911,1.431853 -1.42049,1.431853 -0.9773,0 -1.61368,-0.823883 -1.61368,-2.375057 0,-1.53981 0.64206,-2.363693 1.61368,-2.363693 0.79547,0 1.31253,0.539785 1.42049,1.431852 h 2.59665 c -0.0625,-2.147779 -1.63072,-3.54554 -4.05692,-3.54554 -2.74438,0 -4.40351,1.82959 -4.40351,4.505791 0,2.664836 1.64777,4.500108 4.4092,4.500108 z"
inkscape:connector-curvature="0" />
<path
id="path3230"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:16.00038528px;font-family:Inter;-inkscape-font-specification:'Inter, Ultra-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
d="m 125.3826,19.150122 h 2.77848 v -2.619381 l 0.56251,-0.681835 2.0796,3.301216 h 3.2103 l -3.22167,-4.926255 3.09667,-3.801228 h -3.1478 l -2.45461,3.125076 h -0.125 V 7.5134784 h -2.77848 z"
inkscape:connector-curvature="0" />
</g>
<path
d="m 2146.72,133.51812 a 23.030019,11.514995 45 0 1 -24.427,-8.1423 23.030019,11.514995 45 0 1 -8.1423,-24.427 23.030019,11.514995 45 0 1 24.427,8.1423 23.030019,11.514995 45 0 1 8.1423,24.427 z m -16.2137,16.2138 a 23.030019,11.514995 45 0 1 -24.427,-8.1424 23.030019,11.514995 45 0 1 -8.1424,-24.4269 23.030019,11.514995 45 0 1 24.4271,8.1422 23.030019,11.514995 45 0 1 8.1423,24.4271 z"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:37.79527664;stroke-opacity:1"
id="path1681-6-5-3-7-4-9-2-0-2-7-6"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

View File

@ -133,6 +133,10 @@ function load_spaces(id, is_home, on_success, on_error) {
}, on_error);
}
function load_writable_folders( on_success, on_error) {
load_resource("get", "/spaces?writablefolders=true", null, on_success, on_error);
}
function load_history(s, on_success, on_error) {
load_resource("get", "/spaces/"+ s._id +"/digest", null, on_success, on_error);
}
@ -178,10 +182,12 @@ function delete_space(s, on_success, on_error) {
load_resource("delete", "/spaces/"+s._id, null, on_success, on_error);
}
function delete_artifact(a, on_success, on_error) {
load_resource("delete", "/spaces/"+a.space_id+"/artifacts/"+a._id);
}
function duplicate_space(s, to_space_id, on_success, on_error) {
var path = "/spaces/"+s._id+"/duplicate";
if(to_space_id) {
@ -260,8 +266,8 @@ function delete_user(u, password, on_success, on_error) {
load_resource("delete", "/users/"+u._id +"?password="+password,null,on_success,on_error);
}
function create_user(name, email, password, password_confirmation, invite_code, on_success, on_error) {
load_resource("post", "/users", {email:email, nickname:name, password:password, password_confirmation: password_confirmation, invite_code: invite_code}, on_success, on_error);
function create_user(name, email, password, password_confirmation, on_success, on_error) {
load_resource("post", "/users", {email:email, nickname:name, password:password, password_confirmation: password_confirmation}, on_success, on_error);
}
function create_session(email, password, on_success, on_error) {

File diff suppressed because it is too large Load Diff

View File

@ -8,22 +8,24 @@ SpacedeckAccount = {
account_confirmed_sent: false,
account_tab: 'invoices',
password_change_error: null,
feedback_text: "",
feedback_text: ""
},
methods: {
show_account: function() {
show_account: function(user) {
this.activate_dropdown('account');
this.load_subscription();
this.load_billing();
},
account_save_user_digest: function(val) {
this.user.prefs_email_digest = val;
this.save_user(function() {
this.user.preferences.daily_digest = val;
this.save_user(function(){
});
},
account_save_user_notifications: function(val) {
this.user.prefs_email_notifications = val;
this.save_user(function() {
this.user.preferences.email_notifications = val;
this.save_user(function(){
});
},
@ -34,11 +36,13 @@ SpacedeckAccount = {
save_user_language: function(lang) {
localStorage.lang = lang;
this.user.prefs_language = lang;
if (this.user.preferences) {
this.user.preferences.language = lang;
this.save_user(function() {
window._spacedeck_location_change = true;
location.href="/spaces";
}.bind(this));
}
},
save_user: function(on_success) {

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.toString());
medium.value(a.description);
}
}
},
@ -61,16 +61,16 @@ var SpacedeckBoardArtifacts = {
},
artifact_link: function(a) {
if (a.link_uri) {
return a.link_uri;
if (a.meta && a.meta.link_uri) {
return a.meta.link_uri;
} else {
return "";
}
},
artifact_link_caption: function(a) {
if (a.link_uri) {
var parts = a.link_uri.split("/");
if (a.meta && a.meta.link_uri) {
var parts = a.meta.link_uri.split("/");
// scheme://domain.foo/...
// 0 1 2
if (parts.length>2) {
@ -88,11 +88,10 @@ var SpacedeckBoardArtifacts = {
},
artifact_is_text_blank: function(a) {
if (a.description) {
desc = a.description.toString();
var filtered = desc.replace(/<[^>]+>/g,"").replace(/\s/g,"");
if(a.description){
var filtered = a.description.replace(/<[^>]+>/g,"").replace(/\s/g,"");
return (filtered.length<1);
} else {
}else{
return false;
}
},
@ -103,8 +102,10 @@ var SpacedeckBoardArtifacts = {
if (this.artifact_is_selected(a) && this.editing_artifact_id!=a._id) clzs.push("selected");
if (!a._id) clzs.push("creating");
if (a.align) clzs.push("align-"+a.align);
if (a.valign) clzs.push("align-"+a.valign);
if (a.style) {
clzs.push("align-"+a.style.align);
clzs.push("align-"+a.style.valign);
}
clzs.push("state-"+a.state);
@ -122,56 +123,56 @@ var SpacedeckBoardArtifacts = {
artifact_inner_style: function(a) {
var styles = [];
//if (a.style) {
if (a.style) {
var svg_style = ((a.mime.match("vector") || a.mime.match("shape")) && a.shape!="square");
var svg_style = ((a.mime.match("vector") || a.mime.match("shape")) && a.style.shape!="square");
if (!svg_style) {
if (a.stroke) {
styles.push("border-width:"+a.stroke+"px");
styles.push("border-style:"+(a.stroke_style||"solid"));
if (a.style.stroke) {
styles.push("border-width:"+a.style.stroke+"px");
styles.push("border-style:"+(a.style.stroke_style||"solid"));
}
if (a.stroke_color) {
styles.push("border-color:"+a.stroke_color);
if (a.style.stroke_color) {
styles.push("border-color:"+a.style.stroke_color);
}
if (a.border_radius) {
styles.push("border-radius:"+a.border_radius+"px");
if (a.style.border_radius) {
styles.push("border-radius:"+a.style.border_radius+"px");
}
}
if (a.fill_color && !svg_style) {
styles.push("background-color:"+a.fill_color);
if (a.style.fill_color && !svg_style) {
styles.push("background-color:"+a.style.fill_color);
}
if (a.text_color) {
styles.push("color:"+a.text_color);
if (a.style.text_color) {
styles.push("color:"+a.style.text_color);
}
var filters = [];
if (!isNaN(a.brightness) && a.brightness != 100) {
filters.push("brightness("+a.brightness+"%)");
if (!isNaN(a.style.brightness) && a.style.brightness != 100) {
filters.push("brightness("+a.style.brightness+"%)");
}
if (!isNaN(a.contrast) && a.contrast != 100) {
filters.push("contrast("+a.contrast+"%)");
if (!isNaN(a.style.contrast) && a.style.contrast != 100) {
filters.push("contrast("+a.style.contrast+"%)");
}
if (!isNaN(a.opacity) && a.opacity != 100) {
filters.push("opacity("+a.opacity+"%)");
if (!isNaN(a.style.opacity) && a.style.opacity != 100) {
filters.push("opacity("+a.style.opacity+"%)");
}
if (!isNaN(a.hue) && a.hue) {
filters.push("hue-rotate("+a.hue+"deg)");
if (!isNaN(a.style.hue) && a.style.hue) {
filters.push("hue-rotate("+a.style.hue+"deg)");
}
if (!isNaN(a.saturation) && a.saturation != 100) {
filters.push("saturate("+a.saturation+"%)");
if (!isNaN(a.style.saturation) && a.style.saturation != 100) {
filters.push("saturate("+a.style.saturation+"%)");
}
if (!isNaN(a.blur) && a.blur) {
filters.push("blur("+a.blur+"px)");
if (!isNaN(a.style.blur) && a.style.blur) {
filters.push("blur("+a.style.blur+"px)");
}
if (filters.length) {
styles.push("-webkit-filter:"+filters.join(" "));
styles.push("filter:"+filters.join(" "));
}
//}
}
return styles.join(";");
},
@ -179,10 +180,12 @@ var SpacedeckBoardArtifacts = {
artifact_text_cell_style: function(a, for_text_editor) {
var styles = [];
if (a.padding_left) styles.push("padding-left:"+a.padding_left+"px");
if (a.padding_right) styles.push("padding-right:"+a.padding_right+"px");
if (a.padding_top) styles.push("padding-top:"+a.padding_top+"px");
if (a.padding_bottom) styles.push("padding-bottom:"+a.padding_bottom+"px");
if (a.style) {
if (a.style.padding_left) styles.push("padding-left:"+a.style.padding_left+"px");
if (a.style.padding_right) styles.push("padding-right:"+a.style.padding_right+"px");
if (a.style.padding_top) styles.push("padding-top:"+a.style.padding_top+"px");
if (a.style.padding_bottom) styles.push("padding-bottom:"+a.style.padding_bottom+"px");
}
return styles.join(";");
},
@ -191,21 +194,25 @@ var SpacedeckBoardArtifacts = {
var styles = [];
var z = 0;
z = a.z;
if (a.board) {
z = a.board.z;
if (z<0) z=0; // fix negative z-index
styles = [
"left:" +a.x+"px",
"top:" +a.y+"px",
"width:" +a.w+"px",
"height:"+a.h+"px",
"left:" +a.board.x+"px",
"top:" +a.board.y+"px",
"width:" +a.board.w+"px",
"height:"+a.board.h+"px",
"z-index:"+z
];
}
if (a.margin_left) styles.push("margin-left:"+a.margin_left+"px");
if (a.margin_right) styles.push("margin-right:"+a.margin_right+"px");
if (a.margin_top) styles.push("margin-top:"+a.margin_top+"px");
if (a.margin_bottom) styles.push("margin-bottom:"+a.margin_bottom+"px");
if (a.style) {
if (a.style.margin_left) styles.push("margin-left:"+a.style.margin_left+"px");
if (a.style.margin_right) styles.push("margin-right:"+a.style.margin_right+"px");
if (a.style.margin_top) styles.push("margin-top:"+a.style.margin_top+"px");
if (a.style.margin_bottom) styles.push("margin-bottom:"+a.style.margin_bottom+"px");
}
// FIXME: via class logic?
if (a.mime.match("vector")) {
@ -234,7 +241,7 @@ var SpacedeckBoardArtifacts = {
artifact_thumbnail_uri: function(a) {
if (a.payload_thumbnail_big_uri && a.board) {
if (a.w>800) {
if (a.board.w>800) {
return a.payload_thumbnail_big_uri;
}
}
@ -248,35 +255,35 @@ var SpacedeckBoardArtifacts = {
var type = parts[0];
var provider = parts[1];
if (!a.link_uri) {
if (!a.meta || !a.meta.link_uri) {
console.log("missing meta / link_uri: ",a);
console.log("type/provider: ",type,provider);
return ("missing metadata: "+a._id);
}
if (provider=="youtube") {
var vid = a.link_uri.match(/(v=|\/)([a-zA-Z0-9\-_]{11})/);
var vid = a.meta.link_uri.match(/(v=|\/)([a-zA-Z0-9\-_]{11})/);
if (vid && vid.length>2) {
var uri = "https://youtube.com/embed/"+vid[2];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"?showinfo=0&rel=0&controls=0\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="dailymotion") {
var match = a.link_uri.match(/dailymotion.com\/video\/([^<]*)/);
var match = a.meta.link_uri.match(/dailymotion.com\/video\/([^<]*)/);
if (match && match.length>1) {
var uri = "https://www.dailymotion.com/embed/video/"+match[1];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="vimeo") {
var match = a.link_uri.match(/https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/);
var match = a.meta.link_uri.match(/https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/);
if (match) {
var uri = "https://player.vimeo.com/video/"+match[2];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="soundcloud") {
return '<iframe width="100%" height="166" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url='+a.link_uri.replace(":", "%3A")+'"></iframe>';
return '<iframe width="100%" height="166" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url='+a.meta.link_uri.replace(":", "%3A")+'"></iframe>';
} else if (provider=="spacedeck") {
@ -292,8 +299,8 @@ var SpacedeckBoardArtifacts = {
if (mtype != "vector" && mtype != "shape") return "";
var shape = a.shape || "";
var padding = 32 + a.stroke*2;
var shape = a.style.shape || "";
var padding = 32 + a.style.stroke*2;
var path_svg;
var fill = "";
@ -303,13 +310,13 @@ var SpacedeckBoardArtifacts = {
fill = "fill:none";
} else {
path_svg = render_vector_shape(a, padding);
fill = "fill:"+a.fill_color+";";
fill = "fill:"+a.style.fill_color+";";
padding = 0;
}
var margin = padding;
var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='"+(a.w+2*padding)+"' height='"+(a.h+2*padding)+"' ";
svg += "style='margin-left:"+(-margin)+"px;margin-top:"+(-margin)+"px;stroke-width:"+a.stroke+";stroke:"+a.stroke_color+";"+fill+"'>";
var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='"+(a.board.w+2*padding)+"' height='"+(a.board.h+2*padding)+"' ";
svg += "style='margin-left:"+(-margin)+"px;margin-top:"+(-margin)+"px;stroke-width:"+a.style.stroke+";stroke:"+a.style.stroke_color+";"+fill+"'>";
svg += path_svg;
svg += "</svg>";
@ -322,10 +329,10 @@ var SpacedeckBoardArtifacts = {
if (arts.length==0) return null;
r = {
x1: parseInt(_.min(arts.map(function(a){return a.x}))),
y1: parseInt(_.min(arts.map(function(a){return a.y}))),
x2: parseInt(_.max(arts.map(function(a){return a.x+a.w}))),
y2: parseInt(_.max(arts.map(function(a){return a.y+a.h})))
x1: parseInt(_.min(arts.map(function(a){return a.board.x}))),
y1: parseInt(_.min(arts.map(function(a){return a.board.y}))),
x2: parseInt(_.max(arts.map(function(a){return a.board.x+a.board.w}))),
y2: parseInt(_.max(arts.map(function(a){return a.board.y+a.board.h})))
};
r.x=r.x1;
r.y=r.y1;
@ -349,7 +356,7 @@ var SpacedeckBoardArtifacts = {
artifacts_in_rect: function(rect) {
return _.filter(this.active_space_artifacts, function(a) {
return this.rects_intersecting(a, rect);
return this.rects_intersecting(a.board, rect);
}.bind(this));
},
@ -359,15 +366,15 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
var overlapping = _.filter(this.artifacts_in_rect(rect), function(a){return !this.is_selected(a)}.bind(this));
var max_z = _.max(overlapping,function(a){ return a.z; });
var max_z = _.max(overlapping,function(a){ return a.board.z; });
if (max_z.board) {
max_z = max_z.z + 1;
max_z = max_z.board.z + 1;
} else {
max_z = 1;
}
this.update_selected_artifacts(function(a) {
return { z: max_z };
return { board: _.extend(a.board, { z: max_z }) };
});
},
@ -377,15 +384,15 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
var overlapping = _.filter(this.artifacts_in_rect(rect), function(a){return !this.is_selected(a);}.bind(this));
var min_z = _.min(overlapping,function(a){ return a.z; });
var min_z = _.min(overlapping,function(a){ return (a.board?a.board.z:0); });
if (min_z.board) {
min_z = min_z.z - 1;
min_z = min_z.board.z - 1;
} else {
min_z = 0;
}
var my_z = _.max(this.selected_artifacts(),function(a){ return a.z; });
var my_z = _.max(this.selected_artifacts(),function(a){ (a.board?a.board.z:0); });
if (my_z.board) {
my_z = my_z.z - 1;
my_z = my_z.board.z - 1;
} else {
my_z = 0;
}
@ -393,14 +400,14 @@ var SpacedeckBoardArtifacts = {
// TODO: move all other items up in this case?
if (min_z < 0) {
this.update_artifacts(overlapping, function(a) {
return { z: (my_z + a.z + 1) };
return { board: _.extend(a.board, { z: (my_z + (a.board?a.board.z:0) + 1) }) };
});
return;
}
this.update_selected_artifacts(function(a) {
return { z: min_z };
return { board: _.extend(a.board, { z: min_z }) };
});
},
@ -409,7 +416,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { x: rect.x1 };
return { board: _.extend(a.board, { x: rect.x1 }) };
});
},
@ -418,7 +425,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { y: rect.y1 };
return { board: _.extend(a.board, { y: rect.y1 }) };
});
},
@ -427,7 +434,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { x: rect.x2 - a.w };
return { board: _.extend(a.board, { x: rect.x2 - a.board.w }) };
});
},
@ -436,7 +443,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { y: rect.y2 - a.h };
return { board: _.extend(a.board, { y: rect.y2 - a.board.h }) };
});
},
@ -446,7 +453,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
var cx = rect.x1 + (rect.x2-rect.x1)/2;
this.update_selected_artifacts(function(a) {
return { x: cx - a.w/2 };
return { board: _.extend(a.board, { x: cx - a.board.w/2 }) };
});
},
@ -456,7 +463,7 @@ var SpacedeckBoardArtifacts = {
var rect = this.artifact_selection_rect();
var cy = rect.y1 + (rect.y2-rect.y1)/2;
this.update_selected_artifacts(function(a) {
return { y: cy - a.h/2 };
return { board: _.extend(a.board, { y: cy - a.board.h/2 }) };
});
},
@ -466,11 +473,11 @@ var SpacedeckBoardArtifacts = {
var arts = this.selected_artifacts();
if (arts.length<2) return;
var totalw = _.reduce(arts, function(sum, a) { return sum + a.w }, 0);
var totalw = _.reduce(arts, function(sum, a) { return sum + a.board.w }, 0);
var avgw = totalw / arts.length;
this.update_selected_artifacts(function(a) {
return { w: avgw };
return { board: _.extend(a.board, { w: avgw }) };
});
},
@ -480,11 +487,11 @@ var SpacedeckBoardArtifacts = {
var arts = this.selected_artifacts();
if (arts.length<2) return;
var totalh = _.reduce(arts, function(sum, a) { return sum + a.h }, 0);
var totalh = _.reduce(arts, function(sum, a) { return sum + a.board.h }, 0);
var avgh = totalh / arts.length;
this.update_selected_artifacts(function(a) {
return { h: avgh };
return { board: _.extend(a.board, { h: avgh }) };
});
},
@ -499,16 +506,16 @@ var SpacedeckBoardArtifacts = {
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.x });
var startx = sorted[0].x + sorted[0].w/2;
var stopx = _.last(sorted).x + _.last(sorted).w/2;
var sorted = _.sortBy(selected, function(a) { return a.board.x });
var startx = sorted[0].board.x + sorted[0].board.w/2;
var stopx = _.last(sorted).board.x + _.last(sorted).board.w/2;
var step = (stopx-startx)/(sorted.length-1);
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var x = startx + step*i - a.w/2;
var x = startx + step*i - a.board.w/2;
this.update_artifacts([a],function(a) {
return { x: x }
return { board: _.extend(a.board, {x: x}) }
});
}
},
@ -519,16 +526,16 @@ var SpacedeckBoardArtifacts = {
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.y });
var starty = sorted[0].y + sorted[0].h/2;
var stopy = _.last(sorted).y + _.last(sorted).h/2;
var sorted = _.sortBy(selected, function(a) { return a.board.y });
var starty = sorted[0].board.y + sorted[0].board.h/2;
var stopy = _.last(sorted).board.y + _.last(sorted).board.h/2;
var step = (stopy-starty)/(sorted.length-1);
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var y = starty + step*i - a.h/2;
var y = starty + step*i - a.board.h/2;
this.update_artifacts([a],function(a) {
return { y: y }
return { board: _.extend(a.board, {y: y}) }
});
}
},
@ -539,21 +546,21 @@ var SpacedeckBoardArtifacts = {
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.x });
var startx = sorted[0].x;
var stopx = _.last(sorted).x + _.last(sorted).w;
var sorted = _.sortBy(selected, function(a) { return a.board.x });
var startx = sorted[0].board.x;
var stopx = _.last(sorted).board.x + _.last(sorted).board.w;
var range = stopx - startx;
var totalw = _.reduce(sorted, function(sum, a) { return sum + a.w }, 0);
var totalw = _.reduce(sorted, function(sum, a) { return sum + a.board.w }, 0);
var avgs = (range - totalw) / (sorted.length-1);
var prevend = startx + sorted[0].w;
var prevend = startx + sorted[0].board.w;
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var x = prevend + avgs;
this.update_artifacts([a],function(a) {
return { x: x }
return { board: _.extend(a.board, {x: x}) }
});
prevend = x+a.w;
prevend = x+a.board.w;
}
},
@ -563,21 +570,21 @@ var SpacedeckBoardArtifacts = {
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.y });
var starty = sorted[0].y;
var stopy = _.last(sorted).y + _.last(sorted).h;
var sorted = _.sortBy(selected, function(a) { return a.board.y });
var starty = sorted[0].board.y;
var stopy = _.last(sorted).board.y + _.last(sorted).board.h;
var range = stopy - starty;
var totalh = _.reduce(sorted, function(sum, a) { return sum + a.h }, 0);
var totalh = _.reduce(sorted, function(sum, a) { return sum + a.board.h }, 0);
var avgs = (range - totalh) / (sorted.length-1);
var prevend = starty + sorted[0].h;
var prevend = starty + sorted[0].board.h;
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var y = prevend + avgs;
this.update_artifacts([a],function(a) {
return { y: y }
return { board: _.extend(a.board, {y: y}) }
});
prevend = y+a.h;
prevend = y+a.board.h;
}
},
@ -587,22 +594,20 @@ 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.width }.bind(this));
var sorted = _.sortBy(selected, function(a) { return a.board.x+a.board.y*this.active_space.advanced.width }.bind(this));
var minx = sorted[0].x;
var miny = sorted[0].y;
var minx = sorted[0].board.x;
var miny = sorted[0].board.y;
var sorted = _.sortBy(selected, function(a) { return -Math.max(a.w,a.h) }.bind(this));
var sorted = _.sortBy(selected, function(a) { return -Math.max(a.board.w,a.board.h) }.bind(this));
var blocks = [];
var padding = 10;
for (var i=0; i<sorted.length; i++) {
var a = sorted[i];
blocks.push({
w: a.w+padding,
h: a.h+padding,
w: a.board.w,
h: a.board.h,
a: a
});
}
@ -615,10 +620,10 @@ var SpacedeckBoardArtifacts = {
if (block.fit) {
var a = block.a;
this.update_artifacts([a],function(a) {
return {
return { board: _.extend(a.board, {
x: minx+block.fit.x,
y: miny+block.fit.y
}
}) }
});
}
}

View File

@ -170,6 +170,7 @@ var SpacedeckRoutes = {
location.href = "/";
} else {
this.active_view = "account";
this.load_subscription();
}
}.bind(this)
}
@ -252,6 +253,8 @@ 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;
@ -267,6 +270,35 @@ 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

@ -63,8 +63,8 @@ var SpacedeckSections = {
active_style: {
border_radius: 0,
stroke: 0,
font_family: "Inter",
font_size: 36,
font_family: "Avenir W01",
font_size: 18,
line_height: 1.5,
letter_spacing: 0,
@ -110,30 +110,18 @@ var SpacedeckSections = {
color_picker_opacity: 255,
swatches: [
{id:1, hex:"#ff00ff"},
{id:2, hex:"#ffff00"},
{id:3, hex:"#00ffff"},
{id:5, hex:"#ff0000"},
{id:6, hex:"#00ff00"},
{id:7, hex:"#0000ff"},
{id:8, hex:"#000000"},
{id:9, hex:"#222222"},
{id:10, hex:"#444444"},
{id:11, hex:"#888888"},
{id:12, hex:"#bbbbbb"},
{id:13, hex:"#dddddd"},
{id:14, hex:"#ffffff"},
{id:20, hex:"#4a2f7e"},
{id:21, hex:"#9b59b6"},
{id:22, hex:"#3498db"},
{id:23, hex:"#2ecc71"},
{id:24, hex:"#f1c40f"},
{id:25, hex:"#e67e22"},
{id:26, hex:"#d55c4b"},
{id:27, hex:"#6f4021"},
{id:29, hex:"#95a5a6"},
{id:30, hex:"rgba(0,0,0,0)"},
{id:0, hex:"#4a2f7e"},
{id:1, hex:"#9b59b6"},
{id:2, hex:"#3498db"},
{id:3, hex:"#2ecc71"},
{id:4, hex:"#f1c40f"},
{id:5, hex:"#e67e22"},
{id:6, hex:"#d55c4b"},
{id:7, hex:"#6f4021"},
{id:8, hex:"#ffffff"},
{id:9, hex:"#95a5a6"},
{id:10, hex:"#252525"},
{id:11, hex:"rgba(0,0,0,0)"},
],
swatches_text: [
@ -148,8 +136,18 @@ var SpacedeckSections = {
],
fonts: [
"Inter",
"Courier"
"Arial",
"Courier",
"Georgia",
"Verdana",
"Comic Sans MS",
"Montserrat",
"Lato",
"Roboto",
"Crimson Text",
"EB Garamond",
"Vollkorn",
"Avenir W01"
],
detected_text_formats: {},
@ -182,7 +180,7 @@ var SpacedeckSections = {
toolbar_props_in: false,
toolbar_artifacts_x: "-1000px",
toolbar_artifacts_y: "-1000px",
toolbar_artifacts_in: true
toolbar_artifacts_in: false
},
methods: {
@ -371,8 +369,8 @@ var SpacedeckSections = {
// canvas
this.$watch('active_style.background_color', function (value, mutation) {
if (this.active_style.background_color != this.active_space.background_color) {
this.$set("active_space.background_color",this.active_style.background_color);
if (this.active_style.background_color != this.active_space.advanced.background_color) {
this.$set("active_space.advanced.background_color",this.active_style.background_color);
this.throttled_save_active_space();
}
@ -450,7 +448,7 @@ var SpacedeckSections = {
for (var i=0; i<props.length; i++) {
var prop = props[i];
this.active_style[prop]=a[prop];
this.active_style[prop]=a.style[prop];
}
// defaults
@ -459,10 +457,10 @@ var SpacedeckSections = {
this.active_style.line_height = this.default_style.line_height;
this.active_style.letter_spacing = this.default_style.letter_spacing;
this.active_style.padding_top = a.padding_top || 0;
this.active_style.padding_bottom = a.padding_bottom || 0;
this.active_style.padding_left = a.padding_left || 0;
this.active_style.padding_right = a.padding_right || 0;
this.active_style.padding_top = a.style.padding_top || 0;
this.active_style.padding_bottom = a.style.padding_bottom || 0;
this.active_style.padding_left = a.style.padding_left || 0;
this.active_style.padding_right = a.style.padding_right || 0;
if (this.active_style.padding_top == this.active_style.padding_bottom) {
this.active_style.padding_vert = this.active_style.padding_top;
@ -478,10 +476,10 @@ var SpacedeckSections = {
this.active_style.padding = this.active_style.padding_top;
}
this.active_style.margin_top = a.margin_top || 0;
this.active_style.margin_bottom = a.margin_bottom || 0;
this.active_style.margin_left = a.margin_left || 0;
this.active_style.margin_right = a.margin_right || 0;
this.active_style.margin_top = a.style.margin_top || 0;
this.active_style.margin_bottom = a.style.margin_bottom || 0;
this.active_style.margin_left = a.style.margin_left || 0;
this.active_style.margin_right = a.style.margin_right || 0;
if (this.active_style.margin_top == this.active_style.margin_bottom) {
this.active_style.margin_vert = this.active_style.margin_top;
@ -760,8 +758,8 @@ var SpacedeckSections = {
},
resize_minimap: function() {
if (!this.active_space) return;
this.minimap_scale = this.active_space.width/100.0;
if (!this.active_space || !this.active_space.advanced) return;
this.minimap_scale = this.active_space.advanced.width/100.0;
},
handle_minimap_mouseup: function(evt) {
@ -923,7 +921,7 @@ var SpacedeckSections = {
discover_zones: function() {
this.zones = _.sortBy(_.filter(this.active_space_artifacts, function(a) { return (a.mime=="x-spacedeck/zone") }),
function(z){return z.order});
function(z){return z.style.order});
},
artifact_plaintext: function(a) {
@ -1017,10 +1015,10 @@ var SpacedeckSections = {
arts = _.filter(arts); // remove any nulls
return {
x1: parseInt(_.min(arts.map(function(a){return ((!a || !a.x)?0:a.x)}))),
y1: parseInt(_.min(arts.map(function(a){return ((!a || !a.y)?0:a.y)}))),
x2: parseInt(_.max(arts.map(function(a){return (!a?0:a.x+a.w)}))),
y2: parseInt(_.max(arts.map(function(a){return (!a?0:a.y+a.h)})))
x1: parseInt(_.min(arts.map(function(a){return ((!a.board || !a.board.x)?0:a.board.x)}))),
y1: parseInt(_.min(arts.map(function(a){return ((!a.board || !a.board.y)?0:a.board.y)}))),
x2: parseInt(_.max(arts.map(function(a){return (!a.board?0:a.board.x+a.board.w)}))),
y2: parseInt(_.max(arts.map(function(a){return (!a.board?0:a.board.y+a.board.h)})))
};
},
@ -1059,7 +1057,7 @@ var SpacedeckSections = {
this.toolbar_props_x = pp.x+"px";
this.toolbar_props_y = pp.y+"px";
//this.hide_toolbar_artifacts();
this.hide_toolbar_artifacts();
}
this.selection_metrics.x1 = sr.x1;
@ -1078,7 +1076,7 @@ var SpacedeckSections = {
this.selection_metrics.count=arts.length;
this.selection_metrics.scribble_selection = false;
if (arts.length == 1 && arts[0].mime == "x-spacedeck/vector") {
if (arts[0].shape == "scribble") {
if (arts[0].style.shape == "scribble") {
this.selection_metrics.scribble_selection = true;
}
this.selection_metrics.vector_points = arts[0].control_points;
@ -1114,8 +1112,8 @@ var SpacedeckSections = {
fixup_space_size: function() {
if (!this.active_space) return;
this.active_space.width =Math.max(this.active_space.width, window.innerWidth);
this.active_space.height=Math.max(this.active_space.height, window.innerHeight);
this.active_space.advanced.width =Math.max(this.active_space.advanced.width, window.innerWidth);
this.active_space.advanced.height=Math.max(this.active_space.advanced.height, window.innerHeight);
},
end_transaction: function() {
@ -1127,16 +1125,13 @@ var SpacedeckSections = {
var er = this.enclosing_rect(this.active_space_artifacts);
if (!er) return;
// resize space
this.active_space.width =Math.max((parseInt(er.x2/window.innerWidth)+2)*window.innerWidth, window.innerWidth);
this.active_space.height=Math.max((parseInt(er.y2/window.innerHeight)+2)*window.innerHeight, window.innerHeight);
this.active_space.advanced.width =Math.max(er.x2+100, window.innerWidth);
this.active_space.advanced.height=Math.max(er.y2+100, window.innerHeight);
console.log("bounds: ",this.active_space.width,this.active_space.height);
if (this._last_bounds_width != this.active_space.width ||
this._last_bounds_height != this.active_space.height) {
this._last_bounds_width = this.active_space.width;
this._last_bounds_height = this.active_space.height;
if (this._last_bounds_width != this.active_space.advanced.width ||
this._last_bounds_height != this.active_space.advanced.height) {
this._last_bounds_width = this.active_space.advanced.width;
this._last_bounds_height = this.active_space.advanced.height;
save_space(this.active_space);
}
@ -1219,7 +1214,7 @@ var SpacedeckSections = {
// this is a bit hacky, but might be the smartest place to do it
if (a.view && a.view.vector_svg) {
a.shape_svg = a.view.vector_svg;
a.style.shape_svg = a.view.vector_svg;
}
window.artifact_save_queue[a._id] = a;
@ -1334,7 +1329,7 @@ var SpacedeckSections = {
this.update_selected_artifacts(function(a) {
var c = {};
if (a[prop] != val) {
if (c[prop] != val) {
//console.log("set_artifact_prop: ",c,val);
c[prop]=val;
return c;
@ -1348,11 +1343,11 @@ var SpacedeckSections = {
this.begin_transaction();
this.update_selected_artifacts(function(a) {
var c = {};
var c = {style: a.style||{}};
if (a[prop] != val) {
if (c.style[prop] != val) {
//console.log("set_artifact_style_prop: ",c,val);
c[prop]=val;
c.style[prop]=val;
return c;
}
@ -1424,7 +1419,7 @@ var SpacedeckSections = {
if (this.selected_artifacts().length!=1 && this.opened_dialog!="background") return;
if (this.opened_dialog=="background") {
this.active_style[this.color_picker_target] = this.active_space.background_color;
this.active_style[this.color_picker_target] = this.active_space.advanced.background_color;
} else {
if (!this.active_style[this.color_picker_target]) {
this.active_style[this.color_picker_target] = this.default_style[this.color_picker_target];
@ -1483,8 +1478,10 @@ var SpacedeckSections = {
this.update_selected_artifacts(function(a) {
return {
x: a.x+dx,
y: a.y+dy
board: _.extend(a.board, {
x: a.board.x+dx,
y: a.board.y+dy
})
};
});
},
@ -1492,7 +1489,7 @@ var SpacedeckSections = {
/* -------------------------------------------------------------------- */
highest_z: function() {
var z = _.max(this.active_space_artifacts.map(function(a){return a.z||0}));
var z = _.max(this.active_space_artifacts.map(function(a){return a.board.z||0}));
if (z<0) z=0;
if (z>999) z=999;
return z;
@ -1549,7 +1546,7 @@ var SpacedeckSections = {
add_artifact: function (space, item_type, url, evt) {
this.active_tool = "pointer";
this.mouse_state = "idle";
//this.hide_toolbar_artifacts();
this.hide_toolbar_artifacts();
if (!url && (item_type == 'image' || item_type == 'video' || item_type == 'embed')) {
url = prompt("URL?");
@ -1577,18 +1574,20 @@ var SpacedeckSections = {
payload_thumbnail_web_uri: url || null,
space_id: space._id,
style: {
order: this.active_space_artifacts.length+1,
valign: "middle",
align: "center"
//fill_color: "#f8f8f8"
}
};
if (mimes[item_type] == "text/html") {
new_item.padding_left = 10;
new_item.padding_top = 10;
new_item.padding_right = 10;
new_item.padding_bottom = 10;
new_item.fill_color = "rgba(255,255,255,1)";
new_item.style.padding_left = 10;
new_item.style.padding_top = 10;
new_item.style.padding_right = 10;
new_item.style.padding_bottom = 10;
new_item.style.fill_color = "rgba(255,255,255,1)";
new_item.description = "<p>Text</p>";
}
@ -1601,11 +1600,13 @@ var SpacedeckSections = {
z = point.z;
}
new_item.x = parseInt(point.x);
new_item.y = parseInt(point.y);
new_item.z = z;
new_item.w = w;
new_item.h = h;
new_item.board = {
x: parseInt(point.x),
y: parseInt(point.y),
w: w,
h: h,
z: z
};
if (this.guest_nickname) {
new_item.editor_name = this.guest_nickname;
@ -1664,7 +1665,7 @@ var SpacedeckSections = {
for (var i=0; i<new_zones.length; i++) {
if (new_zones[i]) {
if (!new_zones[i].style) new_zones[i].style = {};
new_zones[i].order = i;
new_zones[i].style.order = i;
save_artifact(new_zones[i]);
}
}
@ -1678,7 +1679,7 @@ var SpacedeckSections = {
for (var i=0; i<new_zones.length; i++) {
if (new_zones[i]) {
if (!new_zones[i].style) new_zones[i].style = {};
new_zones[i].order = i;
new_zones[i].style.order = i;
save_artifact(new_zones[i]);
}
}
@ -1694,13 +1695,17 @@ var SpacedeckSections = {
space_id: this.active_space._id,
mime: "x-spacedeck/zone",
description: "Zone "+(this.zones.length+1),
board: {
x: point.x,
y: point.y,
w: w,
h: h,
z: 0,
z: 0
},
style: {
valign: "middle",
align: "center"
}
};
if (this.guest_nickname) {
@ -1729,19 +1734,23 @@ var SpacedeckSections = {
var a = {
space_id: this.active_space._id,
mime: "x-spacedeck/shape",
description: "",
description: "Text",
board: {
x: point.x,
y: point.y,
z: point.z,
w: w,
h: h,
h: h
},
style: {
stroke_color: "#ffffff",
text_color: "#ffffff",
stroke: 0,
fill_color: "#000000",
shape: shape_type,
valign: "middle",
align: "center",
align: "center"
}
};
if (this.guest_nickname) {
@ -1794,6 +1803,8 @@ var SpacedeckSections = {
return false;
}
this.hide_toolbar_artifacts();
// 1. create placeholder artifact
var w=300,h=150;
var fill="transparent";
@ -1818,14 +1829,18 @@ var SpacedeckSections = {
state: "uploading",
payload_thumbnail_medium_uri: null,
payload_thumbnail_web_uri: null,
board: {
x: point.x,
y: point.y,
w: w,
h: h,
z: point.z,
z: point.z
},
style: {
order: this.active_space_artifacts.length+1,
fill_color: fill
}
}
this.update_board_artifact_viewmodel(a);
@ -1849,11 +1864,7 @@ var SpacedeckSections = {
a.payload_thumbnail_big_uri = updated_a.payload_thumbnail_big_uri;
a.payload_alternatives = updated_a.payload_alternatives;
a.mime = updated_a.mime;
a.x = updated_a.x;
a.y = updated_a.y;
a.w = updated_a.w;
a.h = updated_a.h;
a.z = updated_a.z;
a.board = updated_a.board;
a.state = updated_a.state;
this.update_board_artifact_viewmodel(a);
@ -1991,26 +2002,26 @@ var SpacedeckSections = {
clear_formatting_walk: function(el,cmd,arg1,arg2) {
if (el && el.style) {
if (cmd == "preciseFontSize") {
el.fontSize = null;
el.style.fontSize = null;
} else if (cmd == "letterSpacing") {
el.letterSpacing = null;
el.style.letterSpacing = null;
} else if (cmd == "lineHeight") {
el.lineHeight = null;
el.style.lineHeight = null;
} else if (cmd == "fontName") {
el.fontFamily = null;
el.style.fontFamily = null;
} else if (cmd == "fontWeight") {
el.fontWeight = null;
el.fontStyle = null;
el.style.fontWeight = null;
el.style.fontStyle = null;
} else if (cmd == "bold") {
el.fontWeight = null;
el.style.fontWeight = null;
} else if (cmd == "italic") {
el.fontStyle = null;
el.style.fontStyle = null;
} else if (cmd == "underline") {
el.textDecoration = null;
el.style.textDecoration = null;
} else if (cmd == "strikeThrough") {
el.textDecoration = null;
el.style.textDecoration = null;
} else if (cmd == "forecolor") {
el.color = null;
el.style.color = null;
}
}
@ -2097,9 +2108,6 @@ var SpacedeckSections = {
if (a.description!=dom.innerHTML) {
a.description = dom.innerHTML;
console.log("new DOM:",dom.innerHTML);
this.update_board_artifact_viewmodel(a);
this.queue_artifact_for_save(a);
@ -2133,7 +2141,10 @@ var SpacedeckSections = {
remove_link_from_selected_artifacts: function() {
this.update_selected_artifacts(function(a) {
return {link_uri: ""};
var meta = a.meta || {};
delete meta.link_uri;
return {meta: meta};
});
},
@ -2149,7 +2160,9 @@ var SpacedeckSections = {
var insert_link_url = prompt("URL:",def);
this.update_selected_artifacts(function(a) {
var update = {link_uri: insert_link_url};
var meta = a.meta || {};
meta.link_uri = insert_link_url;
var update = {meta: meta};
if (a.payload_uri && a.payload_uri.match("webgrabber")) {
var enc_uri = encodeURIComponent(btoa(insert_link_url));
@ -2172,10 +2185,11 @@ var SpacedeckSections = {
delete copy["$index"];
delete copy["_id"];
if (dx) copy.x += dx;
if (dy) copy.y += dy;
if (dx) copy.board.x += dx;
if (dy) copy.board.y += dy;
copy.order = this.active_space_artifacts.length+1;
if (!copy.style) copy.style = {};
copy.style.order = this.active_space_artifacts.length+1;
if (this.guest_nickname) {
copy.editor_name = this.guest_nickname;
@ -2296,6 +2310,11 @@ var SpacedeckSections = {
if (!pastedText) return;
if (!pastedText.match(/<[a-zA-Z]+>/g)) {
// crappy heuristic if this is actually HTML
pastedText = pastedText.replace(/\n/g,"<br>");
}
this.insert_embedded_artifact(pastedText);
},
@ -2315,16 +2334,16 @@ var SpacedeckSections = {
if (parsed[i].mime) {
var z = this.highest_z()+1;
if (parsed.length==1) {
var w = parsed[i].w;
var h = parsed[i].h;
var w = parsed[i].board.w;
var h = parsed[i].board.h;
var point = this.find_place_for_item(w,h);
parsed[i].x = point.x;
parsed[i].y = point.y;
parsed[i].z = point.z;
parsed[i].board.x = point.x;
parsed[i].board.y = point.y;
parsed[i].board.z = point.z;
} else {
parsed[i].x = parsed[i].x+50;
parsed[i].y = parsed[i].y+50;
parsed[i].y = parsed[i].z+z;
parsed[i].board.x = parsed[i].board.x+50;
parsed[i].board.y = parsed[i].board.y+50;
parsed[i].board.y = parsed[i].board.z+z;
}
this.clone_artifact(parsed[i], 0,0, function(a) {
this.multi_select([a]);
@ -2342,6 +2361,34 @@ var SpacedeckSections = {
this.create_artifact_via_embed_url(text);
return;
}
var new_item = {
mime: "text/html",
description: text.replace("\n", "<br />"),
title: "",
space_id: space._id
};
var w = 400;
var h = 300;
var point = this.find_place_for_item(w,h);
new_item.board = {
x: point.x,
y: point.y,
w: w,
h: h,
z: point.z
};
if (this.guest_nickname) {
new_item.editor_name = this.guest_nickname;
}
save_artifact(new_item, function(saved_item) {
this.update_board_artifact_viewmodel(saved_item);
this.active_space_artifacts.push(saved_item);
}.bind(this));
},
create_artifact_via_embed_url: function(url) {
@ -2355,13 +2402,17 @@ var SpacedeckSections = {
mime: "image/png",
description: url,
state: "uploading",
board: {
x: point.x,
y: point.y,
w: 200,
h: 200,
z: z,
z: z
},
style: {
order: this.active_space_artifacts.length
}
}
var metadata = parse_link(url)
@ -2422,12 +2473,16 @@ var SpacedeckSections = {
payload_thumbnail_medium_uri: metadata.thumbnail_url,
payload_thumbnail_web_uri: metadata.thumbnail_url,
state: "idle",
meta: {
title: metadata.title,
link_uri: metadata.url || url,
link_uri: metadata.url || url
},
board: {
x: point.x - w/2,
y: point.y - h/2,
w: w,
h: h
}
});
if (this.guest_nickname) {
@ -2500,11 +2555,20 @@ var SpacedeckSections = {
this.opened_dialog = "none";
if (files && files.length) {
console.log("file: ",files[0]);
for (var i=0; i<files.length; i++) {
var file = files[i];
if (file.type === "application/pdf") {
var point = {x: 100, y: 100}; //fixme, center upload?
this.dropped_point = point;
this.pending_pdf_file = file;
this.activate_modal('pdfoptions');
} else {
this.create_artifact_via_upload(null, file, true);
}
}
}
},
handle_image_file_upload: function(evt) {
@ -2527,7 +2591,7 @@ var SpacedeckSections = {
},
remove_section_background: function() {
this.active_space.background_uri = null;
this.active_space.advanced.background_uri = null;
save_space(this.active_space);
},
@ -2541,11 +2605,12 @@ var SpacedeckSections = {
},
hide_toolbar_props: function() {
// FIXME test
//this.toolbar_props_in = false;
this.toolbar_props_in = false;
},
show_toolbar_artifacts: function(x,y) {
this.toolbar_artifacts_x = (x-175)+"px";
this.toolbar_artifacts_y = y+"px";
this.toolbar_artifacts_in = true;
},
@ -2555,19 +2620,29 @@ var SpacedeckSections = {
start_adding_artifact: function(evt) {
evt = fixup_touches(evt);
// toggle
if (this.toolbar_artifacts_in) {
this.hide_toolbar_artifacts();
return;
}
this.show_toolbar_artifacts(evt.pageX,evt.pageY);
},
start_drawing_scribble: function(evt) {
this.hide_toolbar_artifacts();
this.active_tool = "scribble";
this.opened_dialog = "none";
},
start_drawing_arrow: function(evt) {
this.hide_toolbar_artifacts();
this.active_tool = "arrow";
this.opened_dialog = "none";
},
start_drawing_line: function(evt) {
this.hide_toolbar_artifacts();
this.active_tool = "line";
this.opened_dialog = "none";
},
@ -2577,8 +2652,8 @@ var SpacedeckSections = {
this.bounds_zoom = this.viewport_zoom;
var eff_w = this.active_space.width*this.viewport_zoom;
var eff_h = this.active_space.height*this.viewport_zoom;
var eff_w = this.active_space.advanced.width*this.viewport_zoom;
var eff_h = this.active_space.advanced.height*this.viewport_zoom;
if (window.innerWidth>eff_w) {
// horizontal centering
@ -2771,8 +2846,8 @@ var SpacedeckSections = {
var el = $("#space")[0];
var eff_w = this.active_space.width*this.viewport_zoom;
var eff_h = this.active_space.height*this.viewport_zoom;
var eff_w = this.active_space.advanced.width*this.viewport_zoom;
var eff_h = this.active_space.advanced.height*this.viewport_zoom;
var sx = el.scrollLeft;
var sy = el.scrollTop;
@ -2846,6 +2921,32 @@ var SpacedeckSections = {
}.bind(this),500);
},
approve_pdf_upload: function(evt,approve_pdf_upload, mode){
this.close_modal();
if(mode == "classic"){
this.create_artifact_via_upload(evt, this.pending_pdf_file, false);
}
if(mode == "grid") {
this.global_spinner = true;
save_pdf_file(this.active_space, this.dropped_point, this.pending_pdf_file, approve_pdf_upload, function(createdArtifacts){
this.global_spinner = false;
_.each(createdArtifacts, function(new_artifact){
this.update_board_artifact_viewmodel(new_artifact);
this.active_space_artifacts.push(new_artifact)
}.bind(this));
}.bind(this), function(xhr) {
this.global_spinner = false;
alert("Error PDF ("+xhr.status+")");
}.bind(this));
}
},
handle_data_drop: function(evt) {
if (this.active_space_role=="viewer") {
return false;
@ -2858,8 +2959,17 @@ var SpacedeckSections = {
if (files && files.length) {
for (var i=0; i<files.length; i++) {
var file = files[i];
if (file.type === "application/pdf") {
var point = this.cursor_point_to_space(evt);
this.dropped_point = point;
this.pending_pdf_file = file;
this.activate_modal('pdfoptions');
} else {
this.create_artifact_via_upload(evt, file, (files.length>1));
}
}
} else {
var json = evt.dataTransfer.getData('application/json');
@ -2870,9 +2980,9 @@ var SpacedeckSections = {
var w = 300;
var h = 200;
if (parsed.board && parsed.w && parsed.h) {
w = parsed.w;
h = parsed.h;
if (parsed.board && parsed.board.w && parsed.board.h) {
w = parsed.board.w;
h = parsed.board.h;
}
var point = this.cursor_point_to_space(evt);

View File

@ -18,6 +18,8 @@ var SpacedeckSpaces = {
active_space_path: [],
access_settings_space: null,
access_settings_memberships: [],
duplicate_folders: [],
duplicate_folder_id: "",
pending_pdf_files: [],
meta_visible: false,
@ -69,7 +71,9 @@ 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));
},
@ -81,7 +85,14 @@ var SpacedeckSpaces = {
location.reload();
},
ask_guestname: function(dft, cb) {
smoke.prompt(__('what_is_your_name', "Spacedeck") , function(content) {
console.log("ask_guestname");
var team_name = "Spacedeck";
if(subdomainTeam) {
team_name = subdomainTeam.name;
}
smoke.prompt(__('what_is_your_name', team_name) , function(content) {
if (!content || (content.length === 0)) {
this.ask_guestname(dft, cb);
} else {
@ -90,7 +101,7 @@ var SpacedeckSpaces = {
if ("localStorage" in window && localStorage) {
try {
localStorage['guest_nickname'] = this.guest_nickname;
} catch(e) {
}catch(e) {
console.error(e);
}
}
@ -108,6 +119,27 @@ var SpacedeckSpaces = {
space_auth = get_query_param("spaceAuth");
var userReady = function() {
if (get_query_param("embedded")) {
this.embedded = true;
this.guest_signup_enabled = true;
if (get_query_param("publish_cta")) {
this.publish_cta = get_query_param("publish_cta");
}
if (get_query_param("nosocial")) {
this.social_bar = false;
}
}
if (get_query_param("confirm") && this.logged_in) {
var token = get_query_param("confirm");
confirm_user(this.user, token, function() {
this.redirect_to("/spaces/"+space_id+"?show_access=1");
}.bind(this), function() {
alert("An error occured confirming your email with the given token.");
});
return;
}
this.close_dropdown();
this.active_space_loaded = false;
@ -135,9 +167,12 @@ var SpacedeckSpaces = {
load_space(space_id, function(space, role) {
document.title = space.name;
this.active_space_role = role || "viewer"; // via req header from backend
this.active_space_role = role || "viewer"; //via req header from backend
this.space_embed_html = "<iframe width=\"1024\" height=\"768\" seamless src=\""+ENV.webEndpoint+"/spaces/"+space._id+"?embedded=1\"></iframe>";
if (!is_home) {
//console.log(space);
load_members(space, function(members) {
this.active_space_memberships = members;
}.bind(this));
@ -276,6 +311,15 @@ var SpacedeckSpaces = {
// FIXME
this.active_join_link = "";
this.join_link_role = "viewer";
// FIXME
if (this.active_space_role == "admin") {
this.space_info_section="access";
} else if (this.active_space_role == "editor") {
//this.space_info_section="versions";
} else {
this.space_info_section="info";
}
}
}.bind(this), function(xhr) {
@ -304,7 +348,7 @@ var SpacedeckSpaces = {
userReady();
}
if (!this.user && space_auth) {
if (space_auth) {
if (this.guest_nickname) {
userReady();
} else {
@ -592,14 +636,17 @@ var SpacedeckSpaces = {
download_space: function() {
smoke.quiz(__("download_space"), function(e, test) {
if (e == "PDF") {
if (e == "PDF"){
this.download_space_as_pdf(this.active_space);
} else if (e == "ZIP") {
}else if (e == "ZIP"){
this.download_space_as_zip(this.active_space);
}else if (e == "TXT"){
this.download_space_as_list(this.active_space);
}
}.bind(this), {
button_1: "PDF",
button_2: "ZIP",
button_3: "TXT",
button_cancel:__("cancel")
});
@ -639,6 +686,47 @@ var SpacedeckSpaces = {
location.href = "/api/spaces/" + space._id + "/list";
},
duplicate_space_into_folder: function() {
load_writable_folders( function(folders){
this.duplicate_folders = _.sortBy(folders, function (folder) { return folder.name; });
}.bind(this), function(xhr) {
console.error(xhr);
});
},
duplicate_folder_confirm: function() {
var folderId = this.duplicate_folder_id;
var idx = _.findIndex(this.duplicate_folders, function(s) { return s._id == folderId;});
if (idx<0) idx = 0;
var folder = this.duplicate_folders[idx];
console.log("df f",folder);
if (!folder) return;
duplicate_space(this.active_space, folder._id, function(new_space) {
this.duplicate_folders = [];
this.duplicate_folder = null;
smoke.quiz(__("duplicate_success", this.active_space.name, folder.name), function(e, test){
if (e == __("goto_space", new_space.name)){
this.redirect_to("/spaces/" + new_space._id);
}else if (e == __("goto_folder", folder.name)){
this.redirect_to("/folders/" + folder._id);
}
}.bind(this), {
button_1: __("goto_space", new_space.name),
button_2: __("goto_folder", folder.name),
button_cancel:__("stay_here")
});
}.bind(this), function(xhr){
console.error(xhr);
smoke.prompt("error: " + xhr.statusText);
}.bind(this));
},
toggle_follow_mode: function() {
this.deselect();
this.follow_mode = !this.follow_mode;
@ -744,12 +832,9 @@ var SpacedeckSpaces = {
this.invite_message = "";
}
}.bind(this), function(xhr){
try {
var res = JSON.parse(xhr.response);
alert("Error: "+res.error);
} catch (e) {
console.error(e, xhr);
}
text = JSON.stringify(xhr.responseText);
smoke.alert("Error: "+text);
}.bind(this));
}.bind(this));
},
@ -757,13 +842,9 @@ var SpacedeckSpaces = {
update_member: function(space, m, role) {
m.role = role;
save_membership(space, m, function() {
console.log("saved")
}.bind(this), function(xhr) {
try {
var res = JSON.parse(xhr.response);
alert("Error: "+res.error);
} catch (e) {
console.error(e, xhr);
}
console.error(xhr);
}.bind(this));
},
@ -772,12 +853,7 @@ var SpacedeckSpaces = {
delete_membership(space, m, function() {
this.access_settings_memberships.splice(this.access_settings_memberships.indexOf(m), 1);
}.bind(this), function(xhr) {
try {
var res = JSON.parse(xhr.response);
alert("Error: "+res.error);
} catch (e) {
console.error(e, xhr);
}
console.error(xhr);
}.bind(this));
},
@ -813,6 +889,10 @@ var SpacedeckSpaces = {
}.bind(this));
},
emojified_comment: function(comment) {
return twemoji.parse(comment);
},
set_folder_sorting: function(key,reverse) {
this.folder_sorting = key;
this.folder_reverse = reverse?-1:1;

View File

@ -11,13 +11,11 @@ SpacedeckUsers = {
login_email: "",
login_password: "",
signup_password: "",
signup_invite_code: "",
signup_password_confirmation: "",
account_remove_error: null,
loading_user: false,
password_reset_confirm_error: "",
password_reset_error: "",
password_reset_error: ""
},
methods:{
load_user: function(on_success, on_error) {
@ -42,7 +40,23 @@ SpacedeckUsers = {
}.bind(this));
},
login_google: function(evt) {
this.loading_user = true;
create_oauthtoken(function(data){
this.loading_user = false;
location.href = data.url;
}, function(xhr){
this.loading_user = false;
alert("could not get oauth token");
});
},
finalize_login: function(session_token, on_success) {
if(!window.socket_auth || window.socket_auth == '' || window.socket_auth == 'null') {
window.socket_auth = session_token;
}
this.load_user(function(user) {
if (this.invitation_token) {
accept_invitation(this.invitation_token, function(memberships){
@ -117,7 +131,7 @@ SpacedeckUsers = {
signup_guest: function(on_success) {
},
signup_submit: function($event, name, email, password, password_confirmation, invite_code, on_success) {
signup_submit: function($event, name, email, password, password_confirmation, on_success) {
this.creating_user = true;
this.signup_error = null;
@ -131,7 +145,7 @@ SpacedeckUsers = {
$event.stopPropagation();
}
create_user(name, email, password, password_confirmation, invite_code, function(session) {
create_user(name, email, password, password_confirmation, function(session) {
this.creating_user = false;
this.login_submit(email, password, null, on_success);
}.bind(this), function(req) {
@ -147,14 +161,15 @@ SpacedeckUsers = {
}.bind(this));
},
signup_submit_modal: function($event, name, email, password, password_confirmation, invite_code) {
this.signup_submit($event, name, email, password, password_confirmation, invite_code, function() {
signup_submit_modal: function($event, name, email, password, password_confirmation) {
this.signup_submit($event, name, email, password, password_confirmation, function() {
alert("Success.");
location.reload();
});
},
password_reset_submit: function(evt, email) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
@ -188,6 +203,7 @@ SpacedeckUsers = {
},
password_reset_confirm: function(evt, password, password_confirmation) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
@ -196,29 +212,27 @@ SpacedeckUsers = {
this.password_reset_confirm_error = null;
this.password_reset_send = false;
if (password != password_confirmation) {
if(password != password_confirmation) {
this.password_reset_confirm_error = "Passwords do not match.";
return;
}
if (password.length < 5) {
if(password.length < 5) {
this.password_reset_confirm_error = "Password too short (must have at least 5 characters).";
return;
}
confirm_password_reset(password, this.reset_token, function(parsed,req) {
if (req.status==201) {
alert("New password set successfully.");
if(req.status==201){
this.active_view = "login";
} else {
alert("An unknown error occured.");
}
}.bind(this), function(req) {
if (req.status==404) {
alert("Error: Unknown user.");
var msg = "user not found";
} else {
alert("Error: "+req.statusText);
var msg = "error: " + req.statusText;
}
this.password_reset_confirm_error = msg;
}.bind(this));
},

View File

@ -158,7 +158,7 @@ function boot_spacedeck() {
});
}
document.addEventListener("DOMContentLoaded",function() {
$(document).ready(function(){
window.smoke = smoke;
window.alert = smoke.alert;

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,13 +101,11 @@ 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: token,
auth_token: window.socket_auth,
space_id: space._id
};
console.log("[websocket] auth space");

View File

@ -80,16 +80,10 @@ function setup_whiteboard_directives() {
evt.stopPropagation();
}
if ($scope.active_tool == "zoom") return;
if (evt.which == 2) {
// middle mouse button
this.handle_mouse_down_space(evt);
return;
}
var a = $scope.find_artifact_by_id(evt.currentTarget.id.replace("artifact-",""));
if ($scope.active_tool == "zoom") return;
if ($scope.active_tool == "eyedrop") {
var arts = $scope.selected_artifacts();
if (!$scope.is_selected(a) && arts.length > 0) {
@ -202,9 +196,7 @@ function setup_whiteboard_directives() {
},
handle_mouse_down_space: function(evt) {
if (evt.which != 2) {
if (evt.target != evt.currentTarget && !_.include(["wrapper"],evt.target.className)) return;
}
var $scope = this.vm.$root;
@ -222,7 +214,7 @@ function setup_whiteboard_directives() {
this.deselect();
this.mouse_state = "transform";
$scope.mouse_state = this.mouse_state;
this.start_drawing_note(evt);
this.start_adding_note(evt);
return;
} else if ($scope.active_tool=="arrow") {
@ -339,7 +331,7 @@ function setup_whiteboard_directives() {
var $scope = this.vm.$root;
return _.filter($scope.active_space_artifacts, function(a) {
return this.rects_intersecting(a, rect);
return this.rects_intersecting(a.board, rect);
}.bind(this));
},
@ -447,15 +439,15 @@ function setup_whiteboard_directives() {
dists = $scope.unselected_artifacts().map(function(a){
var r = this.rect_to_points(a);
var r = this.rect_to_points(a.board);
var xd1 = Math.abs(r[0].x-x);
var xd2 = Math.abs(r[1].x-x);
var xd3 = Math.abs(r[0].x+a.w/2 - x);
var xd3 = Math.abs(r[0].x+a.board.w/2 - x);
var yd1 = Math.abs(r[0].y-y);
var yd2 = Math.abs(r[2].y-y);
var yd3 = Math.abs(r[0].y+a.h/2 - y);
var yd3 = Math.abs(r[0].y+a.board.h/2 - y);
if (!snap_middle) {
if (xd2<xd1) {
@ -477,10 +469,10 @@ function setup_whiteboard_directives() {
if (snap_middle) {
var xd = xd3;
var sx = r[0].x+a.w/2;
var sx = r[0].x+a.board.w/2;
var yd = yd3;
var sy = r[0].y+a.h/2;
var sy = r[0].y+a.board.h/2;
}
return [[xd,sx],[yd,sy]];
@ -500,7 +492,6 @@ function setup_whiteboard_directives() {
if (!xdists[0] || xdists[0][0]>TOL) {
results.snapx = [0,x]; // distance, coordinate
} else {
// FIXME snap rulers are broken
//$scope.snap_ruler_x = xdists[0][1];
}
if (!ydists[0] || ydists[0][0]>TOL) {
@ -525,41 +516,6 @@ function setup_whiteboard_directives() {
return point;
},
start_drawing_note: function(evt) {
evt.preventDefault();
evt.stopPropagation();
var $scope = this.vm.$root;
var point = this.cursor_point_to_space(evt);
this.offset_point_in_wrapper(point);
var z = $scope.highest_z()+1;
var a = {
space_id: $scope.active_space._id,
mime: "text/html",
description: "<p>Text</p>",
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64,
align: "center",
valign: "middle",
stroke_color: "#000000",
fill_color: "rgb(241, 196, 15)",
stroke: 0
};
$scope.save_artifact(a, function(saved_a) {
$scope.update_board_artifact_viewmodel(saved_a);
$scope.active_space_artifacts.push(saved_a);
$scope.select(evt,a);
$scope.transform_ox = 0;
$scope.transform_oy = 0;
$scope.begin_transaction();
}.bind(this));
},
start_drawing_scribble: function(evt) {
evt.preventDefault();
evt.stopPropagation();
@ -575,14 +531,18 @@ function setup_whiteboard_directives() {
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "scribble"
}
};
$scope.save_artifact(a, function(saved_a) {
@ -612,14 +572,18 @@ function setup_whiteboard_directives() {
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0},{dx:0,dy:0},{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "arrow"
}
};
$scope.save_artifact(a, function(saved_a) {
@ -648,14 +612,18 @@ function setup_whiteboard_directives() {
mime: "x-spacedeck/vector",
description: "",
control_points: [{dx:0,dy:0},{dx:0,dy:0}],
board: {
x: point.x,
y: point.y,
z: z,
w: 64,
h: 64,
h: 64
},
style: {
stroke_color: "#000000",
stroke: 2,
shape: "line"
}
};
$scope.save_artifact(a, function(saved_a) {
@ -707,11 +675,11 @@ function setup_whiteboard_directives() {
if (_.include(["text","placeholder"],$scope.artifact_major_type(ars[i]))) {
// some types of artifact need a minimum size
if (ars[i].w<10) {
ars[i].w = 10;
if (ars[i].board.w<10) {
ars[i].board.w = 10;
}
if (ars[i].h<10) {
ars[i].h = 10;
if (ars[i].board.h<10) {
ars[i].board.h = 10;
}
}
@ -735,7 +703,7 @@ function setup_whiteboard_directives() {
this.mouse_state = "idle";
$scope.mouse_state = this.mouse_state;
this.lasso = null;
//$scope.active_tool = "pointer"; disabled to keep the same tool (eg. "Scribble") after drawing a single line.
$scope.active_tool = "pointer";
$scope.end_transaction();
$scope.show_toolbar_props();
@ -859,8 +827,10 @@ function setup_whiteboard_directives() {
if (old_a) {
return {
x: old_a.x + dx - snap_dx,
y: old_a.y + dy - snap_dy
board: _.extend(a.board, {
x: old_a.board.x + dx - snap_dx,
y: old_a.board.y + dy - snap_dy
})
};
} else {
// deleted?
@ -900,19 +870,21 @@ function setup_whiteboard_directives() {
$scope.update_selected_artifacts(function(a) {
var old_a = $scope.find_artifact_before_transaction(a);
var x1 = origin_x + ((old_a.x - origin_x) * scale_x);
var y1 = origin_y + ((old_a.y - origin_y) * scale_y);
var x2 = origin_x + (((old_a.x + old_a.w) - origin_x) * scale_x);
var y2 = origin_y + (((old_a.y + old_a.h) - origin_y) * scale_y);
var x1 = origin_x + ((old_a.board.x - origin_x) * scale_x);
var y1 = origin_y + ((old_a.board.y - origin_y) * scale_y);
var x2 = origin_x + (((old_a.board.x + old_a.board.w) - origin_x) * scale_x);
var y2 = origin_y + (((old_a.board.y + old_a.board.h) - origin_y) * scale_y);
if (x1>x2) { var t = x1; x1 = x2; x2 = t; }
if (y1>y2) { var t = y1; y1 = y2; y2 = t; }
return {
board: _.extend(a.board, {
x: x1,
y: y1,
w: x2 - x1,
h: y2 - y1
})
};
}.bind(this));
@ -930,17 +902,18 @@ function setup_whiteboard_directives() {
var old_a = $scope.find_artifact_before_transaction(a);
var control_points = _.cloneDeep(old_a.control_points);
var board = _.clone(old_a.board);
var cp = control_points[$scope.selected_control_point_idx];
var snapped = _this.snap_point(old_a.x+cp.dx+dx, old_a.y+cp.dy+dy);
dx = snapped.snapx[1]-(old_a.x+cp.dx);
dy = snapped.snapy[1]-(old_a.y+cp.dy);
var snapped = _this.snap_point(board.x+cp.dx+dx, board.y+cp.dy+dy);
dx = snapped.snapx[1]-(board.x+cp.dx);
dy = snapped.snapy[1]-(board.y+cp.dy);
cp.dx += dx;
cp.dy += dy;
// special case for arrow's 3rd point
if (a.shape == "arrow" && $scope.selected_control_point_idx!=2) {
if (a.style.shape == "arrow" && $scope.selected_control_point_idx!=2) {
/*control_points[2].dx += dx/2;
control_points[2].dy += dy/2; */
@ -948,7 +921,7 @@ function setup_whiteboard_directives() {
control_points[2].dy = (control_points[0].dy+control_points[1].dy)/2;
}
return _this.normalize_control_points(control_points, old_a);
return _this.normalize_control_points(control_points, board);
});
} else if (this.mouse_state == "scribble") {
@ -957,14 +930,16 @@ function setup_whiteboard_directives() {
var old_a = a;
var control_points = _.cloneDeep(old_a.control_points);
var board = _.clone(old_a.board);
var offset = this.offset_point_in_wrapper({x:cursor.x,y:cursor.y});
control_points.push({
dx: offset.x-old_a.x,
dy: offset.y-old_a.y
dx: offset.x-board.x,
dy: offset.y-board.y
});
return this.normalize_control_points(simplify_scribble_points(control_points), old_a);
return this.normalize_control_points(simplify_scribble_points(control_points), board);
}.bind(this));
var arts = $scope.selected_artifacts();
@ -984,7 +959,7 @@ function setup_whiteboard_directives() {
}
},
normalize_control_points: function(control_points, artifact) {
normalize_control_points: function(control_points, board) {
var x1 = _.min(control_points,"dx").dx;
var y1 = _.min(control_points,"dy").dy;
var x2 = _.max(control_points,"dx").dx;
@ -1006,15 +981,19 @@ function setup_whiteboard_directives() {
var bshiftx = 0;
var bshifty = 0;
if (artifact.w < 0) bshiftx = -artifact.w;
if (artifact.h < 0) bshifty = -artifact.h;
if (board.w < 0) bshiftx = -board.w;
if (board.h < 0) bshifty = -board.h;
return {
x: artifact.x + bshiftx - shiftx,
y: artifact.y + bshifty - shifty,
var shifted_board = {
x: board.x + bshiftx - shiftx,
y: board.y + bshifty - shifty,
w: w,
h: h,
z: artifact.z,
z: board.z
};
return {
board: shifted_board,
control_points: shifted_cps
};
}

View File

@ -21,7 +21,7 @@ function vec2_angle(v) {
}
function render_vector_drawing(a, padding) {
var shape = a.shape || "";
var shape = a.style.shape || "";
var path = [];
var p = a.control_points[0];
@ -48,8 +48,8 @@ function render_vector_drawing(a, padding) {
var d = "M" + (cps.dx + padding) + "," + (cps.dy + padding) + " Q" + (scaledMiddlePoint.dx + padding) + "," + (scaledMiddlePoint.dy + padding) + " " + (cpe.dx + padding) + "," + (cpe.dy + padding);
var tip = "<defs><marker id='ae" + markerId + "' refX=\"0.1\" refY=\"3\" markerWidth=\"3\" markerHeight=\"6\" orient=\"auto\">";
tip += "<path d=\"M-3,0 V6 L3,3 Z\" fill=\""+a.stroke_color+"\" stroke-width=\"0\"/></marker></defs>";
var svg = tip + "<path d='" + d + "' style='stroke-width:" + a.stroke + ";' marker-end='url(#ae" + markerId + ")'/>";
tip += "<path d=\"M-3,0 V6 L3,3 Z\" fill=\""+a.style.stroke_color+"\" stroke-width=\"0\"/></marker></defs>";
var svg = tip + "<path d='" + d + "' style='stroke-width:" + a.style.stroke + ";' marker-end='url(#ae" + markerId + ")'/>";
return svg;
}
@ -237,11 +237,11 @@ function render_vector_rect(xradius,yradius,offset) {
}
function render_vector_shape(a) {
var stroke = parseInt(a.stroke) + 4;
var stroke = parseInt(a.style.stroke) + 4;
var offset = stroke / 2;
var xr = (a.w-stroke) / 2;
var yr = (a.h-stroke) / 2;
var xr = (a.board.w-stroke) / 2;
var yr = (a.board.h-stroke) / 2;
var shape_renderers = {
ellipse: function() { return render_vector_ellipse(xr, yr, offset); },
@ -258,7 +258,7 @@ function render_vector_shape(a) {
cloud: function() { return render_vector_cloud(xr, yr, offset); },
}
var render_func = shape_renderers[a.shape];
var render_func = shape_renderers[a.style.shape];
if (!render_func) return "";

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,57 @@
"use strict";
var config = require('config');
require('../../models/schema');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var async = require('async');
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router();
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
var userMapping = { '_id': 1, 'nickname': 1, 'email': 1};
var spaceMapping = { '_id': 1, name: 1};
router.get('/:membership_id/accept', function(req, res, next) {
if (req.user) {
db.Membership.findOne({where:{
Membership.findOne({
_id: req.params.membership_id,
code: req.query.code
}, include: ['space']}).then((mem) => {
state: "pending",
code: req.query.code,
user: { "$exists": false }
}).populate('space').exec((err,mem) => {
if (err) res.sendStatus(400);
else {
if (mem) {
if (!mem.user) {
if(!mem.user) {
mem.code = null;
mem.state = "active";
mem.user_id = req.user._id;
mem.user = req.user;
mem.save().then(function() {
mem.save(function(err){
if (err) res.status(400).json(err);
else {
console.log(mem);
res.status(200).json(mem);
}
});
} else {
res.status(200).json(mem);
res.status(400).json({"error": "already_used"});
}
} else {
res.status(404).json({"error": "not found"});
}
}
});
} else {
res.sendStatus(403);

View File

@ -1,10 +1,10 @@
"use strict";
var config = require('config');
const db = require('../../models/db');
require('../../models/schema');
var bcrypt = require('bcryptjs');
var crypto = require('crypto');
var crypo = require('crypto');
var URL = require('url').URL;
var express = require('express');
@ -12,61 +12,68 @@ var router = express.Router();
router.post('/', function(req, res) {
var data = req.body;
if (!data.email || !data.password) {
res.status(400).json({});
return;
}
if (data.email && data.password) {
var email = req.body.email.toLowerCase();
var password = req.body["password"];
db.User.findOne({where: {email: email}})
.error(err => {
res.sendStatus(404);
})
.then(user => {
if (!user) {
res.sendStatus(404);
}
else if (bcrypt.compareSync(password, user.password_hash)) {
crypto.randomBytes(48, function(ex, buf) {
User.find({email: email, account_type: "email"}, (function (err, users) {
if (err) {
res.status(400).json({"error":"session.users"});
} else {
if (users.length == 1) {
var user = users[0];
if (bcrypt.compareSync(password, user.password_hash)) {
crypo.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex');
var session = {
user_id: user._id,
token: token,
ip: req.ip,
device: "web",
created_at: new Date()
};
db.Session.create(session)
.error(err => {
console.error("Error creating Session:",err);
res.sendStatus(500);
})
.then(() => {
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : req.headers.hostname;
if (!user.sessions)
user.sessions = [];
user.sessions.push(session);
user.save(function(err, result) {
if (err) console.error("Error saving user:",err);
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : "localhost";
res.cookie('sdsession', token, { domain: domain, httpOnly: true });
res.status(201).json(session);
});
});
} else {
}else{
res.sendStatus(403);
}
});
} else {
res.sendStatus(404);
}
}
}));
} else {
res.status(400).json({});
}
});
router.delete('/current', function(req, res, next) {
if (req.user) {
var token = req.cookies['sdsession'];
db.Session.findOne({where: {token: token}})
.then(session => {
session.destroy();
var user = req.user;
var newSessions = user.sessions.filter( function(session){
return session.token != req.token;
});
var domain = (process.env.NODE_ENV == "production") ? new URL(config.get('endpoint')).hostname : req.headers.hostname;
user.sessions = newSessions;
user.save(function(err, result) {
var domain = new URL(config.get('endpoint')).hostname;
res.clearCookie('sdsession', { domain: domain });
res.sendStatus(204);
});
} else {
res.sendStatus(404);
}

View File

@ -1,12 +1,7 @@
"use strict";
var config = require('config');
const os = require('os');
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
require('../../models/schema');
var payloadConverter = require('../../helpers/artifact_converter');
var redis = require('../../helpers/redis');
@ -14,11 +9,13 @@ var redis = require('../../helpers/redis');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
@ -49,17 +46,15 @@ var roleMapping = {
// ARTIFACTS
router.get('/', (req, res) => {
db.Artifact.findAll({where: {
Artifact.find({
space_id: req.space._id
}}).then(artifacts => {
}).exec((err, artifacts) => {
async.map(artifacts, (a, cb) => {
db.unpackArtifact(a);
a = a.toObject();
if (a.user_id) {
// FIXME JOIN
/*User.findOne({where: {
User.findOne({
"_id": a.user_id
}}).select({
}).select({
"_id": 1,
"nickname": 1,
"email": 1
@ -68,8 +63,7 @@ router.get('/', (req, res) => {
a['user'] = user.toObject();
}
cb(err, a);
});*/
cb(null, a);
});
} else {
cb(null, a);
}
@ -87,8 +81,9 @@ router.post('/', function(req, res, next) {
attrs['space_id'] = req.space._id;
var artifact = attrs;
artifact._id = uuidv4();
var artifact = new Artifact(attrs);
artifact.created_from_ip = req['real_ip'];
if (req.user) {
artifact.user_id = req.user._id;
@ -97,18 +92,23 @@ router.post('/', function(req, res, next) {
artifact.last_update_editor_name = req.editor_name;
}
db.packArtifact(artifact);
if (req.spaceRole == "editor" || req.spaceRole == "admin") {
db.Artifact.create(artifact).then(() => {
//if (err) res.status(400).json(err);
db.unpackArtifact(artifact);
db.Space.update({ updated_at: new Date() }, {where: {_id: req.space._id}});
if (req.spaceRole == "editor"  ||  req.spaceRole == "admin") {
artifact.save(function(err) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeCreate("Artifact", artifact);
}
});
} else {
res.status(401).json({
"error": "Access denied"
"error": "no access"
});
}
});
@ -118,26 +118,30 @@ router.post('/:artifact_id/payload', function(req, res, next) {
var a = req.artifact;
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9_\-\.]/g, '');
var localFilePath = os.tmpdir() + "/" + fileName;
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
var progressCallback = function(progressMsg) {
a.description = progressMsg.toString();
db.packArtifact(a);
var progress_callback = function(progress_msg) {
a.description = progress_msg;
a.save();
redis.sendMessage("update", "Artifact", a, req.channelId);
redis.sendMessage("update", a, a.toJSON(), req.channelId);
};
stream.on('finish', function() {
payloadConverter.convert(a, fileName, localFilePath, function(error, artifact) {
if (error) res.status(400).json(error);
else {
db.Space.update({ updated_at: new Date() }, {where: {_id: req.space._id}});
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeUpdate("Artifact", artifact);
}
}, progressCallback);
}, progress_callback);
});
} else {
res.status(401).json({
@ -158,23 +162,41 @@ router.put('/:artifact_id', function(req, res, next) {
newAttr.last_update_editor_name = req.editor_name;
}
db.packArtifact(newAttr);
db.Artifact.update(newAttr, { where: {
Artifact.findOneAndUpdate({
"_id": a._id
}}).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);
}, {
"$set": newAttr
}, {
"new": true
}, function(err, artifact) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeUpdate("Artifact", artifact);
}
});
});
router.delete('/:artifact_id', function(req, res, next) {
var artifact = req.artifact;
db.Artifact.destroy({where: { "_id": artifact._id}}).then(() => {
db.Space.update({ updated_at: new Date() }, {where: {_id: req.space._id} });
artifact.remove(function(err) {
if (err) res.status(400).json(err);
else {
Space.update({
_id: req.space._id
}, {
"$set": {
updated_at: new Date()
}
});
res.distributeDelete("Artifact", artifact);
}
});
});

View File

@ -1,15 +1,17 @@
"use strict";
var config = require('config');
require('../../models/db');
require('../../models/schema');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
@ -38,12 +40,6 @@ var roleMapping = {
};
router.get('/', function(req, res, next) {
res.status(200).json([]);
return;
// FIXME TODO
var showActionForSpaces = function(err, spaceIds) {
var userMapping = {
'_id': 1,
@ -138,6 +134,7 @@ router.get('/', function(req, res, next) {
"$exists": 1
}
}).populate("space").exec(function(err, memberships) {
async.map(memberships, function(membership, memcb) {
Space.getRecursiveSubspacesForSpace(membership.space, function(err, spaces) {
cb(null, spaces.map(function(s) {

View File

@ -1,7 +1,8 @@
"use strict";
var config = require('config');
const db = require('../../models/db');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
@ -11,11 +12,13 @@ var async = require('async');
var moment = require('moment');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var sanitizeHtml = require('sanitize-html');
@ -46,17 +49,26 @@ var roleMapping = {
router.get('/png', function(req, res, next) {
var triggered = new Date();
var s3_filename = "s" + req.space._id + "/" + "thumb_" + triggered.getTime() + ".jpg";
if (!req.space.thumbnail_updated_at || req.space.thumbnail_updated_at < req.space.updated_at || !req.space.thumbnail_url) {
db.Space.update({ thumbnail_updated_at: triggered }, {where : {"_id": req.space._id }});
phantom.takeScreenshot(req.space, "png", function(local_path) {
Space.update({
"_id": req.space._id
}, {
"$set": {
thumbnail_updated_at: triggered
}
}, function(a, b, c) {});
phantom.takeScreenshot(req.space, "png",
function(local_path) {
var localResizedFilePath = local_path + ".thumb.jpg";
gm(local_path).resize(640, 480).quality(70.0).autoOrient().write(localResizedFilePath, function(err) {
if (err) {
console.error("[space screenshot] resize error: ", err);
console.error("screenshot resize error: ", err);
res.status(500).send("Error taking screenshot.");
return;
}
@ -64,28 +76,35 @@ router.get('/png', function(req, res, next) {
uploader.uploadFile(s3_filename, "image/jpeg", localResizedFilePath, function(err, thumbnailUrl) {
if (err) {
console.error("[space screenshot] upload error. filename: " + s3_filename + " details: ", err);
console.error("screenshot s3 upload error. filename: " + s3_filename + " details: ", err);
res.status(500).send("Error uploading screenshot.");
return;
}
var oldUrl = req.space.thumbnail_url;
db.Space.update({ thumbnail_url: thumbnailUrl }, {where : {"_id": req.space._id }}).then(() => {
Space.update({
"_id": req.space._id
}, {
"$set": {
thumbnail_url: thumbnailUrl
}
}, function(a, b, c) {
res.redirect(thumbnailUrl);
try {
if (oldUrl) {
var oldPath = url.parse(oldUrl).pathname;
uploader.removeFile(oldPath, function(err, res) {});
}
fs.unlinkSync(local_path);
fs.unlink(local_path);
} catch (e) {
console.error(e);
}
});
try {
fs.unlinkSync(localResizedFilePath);
fs.unlink(localResizedFilePath);
} catch (e) {
console.error(e);
}
@ -94,7 +113,7 @@ router.get('/png', function(req, res, next) {
},
function() {
// on_error
console.error("[space screenshot] could not create screenshot for space " + req.space_id);
console.error("phantom could not create screenshot for space " + req.space_id);
res.status(404).send("Not found");
});
} else {
@ -106,6 +125,77 @@ function make_export_filename(space, extension) {
return space.name.replace(/[^\w]/g, '') + "-" + space._id + "-" + moment().format("YYYYMMDD-HH-mm-ss") + "." + extension;
}
router.get('/list', function(req, res, next) {
if (req.user) {
if (req.spaceRole == "admin" ||  req.spaceRole == "editor") {
if (req.space.space_type == "space") {
Artifact.find({
space_id: req.space._id
}).exec(function(err, artifacts) {
async.map(artifacts, function(a, cb) {
if (a.user_id) {
User.findOne({
"_id": a.user_id
}).exec(function(err, user) {
a.user = user;
if (a.last_update_user_id) {
User.findOne({
"_id": a.last_update_user_id
}).exec(function(err, updateUser) {
a.update_user = updateUser;
cb(null, a);
});
} else {
cb(null, a);
}
});
} else {
cb(null, a);
}
}, function(err, mappedArtifacts) {
req.space.artifacts = mappedArtifacts.map(function(a) {
a.description = sanitizeHtml(a.description, {
allowedTags: [],
allowedAttributes: []
});
if (a.payload_uri) {
var parsed = url.parse(a.payload_uri);
var fileName = path.basename(parsed.pathname) || "file.bin";
a.filename = fileName;
}
return a;
});
res.render('artifact_list', {
space: req.space
});
});
});
} else {
Space.getRecursiveSubspacesForSpace(req.space, (err, subspaces) => {
res.render('space_list', {
subspaces: subspaces.map((s) => {
s.ae_link = config.endpoint + '/s/' + s.edit_hash + (s.edit_slug ? ('-'+s.edit_slug) : '')
return s;
}),
space: req.space
});
});
}
} else {
res.sendStatus(403);
}
} else {
res.sendStatus(403);
}
});
router.get('/pdf', function(req, res, next) {
var s3_filename = make_export_filename(req.space, "pdf");
@ -239,13 +329,36 @@ router.get('/zip', function(req, res, next) {
});
router.get('/html', function(req, res) {
db.Artifact.findAll({where: {
Artifact.find({
space_id: req.space._id
}}).then(function(artifacts) {
}, function(err, artifacts) {
var space = req.space;
res.send(space_render.render_space_as_html(space, artifacts));
});
});
module.exports = router;
router.get('/path', (req, res) => {
// build up a breadcrumb trail (path)
var path = [];
var buildPath = (space) => {
if (space.parent_space_id) {
Space.findOne({
"_id": space.parent_space_id
}, (err, parentSpace) => {
if (space._id == parentSpace._id) {
console.log("error: circular parent reference for space " + space._id);
res.send("error: circular reference");
} else {
path.push(parentSpace);
buildPath(parentSpace);
}
});
} else {
// reached the top
res.json(path.reverse());
}
}
buildPath(req.space);
});
module.exports = router;

View File

@ -1,31 +1,57 @@
"use strict";
var config = require('config');
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var space_render = require('../../helpers/space-render');
var phantom = require('../../helpers/phantom');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var glob = require('glob');
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
// JSON MAPPINGS
var userMapping = {
_id: 1,
nickname: 1,
email: 1,
avatar_thumb_uri: 1
};
var spaceMapping = {
_id: 1,
name: 1,
thumbnail_url: 1
};
var roleMapping = {
"none": 0,
"viewer": 1,
"editor": 2,
"admin": 3
}
router.get('/', function(req, res, next) {
db.Membership
.findAll({where: {
space_id: req.space._id
}, include: ['user']})
.then(memberships => {
Membership
.find({
space: req.space._id
})
.populate("user")
.exec(function(err, memberships) {
res.status(200).json(memberships);
});
});
@ -33,28 +59,26 @@ router.get('/', function(req, res, next) {
router.post('/', function(req, res, next) {
if (req.spaceRole == "admin") {
var attrs = req.body;
attrs.space_id = req.space._id;
attrs.state = "pending";
attrs._id = uuidv4();
var membership = attrs;
attrs['space'] = req.space._id;
attrs['state'] = "pending";
var membership = new Membership(attrs);
var msg = attrs.personal_message;
if (membership.email_invited != req.user.email) {
db.User.findOne({where:{
User.findOne({
"email": membership.email_invited
}}).then(function(user) {
}, function(err, user) {
// existing user? then immediately activate membership
if (user) {
membership.user_id = user._id;
membership.user = user;
membership.state = "active";
} else {
// if not, invite via email and invite code
membership.code = crypto.randomBytes(64).toString('hex').substring(0, 12);
}
db.Membership.create(membership).then(function() {
membership.save(function(err) {
if (err) res.sendStatus(400);
else {
var accept_link = config.endpoint + "/accept/" + membership._id + "?code=" + membership.code;
if (user) {
@ -80,19 +104,20 @@ router.post('/', function(req, res, next) {
});
res.status(201).json(membership);
}
});
});
} else {
res.status(400).json({
"error": "This email is already included in the Space memberships."
"error": "user already in space"
});
}
} else {
res.status(403).json({
"error": "Only administrators can do that."
"error": "not_permitted"
});
}
});
@ -100,20 +125,19 @@ router.post('/', function(req, res, next) {
router.put('/:membership_id', function(req, res, next) {
if (req.user) {
if (req.spaceRole == "admin") {
db.Membership.findOne({ where: {
Membership.findOne({
_id: req.params.membership_id
}}).then(function(mem) {
}, function(err, mem) {
if (err) res.sendStatus(400);
else {
if (mem) {
// is the user trying to change their own role?
if (mem.user_id == req.user._id) {
res.status(400).json({
"error": "Cannot change your own role."
});
} else {
var attrs = req.body;
mem.role = attrs.role;
mem.save(function() {
mem.save(function(err) {
if (err) res.sendStatus(400);
else {
res.status(201).json(mem);
}
});
}
}
@ -127,25 +151,21 @@ router.put('/:membership_id', function(req, res, next) {
});
router.delete('/:membership_id', function(req, res, next) {
if (req.user && req.spaceRole == 'admin') {
db.Membership.count({ where: {
space_id: req.space._id,
role: "admin"
}}).then(function(adminCount) {
db.Membership.findOne({ where: {
if (req.user) {
Membership.findOne({
_id: req.params.membership_id
}}).then(function(mem) {
// deleting an admin? need at least 1
if (mem.role != "admin" || adminCount > 1) {
mem.destroy().then(function() {
res.sendStatus(204);
});
}, function(err, mem) {
if (err) res.sendStatus(400);
else {
mem.remove(function(err) {
if (err) {
res.status(400).json(err);
} else {
res.status(400).json({
"error": "Space needs at least one administrator."
// FIXME might need to delete the user?
res.sendStatus(204);
}
});
}
})
});
} else {
res.sendStatus(403);

View File

@ -1,9 +1,6 @@
"use strict";
var config = require('config');
const db = require('../../models/db');
const Sequelize = require('sequelize');
const Op = Sequelize.Op;
const uuidv4 = require('uuid/v4');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
@ -14,12 +11,15 @@ var phantom = require('../../helpers/phantom');
var async = require('async');
var fs = require('fs');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
var express = require('express');
var router = express.Router({mergeParams: true});
@ -49,44 +49,90 @@ var roleMapping = {
// MESSAGES
router.get('/', function(req, res, next) {
db.Message.findAll({where:{
space_id: req.space._id
}, include: ['user']})
.then(function(messages) {
Message.find({
space: req.space._id
}).populate('user', userMapping).exec(function(err, messages) {
res.status(200).json(messages);
});
});
router.post('/', function(req, res, next) {
var attrs = req.body;
attrs.space_id = req.space._id;
attrs.space = req.space;
if (req.user) {
attrs.user = req.user;
attrs.user_id = req.user._id;
} else {
attrs.user = null;
}
var msg = attrs;
msg._id = uuidv4();
db.Message.create(msg).then(function() {
var msg = new Message(attrs);
msg.save(function(err) {
if (err) res.status(400).json(erra);
else {
if (msg.message.length <= 1) return;
// TODO reimplement notifications
Membership
.find({
space: req.space,
user: {
"$exists": true
}
})
.populate('user')
.exec(function(err, memberships) {
var users = memberships.map(function(m) {
return m.user;
});
users.forEach((user) => {
if (user.preferences.email_notifications) {
redis.isOnlineInSpace(user, req.space, function(err, online) {
if (!online) {
var nickname = msg.editor_name;
if (req.user) {
nickname = req.user.nickname;
}
mailer.sendMail(
user.email,
req.i18n.__("space_message_subject", req.space.name),
req.i18n.__("space_message_body", nickname, req.space.name), {
message: msg.message,
action: {
link: config.endpoint + "/spaces/" + req.space._id.toString(),
name: req.i18n.__("open")
}
});
} else {
console.log("not sending message to user: is online.");
}
});
} else {
console.log("not sending message to user: is disabled notifications.");
}
});
});
res.distributeCreate("Message", msg);
}
});
});
router.delete('/:message_id', function(req, res, next) {
db.Message.findOne({where:{
Message.findOne({
"_id": req.params.message_id
}}).then(function(msg) {
}, function(err, msg) {
if (!msg) {
res.sendStatus(404);
} else {
msg.destroy().then(function() {
msg.remove(function(err) {
if (err) res.status(400).json(err);
else {
if (msg) {
res.distributeDelete("Message", msg);
} else {
res.sendStatus(404);
}
}
});
}
});

View File

@ -1,10 +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;
const uuidv4 = require('uuid/v4');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
@ -18,10 +14,13 @@ var slug = require('slug');
var fs = require('fs');
var async = require('async');
var _ = require("underscore");
var mongoose = require("mongoose");
var archiver = require('archiver');
var request = require('request');
var url = require("url");
var path = require("path");
var crypto = require('crypto');
var qr = require('qr-image');
var glob = require('glob');
var gm = require('gm');
const exec = require('child_process');
@ -48,61 +47,132 @@ router.get('/', function(req, res, next) {
error: "auth required"
});
} else {
if (req.query.search) {
db.Membership.findAll({where:{
user_id: req.user._id
}}).then(memberships => {
// search for spaces
if (req.query.writablefolders) {
Membership.find({
user: req.user._id
}, (err, memberships) => {
var validMemberships = memberships.filter(function(m) {
if (!m.space_id || (m.space_id == "undefined"))
var validMemberships = memberships.filter((m) => {
if (!m.space || (m.space == "undefined"))
return false;
else
return true;
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var editorMemberships = validMemberships.filter((m) => {
return (m.role == "editor") || (m.role == "admin")
});
var spaceIds = editorMemberships.map(function(m) {
return new mongoose.Types.ObjectId(m.space);
});
var q = {
"space_type": "folder",
"$or": [{
"creator": req.user._id
}, {
"_id": {
"$in": spaceIds
},
"creator": {
"$ne": req.user._id
}
}]
};
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s.toObject();
return spaceObj;
});
async.map(spaces, (space, cb) => {
Space.getRecursiveSubspacesForSpace(space, (err, spaces) => {
var allSpaces = spaces;
cb(err, allSpaces);
})
}, (err, spaces) => {
var allSpaces = _.flatten(spaces);
var onlyFolders = _.filter(allSpaces, (s) => {
return s.space_type == "folder";
})
var uniqueFolders = _.unique(onlyFolders, (s) => {
return s._id.toString();
})
res.status(200).json(uniqueFolders);
});
});
});
} else if (req.query.search) {
Membership.find({
user: req.user._id
}, function(err, memberships) {
var validMemberships = memberships.filter(function(m) {
if (!m.space || (m.space == "undefined"))
return false;
else
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var spaceIds = validMemberships.map(function(m) {
return m.space_id;
return new mongoose.Types.ObjectId(m.space);
});
// TODO FIXME port
var q = { where: {
[Op.or]: [{"creator_id": req.user._id},
{"_id": {[Op.in]: spaceIds}},
{"parent_space_id": {[Op.in]: spaceIds}}],
name: {[Op.like]: "%"+req.query.search+"%"}
}, include: ['creator']};
var q = {
"$or": [{"creator": req.user._id},
{"_id": {"$in": spaceIds}},
{"parent_space_id": {"$in": spaceIds}}],
name: new RegExp(req.query.search, "i")
};
db.Space
.findAll(q)
.then(function(spaces) {
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = s.toObject();
return spaceObj;
});
res.status(200).json(spaces);
});
});
} else if (req.query.parent_space_id && req.query.parent_space_id != req.user.home_folder_id) {
// list spaces in a folder
db.Space
.findOne({where: {
Space
.findOne({
_id: req.query.parent_space_id
}})
//.populate('creator', userMapping)
.then(function(space) {
})
.populate('creator', userMapping)
.exec(function(err, space) {
if (space) {
db.getUserRoleInSpace(space, req.user, function(role) {
Space.roleInSpace(space, req.user, function(err, role) {
if (role == "none") {
if (space.access_mode == "public") {
if(space.access_mode == "public") {
role = "viewer";
}
}
if (role != "none") {
db.Space
.findAll({where:{
Space
.find({
parent_space_id: req.query.parent_space_id
}, include:['creator']})
.then(function(spaces) {
})
.populate('creator', userMapping)
.exec(function(err, spaces) {
res.status(200).json(spaces);
});
} else {
@ -115,42 +185,41 @@ router.get('/', function(req, res, next) {
});
} else {
// list home folder and spaces/folders that the user is a member of
db.Membership.findAll({ where: {
user_id: req.user._id
}}).then(memberships => {
if (!memberships) memberships = [];
Membership.find({
user: req.user._id
}, function(err, memberships) {
var validMemberships = memberships.filter(function(m) {
if (!m.space_id || (m.space_id == "undefined"))
if (!m.space || (m.space == "undefined"))
return false;
return true;
else
return mongoose.Types.ObjectId.isValid(m.space.toString());
});
var spaceIds = validMemberships.map(function(m) {
return m.space_id;
return new mongoose.Types.ObjectId(m.space);
});
var q = {
[Op.or]: [{
"creator_id": req.user._id,
"$or": [{
"creator": req.user._id,
"parent_space_id": req.user.home_folder_id
}, {
"_id": {
[Op.in]: spaceIds
"$in": spaceIds
},
"creator_id": {
[Op.ne]: req.user._id
"creator": {
"$ne": req.user._id
}
}]
};
db.Space
.findAll({where: q, include: ['creator']})
.then(function(spaces) {
Space
.find(q)
.populate('creator', userMapping)
.exec(function(err, spaces) {
if (err) console.error(err);
var updatedSpaces = spaces.map(function(s) {
var spaceObj = db.spaceToObject(s);
var spaceObj = s.toObject();
return spaceObj;
});
res.status(200).json(spaces);
@ -160,47 +229,47 @@ router.get('/', function(req, res, next) {
}
});
// create a space
router.post('/', function(req, res, next) {
if (req.user) {
var attrs = req.body;
var createSpace = () => {
attrs._id = uuidv4();
attrs.creator_id = req.user._id;
attrs.creator = req.user;
attrs.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7);
attrs.edit_slug = slug(attrs.name);
attrs.access_mode = "private";
db.Space.create(attrs).then(createdSpace => {
res.status(201).json(createdSpace);
// create initial admin membership
var membership = {
_id: uuidv4(),
user_id: req.user._id,
space_id: attrs._id,
role: "admin",
state: "active"
};
db.Membership.create(membership).then(() => {
res.status(201).json(createdSpace);
var space = new Space(attrs);
space.save(function(err, createdSpace) {
if (err) res.sendStatus(400);
else {
var membership = new Membership({
user: req.user,
space: createdSpace,
role: "admin"
});
membership.save(function(err, createdTeam) {
if (err) {
res.status(400).json(err);
} else {
res.status(201).json(createdSpace);
}
});
}
});
}
if (attrs.parent_space_id) {
db.Space.findOne({ where: {
Space.findOne({
"_id": attrs.parent_space_id
}}).then(parentSpace => {
}).populate('creator', userMapping).exec((err, parentSpace) => {
if (parentSpace) {
db.getUserRoleInSpace(parentSpace, req.user, (role) => {
Space.roleInSpace(parentSpace, req.user, (err, role) => {
if ((role == "editor") || (role == "admin")) {
createSpace();
} else {
res.status(403).json({
"error": "not editor in parent Space. role: "+role
"error": "not editor in parent Space"
});
}
});
@ -223,30 +292,6 @@ router.get('/:id', function(req, res, next) {
res.status(200).json(req.space);
});
router.get('/:id/path', (req, res) => {
// build up a breadcrumb trail (path)
var path = [];
var buildPath = (space) => {
if (space.parent_space_id) {
db.Space.findOne({ where: {
"_id": space.parent_space_id
}}).then(parentSpace => {
if (space._id == parentSpace._id) {
console.error("error: circular parent reference for space " + space._id);
res.send("error: circular reference");
} else {
path.push(parentSpace);
buildPath(parentSpace);
}
});
} else {
// reached the top
res.json(path.reverse());
}
}
buildPath(req.space);
});
router.put('/:id', function(req, res) {
var space = req.space;
var newAttr = req.body;
@ -260,29 +305,27 @@ router.put('/:id', function(req, res) {
newAttr.edit_slug = slug(newAttr['name']);
delete newAttr['_id'];
delete newAttr['editor_name'];
delete newAttr['creator'];
delete newAttr['creator_id'];
delete newAttr['space_type'];
if (req['spaceRole'] != "admin") {
delete newAttr['access_mode']
delete newAttr['password']
delete newAttr['edit_hash']
delete newAttr['edit_slug']
delete newAttr['editors_locking']
}
db.Space.update(newAttr, {where: {
Space.findOneAndUpdate({
"_id": space._id
}}).then(space => {
}, {
"$set": newAttr
}, {
"new": true
}, function(err, space) {
if (err) res.status(400).json(err);
else {
res.distributeUpdate("Space", space);
}
});
});
router.post('/:id/background', function(req, res, next) {
var space = req.space;
var newDate = new Date();
var fileName = (req.query.filename || "upload.jpg").replace(/[^a-zA-Z0-9\.]/g, '');
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
@ -291,18 +334,29 @@ router.post('/:id/background', function(req, res, next) {
uploader.uploadFile("s" + req.space._id + "/bg_" + newDate.getTime() + "_" + fileName, "image/jpeg", localFilePath, function(err, backgroundUrl) {
if (err) res.status(400).json(err);
else {
if (space.background_uri) {
var oldPath = url.parse(req.space.background_uri).pathname;
var adv = space.advanced;
if (adv.background_uri) {
var oldPath = url.parse(req.space.thumbnail_url).pathname;
uploader.removeFile(oldPath, function(err) {
console.error("removed old bg error:", err);
console.log("removed old bg error:", err);
});
}
db.Space.update({
background_uri: backgroundUrl
adv.background_uri = backgroundUrl;
Space.findOneAndUpdate({
"_id": space._id
}, {
where: { "_id": space._id }
}, function(rows) {
"$set": {
advanced: adv
}
}, {
"new": true
}, function(err, space) {
if (err) {
res.sendStatus(400);
} else {
fs.unlink(localFilePath, function(err) {
if (err) {
console.error(err);
@ -311,24 +365,65 @@ router.post('/:id/background', function(req, res, next) {
res.status(200).json(space);
}
});
}
});
}
});
});
});
var handleDuplicateSpaceRequest = function(req, res, parentSpace) {
Space.duplicateSpace(req.space, req.user, 0, (err, newSpace) => {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(201).json(newSpace);
}
}, parentSpace);
}
router.post('/:id/duplicate', (req, res, next) => {
if (req.query.parent_space_id) {
Space.findOne({
_id: req.query.parent_space_id
}).populate('creator', userMapping).exec((err, parentSpace) => {
if (!parentSpace) {
res.status(404).json({
"error": "parent space not found for dupicate"
});
} else {
Space.roleInSpace(parentSpace, req.user, (err, role) => {
if (role == "admin" ||  role == "editor") {
handleDuplicateSpaceRequest(req, res, parentSpace);
} else {
res.status(403).json({
"error": "not authed for parent_space_id"
});
}
});
}
});
} else {
handleDuplicateSpaceRequest(req, res);
}
});
router.delete('/:id', function(req, res, next) {
if (req.user) {
const space = req.space;
if (req.spaceRole == "admin") {
const attrs = req.body;
space.destroy().then(function() {
Space.recursiveDelete(space, function(err) {
if (err) res.status(400).json(err);
else {
res.distributeDelete("Space", space);
}
});
} else {
res.status(403).json({
"error": "requires admin role"
"error": "requires admin status"
});
}
} else {
@ -336,4 +431,139 @@ router.delete('/:id', function(req, res, next) {
}
});
router.post('/:id/artifacts-pdf', function(req, res, next) {
if (req.spaceRole == "editor" || req.spaceRole == "admin") {
var withZones = (req.query.zones) ? req.query.zones == "true" : false;
var fileName = (req.query.filename || "upload.bin").replace(/[^a-zA-Z0-9\.]/g, '');
var localFilePath = "/tmp/" + fileName;
var writeStream = fs.createWriteStream(localFilePath);
var stream = req.pipe(writeStream);
req.on('end', function() {
var rawName = fileName.slice(0, fileName.length - 4);
var outputFolder = "/tmp/" + rawName;
var rights = 777;
fs.mkdir(outputFolder, function(db) {
var images = outputFolder + "/" + rawName + "-page-%03d.jpeg";
exec.execFile("gs", ["-sDEVICE=jpeg", "-dDownScaleFactor=4", "-dDOINTERPOLATE", "-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-sOutputFile=" + images, "-r250", "-f", localFilePath], {}, function(error, stdout, stderr) {
if (error === null) {
glob(outputFolder + "/*.jpeg", function(er, files) {
var count = files.length;
var delta = 10;
var limitPerRow = Math.ceil(Math.sqrt(count));
var startX = parseInt(req.query.x, delta);
var startY = parseInt(req.query.y, delta);
async.mapLimit(files, 20, function(localfilePath, cb) {
var fileName = path.basename(localfilePath);
var baseName = path.basename(localfilePath, ".jpeg");
var number = parseInt(baseName.slice(baseName.length - 3, baseName.length), 10);
gm(localFilePath).size(function(err, size) {
var w = 350;
var h = w;
var x = startX + (((number - 1) % limitPerRow) * w);
var y = startY + ((parseInt(((number - 1) / limitPerRow), 10) + 1) * w);
var userId;
if (req.user)
userId = req.user._id;
var a = new Artifact({
mime: "image/jpg",
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: w,
h: h,
x: x,
y: y,
z: (number + (count + 100))
}
});
payloadConverter.convert(a, fileName, localfilePath, (error, artifact) => {
if (error) res.status(400).json(error);
else {
if (withZones) {
var zone = new Artifact({
mime: "x-spacedeck/zone",
description: "Zone " + (number),
space_id: req.space._id,
user_id: userId,
editor_name: req.guest_name,
board: {
w: artifact.board.w + 20,
h: artifact.board.h + 40,
x: x - 10,
y: y - 30,
z: number
},
style: {
order: number,
valign: "middle",
align: "center"
}
});
zone.save((err) => {
redis.sendMessage("create", "Artifact", zone.toJSON(), req.channelId);
cb(null, [artifact, zone]);
});
} else {
cb(null, [artifact]);
}
}
});
});
}, function(err, artifacts) {
exec.execFile("rm", ["-r", outputFolder], function(err) {
res.status(201).json(_.flatten(artifacts));
async.eachLimit(artifacts, 10, (artifact_or_artifacts, cb) => {
if (artifact_or_artifacts instanceof Array) {
_.each(artifact_or_artifacts, (a) => {
redis.sendMessage("create", "Artifact", a.toJSON(), req.channelId);
});
} else  {
redis.sendMessage("create", "Artifact", artifact_or_artifacts.toJSON(), req.channelId);
}
cb(null);
});
});
});
});
} else {
console.error("error:", error);
exec.execFile("rm", ["-r", outputFolder], function(err) {
fs.unlink(localFilePath);
res.status(400).json({});
});
}
});
});
});
} else {
res.status(401).json({
"error": "no access"
});
}
});
module.exports = router;

265
routes/api/teams.js Normal file
View File

@ -0,0 +1,265 @@
"use strict";
var config = require('config');
require('../../models/schema');
var redis = require('../../helpers/redis');
var mailer = require('../../helpers/mailer');
var fs = require('fs');
var _ = require('underscore');
var crypto = require('crypto');
var bcrypt = require('bcryptjs');
var express = require('express');
var router = express.Router();
var userMapping = { '_id': 1, 'nickname': 1, 'email': 1};
router.get('/:id', (req, res) => {
res.status(200).json(req.user.team);
});
router.put('/:id', (req, res) => {
var team = req.user.team;
if (!team) {
res.status(400).json({"error": "user in no team"});
} else {
var newAttr = req.body;
newAttr.updated_at = new Date();
delete newAttr['_id'];
if(newAttr['subdomain']) {
newAttr['subdomain'] = newAttr['subdomain'].toLowerCase();
}
const new_subdomain = newAttr['subdomain'];
var forbidden_subdomains = [];
function updateTeam() {
Team.findOneAndUpdate({"_id": team._id}, {"$set": newAttr}, {"new": true}, (err, team) => {
if (err) res.status(400).json(err);
else {
res.status(200).json(team);
}
});
}
var isForbidden = forbidden_subdomains.indexOf(new_subdomain) > -1;
if (isForbidden) {
res.bad_request("subdomain not valid");
} else {
if (new_subdomain) {
Team.findOne({"domain": new_subdomain}).exec((err, team) => {
if(team) {
res.bad_request("subdomain already used");
} else {
updateTeam()
}
});
} else {
updateTeam()
}
}
}
});
router.get('/:id/memberships', (req, res) => {
User
.find({team: req.user.team})
.populate("team")
.exec(function(err, users){
if (err) res.status(400).json(err);
else {
res.status(200).json(users);
}
});
});
router.post('/:id/memberships', (req, res, next) => {
if (req.body.email) {
const email = req.body.email.toLowerCase();
const team = req.user.team;
User.findOne({"email": email}).populate('team').exec((err, user) => {
if (user) {
const code = crypto.randomBytes(64).toString('hex').substring(0,7);
team.invitation_codes.push(code);
team.save((err) => {
if (err){ res.status(400).json(err); }
else {
mailer.sendMail(email, req.i18n.__("team_invite_membership_subject", team.name), req.i18n.__("team_invite_membership_body", team.name), { action: {
link: config.endpoint + "/teams/" + req.user.team._id + "/join?code=" + code,
name: req.i18n.__("team_invite_membership_action"),
teamname: team.name
}});
res.status(201).json(user);
}
});
} else {
// complete new user
const password = crypto.randomBytes(64).toString('hex').substring(0,7);
const confirmation_token = crypto.randomBytes(64).toString('hex').substring(0,7);
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, (err, hash) => {
crypto.randomBytes(16, (ex, buf) => {
const token = buf.toString('hex');
var u = new User({
email: email,
account_type: "email",
nickname: email,
team: team._id,
password_hash: hash,
payment_plan_key: team.payment_plan_key,
confirmation_token: confirmation_token,
preferences: {
language: req.i18n.locale
}
});
u.save((err) => {
if(err) res.sendStatus(400);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator: u
});
homeSpace.save((err, homeSpace) => {
if (err) res.sendStatus(400);
else {
u.home_folder_id = homeSpace._id;
u.save((err) => {
User.find({"_id": {"$in": team.admins }}).exec((err, admins) => {
admins.forEach((admin) => {
var i18n = req.i18n;
if(admin.preferences && admin.preferences.language){
i18n.setLocale(admin.preferences.language || "en");
}
mailer.sendMail(admin.email, i18n.__("team_invite_membership_subject", team.name), i18n.__("team_invite_admin_body", email, team.name, password), { teamname: team.name });
});
});
mailer.sendMail(email, req.i18n.__("team_invite_membership_subject", team.name), req.i18n.__("team_invite_user_body", team.name, password), { action: {
link: config.endpoint + "/users/byteam/" + req.user.team._id + "/join?confirmation_token=" + confirmation_token,
name: req.i18n.__("team_invite_membership_action")
}, teamname: team.name });
if (err) res.status(400).json(err);
else{
res.status(201).json(u)
}
});
}
});
}
});
});
});
});
}
});
} else {
res.status(400).json({"error": "email missing"});
}
});
router.put('/:id/memberships/:user_id', (req, res) => {
User.findOne({_id: req.params.user_id}, (err,mem) => {
if (err) res.sendStatus(400);
else {
if(user.team._id == req.user.team._id){
user['team'] = req.user.team._id;
user.save((err) => {
res.sendStatus(204);
});
} else {
res.sendStatus(403);
}
}
});
});
router.get('/:id/memberships/:user_id/promote', (req, res) => {
User.findOne({_id: req.params.user_id}, (err,user) => {
if (err) res.sendStatus(400);
else {
if (user.team.toString() == req.user.team._id.toString()) {
var team = req.user.team;
var adminIndex = team.admins.indexOf(user._id);
if (adminIndex == -1) {
team.admins.push(user._id);
team.save((err, team) => {
res.status(204).json(team);
});
} else {
res.status(400).json({"error": "already admin"});
}
} else {
res.status(403).json({"error": "team id not correct"});
}
}
});
});
router.get('/:id/memberships/:user_id/demote', (req, res, next) => {
User.findOne({_id: req.params.user_id}, (err,user) => {
if (err) res.sendStatus(400);
else {
if (user.team.toString() == req.user.team._id.toString()) {
const team = req.user.team;
const adminIndex = team.admins.indexOf(user._id);
if(adminIndex > -1) {
team.admins.splice(adminIndex,1);
team.save((err, team) => {
res.status(204).json(team);
});
} else {
res.sendStatus(404);
}
} else {
res.sendStatus(403);
}
}
});
});
router.delete('/:id/memberships/:user_id', (req, res) => {
User.findOne({_id: req.params.user_id}).populate('team').exec((err,user) => {
if (err) res.sendStatus(400);
else {
const currentUserId = req.user._id.toString();
const team = req.user.team;
const isAdmin = (req.user.team.admins.filter( mem => {
return mem == currentUserId;
}).length == 1)
if (isAdmin) {
user.team = null;
user.payment_plan_key = "free";
user.save( err => {
const adminIndex = team.admins.indexOf(user._id);
if(adminIndex > -1) {
team.admins.splice(adminIndex,1);
team.save((err, team) => {
console.log("admin removed");
});
}
res.sendStatus(204);
});
} else {
res.status(403).json({"error": "not admin"});
}
}
});
});
module.exports = router;

View File

@ -1,16 +1,14 @@
"use strict";
var config = require('config');
const db = require('../../models/db');
const uuidv4 = require('uuid/v4');
const os = require('os');
require('../../models/schema');
var mailer = require('../../helpers/mailer');
var uploader = require('../../helpers/uploader');
var importer = require('../../helpers/importer');
var bcrypt = require('bcryptjs');
var crypto = require('crypto');
var crypo = require('crypto');
var swig = require('swig');
var async = require('async');
var _ = require('underscore');
@ -22,121 +20,241 @@ var URL = require('url').URL;
var express = require('express');
var router = express.Router();
var glob = require('glob');
router.get('/current', function(req, res, next) {
if (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);
console.log(req.user.team);
res.status(200).json(req.user);
} else {
res.status(401).json({"error":"user_not_found"});
}
});
// create user
router.post('/', function(req, res) {
if (!req.body["email"] || !req.body["password"]) {
res.status(400).json({"error":"email or password missing"});
return;
}
if (req.body["email"] && req.body["password"]) {
var email = req.body["email"].toLowerCase();
var nickname = req.body["nickname"];
var password = req.body["password"];
var password_confirmation = req.body["password_confirmation"];
var invite_code = req.body["invite_code"];
if (password_confirmation != password) {
res.status(400).json({"error":"password_confirmation"});
return;
}
if (config.invite_code && invite_code != config.invite_code) {
res.status(400).json({"error":"Invalid Invite Code."});
return;
}
if (!validator.isEmail(email)) {
res.status(400).json({"error":"email_invalid"});
return;
}
if (password_confirmation == password) {
if (validator.isEmail(email)) {
var createUser = function() {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(password, salt, function(err, hash) {
crypto.randomBytes(16, function(ex, buf) {
crypo.randomBytes(16, function(ex, buf) {
var token = buf.toString('hex');
var u = {
_id: uuidv4(),
var u = new User({
email: email,
account_type: "email",
nickname: nickname,
password_hash: hash,
prefs_language: req.i18n.locale,
preferences: {
language: req.i18n.locale
},
confirmation_token: token
};
});
db.User.create(u)
.error(err => {
res.sendStatus(400);
})
.then(u => {
var homeFolder = {
_id: uuidv4(),
u.save(function (err) {
if (err) res.sendStatus(400);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator_id: u._id
};
db.Space.create(homeFolder)
.error(err => {
res.sendStatus(400);
})
.then(homeFolder => {
u.home_folder_id = homeFolder._id;
u.save()
.then(() => {
// home folder created,
// auto accept pending invites
db.Membership.update({
"state": "active"
}, {
where: {
"email_invited": u.email,
"state": "pending"
creator: u
});
homeSpace.save((err, homeSpace) => {
if (err) res.sendStatus(400);
else {
u.home_folder_id = homeSpace._id;
u.save((err) => {
mailer.sendMail(u.email, req.i18n.__("confirm_subject"), req.i18n.__("confirm_body"), {
action: {
link: config.endpoint + "/confirm/" + u.confirmation_token,
name: req.i18n.__("confirm_action")
}
});
if (err) res.status(400).json(err);
else {
res.status(201).json({});
})
.error(err => {
res.status(400).json(err);
}
});
})
}
});
}
});
});
});
});
};
db.User.findAll({where: {email: email}})
.then(users => {
if (users.length == 0) {
User.find({email: email}, (function (err, users) {
if (err) {
res.status(400).json({"error":"password_confirmation"});
} else {
if (users.length === 0) {
var domain = email.slice(email.lastIndexOf('@')+1);
Domain.findOne({domain: domain}, function(err, domain) {
if(domain){
if(domain.edu) {
createUser();
} else {
res.status(400).json({"error":"domain_blocked"});
}
} else {
createUser();
}
});
} else {
res.status(400).json({"error":"user_email_already_used"});
}
})
}
}));
} else {
res.status(400).json({"error":"email_invalid"});
}
} else {
res.status(400).json({"error":"password_confirmation"});
}
} else {
res.status(400).json({"error":"email or password missing"});
}
});
router.get('/current', function(req, res, next) {
router.get('/oauth2callback/url', function(req, res) {
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(
config.google_access,
config.google_secret,
config.endpoint + "/login"
);
var url = oauth2Client.generateAuthUrl({
access_type: 'online',
scope: "email"
});
res.status(200).json({"url":url});
});
router.get('/loginorsignupviagoogle', function(req, res) {
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var plus = google.plus('v1');
var oauth2Client = new OAuth2(
config.google_access,
config.google_secret,
config.endpoint + "/login"
);
var loginUser = function(user, cb) {
crypo.randomBytes(48, function(ex, buf) {
var token = buf.toString('hex');
var session = {
token: token,
created_at: new Date()
};
if(!user.sessions)
user.sessions = [];
user.sessions.push(session);
user.save(function(err, user) {
cb(session);
});
});
};
var code = req.query.code;
oauth2Client.getToken(code, function(err, tokens) {
if (err) res.status(400).json(err);
else {
var apiUrl = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + tokens.access_token;
var finalizeLogin = function(session){
res.cookie('sdsession', session.token, { httpOnly: true });
res.status(201).json(session);
};
request.get(apiUrl, function(error, response, body) {
if (error) res.status(400).json(error);
else {
const data = JSON.parse(body);
const email = data.email;
const name = data.name;
User.findOne({email: email}, function (err, user) {
if (user) {
// login new google user
if (user.account_type == "google") {
// just login
loginUser(user, (session) => {
finalizeLogin(session);
});
} else {
res.status(400).json({"error":"user_email_already_used"});
}
} else {
const u = new User({
email: email,
account_type: "google",
nickname: name,
avatar_thumb_uri: body.picture,
preferences: {
language: req.i18n.locale
},
confirmed_at: new Date()
});
u.save(function (err) {
if (err) res.status(400).json(err);
else {
var homeSpace = new Space({
name: req.i18n.__("home"),
space_type: "folder",
creator: u
});
homeSpace.save(function(err, homeSpace) {
if (err) res.status(400).json(err);
else {
u.home_folder_id = homeSpace._id;
u.save(function(err){
if (err) res.sendStatus(400);
else {
mailer.sendMail(u.email, req.i18n.__("welcome_subject"), req.i18n.__("welcome_body"), {});
loginUser(u, function(session) {
finalizeLogin(session);
});
}
});
}
});
}
});
}
});
}
});
}
});
});
router.get('/ ', function(req, res, next) {
if (req.user) {
console.log(req.user.team);
res.status(200).json(req.user);
} else {
res.status(401).json({"error":"user_not_found"});
@ -144,15 +262,19 @@ router.get('/current', function(req, res, next) {
});
router.put('/:id', function(req, res, next) {
// TODO explicit whitelisting
var user = req.user;
console.log(req.params.id, user._id);
if (user._id == req.params.id) {
var newAttr = req.body;
newAttr.updated_at = new Date();
delete newAttr['_id'];
db.User.update(newAttr, {where: {"_id": user._id}}).then(function(updatedUser) {
res.status(200).json(newAttr);
User.findOneAndUpdate({"_id": user._id}, {"$set": newAttr}, function(err, updatedUser) {
if (err) {
res.sendStatus(400);
} else {
res.status(200).json(updatedUser);
}
});
} else {
res.sendStatus(403);
@ -170,41 +292,46 @@ router.post('/:id/password', function(req, res, next) {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(pass, salt, function(err, hash) {
user.password_hash = hash;
user.save().then(function() {
user.save(function(err){
if(err){
res.status(400).json(err);
}else{
res.sendStatus(204);
}
});
});
});
} else {
res.status(403).json({"error": "Please enter the correct current password."});
res.status(403).json({"error": "old password wrong"});
}
} else {
res.status(403).json({"error": "Access denied."});
res.status(403).json({"error": "wrong user"});
}
} else {
res.status(400).json({"error": "Please choose a new password with at least 6 characters."});
res.status(400).json({"error": "password_to_short"});
}
});
router.delete('/:id', (req, res, next) => {
const user = req.user;
if (user._id == req.params.id) {
if(user._id == req.params.id) {
if (user.account_type == 'email') {
if (bcrypt.compareSync(req.query.password, user.password_hash)) {
// TODO: this doesn't currently work.
// all objects (indirectly) belonging to the user have
// to be walked and deleted first.
user.destroy().then(err => {
user.remove((err) => {
if(err)res.status(400).json(err);
else res.sendStatus(204);
});
} else {
res.bad_request("Please enter the correct current password.");
res.bad_request("password_incorrect");
}
} else {
res.status(403).json({error: "Access denied."});
user.remove((err) => {
if(err)res.status(400).json(err);
else res.sendStatus(204);
});
}
}
else res.status(403).json({error: ""});
});
router.put('/:user_id/confirm', (req, res) => {
@ -230,8 +357,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 = os.tmpdir()+"/"+filename;
const localResizedFilePath = os.tmpdir()+"/resized_"+filename;
const localFilePath = "/tmp/"+filename;
const localResizedFilePath = "/tmp/resized_"+filename;
const writeStream = fs.createWriteStream(localFilePath);
const stream = req.pipe(writeStream);
@ -243,15 +370,19 @@ router.post('/:user_id/avatar', (req, res, next) => {
if (err) res.status(400).json(err);
else {
user.avatar_thumb_uri = url;
user.save().then(() => {
user.save((err, updatedUser) => {
if (err) {
res.sendStatus(400);
} else {
fs.unlink(localResizedFilePath, (err) => {
if (err) {
console.error(err);
res.status(400).json(err);
} else {
res.status(200).json(user);
res.status(200).json(updatedUser);
}
});
}
});
}
});
@ -260,45 +391,72 @@ router.post('/:user_id/avatar', (req, res, next) => {
});
});
router.post('/feedback', function(req, res, next) {
var text = req.body.text;
// FIXME
mailer.sendMail("support@example.org", "Support Request by " + req.user.email, text, {reply_to: req.user.email});
res.sendStatus(201);
});
router.post('/password_reset_requests', (req, res, next) => {
const email = req.query.email;
db.User.findOne({where: {"email": email}}).then((user) => {
User.findOne({"email": email}).exec((err, user) => {
if (err) {
res.status(400).json(err);
} else {
if (user) {
crypto.randomBytes(16, (ex, buf) => {
if(user.account_type == "email") {
crypo.randomBytes(16, (ex, buf) => {
user.password_reset_token = buf.toString('hex');
user.save().then(updatedUser => {
user.save((err, updatedUser) => {
if (err) res.status(400).json(err);
else {
mailer.sendMail(email, req.i18n.__("password_reset_subject"), req.i18n.__("password_reset_body"), {action: {
link: config.endpoint + "/password-confirm/" + user.password_reset_token,
name: req.i18n.__("password_reset_action")
}});
res.status(201).json({});
}
});
});
} else {
res.status(404).json({"error": "error_unknown_email"});
}
} else {
res.status(404).json({"error": "error_unknown_email"});
}
}
});
});
router.post('/password_reset_requests/:confirm_token/confirm', function(req, res, next) {
var password = req.body.password;
db.User
.findOne({where: {"password_reset_token": req.params.confirm_token}})
.then((user) => {
if (user) {
User
.findOne({"password_reset_token": req.params.confirm_token})
.exec((err, user) => {
if (err) {
res.sendStatus(400);
} else {
if(user) {
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(password, salt, function(err, hash) {
user.password_hash = hash;
user.password_token = null;
user.save().then(function(updatedUser) {
user.save(function(err, updatedUser){
if (err) {
res.sendStatus(400);
} else {
res.sendStatus(201);
}
});
});
});
} else {
res.sendStatus(404);
}
}
});
});
@ -310,4 +468,13 @@ router.post('/:user_id/confirm', function(req, res, next) {
res.sendStatus(201);
});
router.get('/:user_id/import', function(req, res, next) {
if (req.query.zip) {
res.send("importing");
importer.importZIP(req.user, req.query.zip);
} else {
res.sendStatus(400);
}
});
module.exports = router;

View File

@ -1,7 +1,7 @@
"use strict";
var config = require('config');
require('../../models/db');
require('../../models/schema');
var fs = require('fs');
var phantom = require('node-phantom-simple');

View File

@ -1,6 +1,7 @@
"use strict";
const config = require('config');
require('../models/schema');
const redis = require('../helpers/redis');
const express = require('express');
@ -8,11 +9,7 @@ const crypto = require('crypto');
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');
const qr = require('qr-image');
router.get('/', (req, res) => {
res.render('index', { title: 'Spaces' });
@ -54,6 +51,10 @@ router.get('/password-confirm/:token', (req, res) => {
res.render('spacedeck', { title: 'Signup' });
});
router.get('/team', (req, res) => {
res.render('spacedeck');
});
router.get('/de/*', (req, res) => {
res.redirect("/t/de");
});
@ -78,6 +79,10 @@ router.get('/en', (req, res) => {
res.redirect("/t/end");
});
router.get('/it', (req, res) => {
res.redirect("/t/en");
});
router.get('/account', (req, res) => {
res.render('spacedeck');
});
@ -90,6 +95,10 @@ router.get('/logout', (req, res) => {
res.render('spacedeck');
});
router.get('/users/oauth2callback', (req, res) => {
res.render('spacedeck');
});
router.get('/contact', (req, res) => {
res.render('public/contact');
});
@ -116,30 +125,180 @@ 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];
}
db.Space.findOne({where: {"edit_hash": token}}).then(function (space) {
if (space) {
if (req.accepts('text/html')){
res.redirect("/spaces/"+space._id + "?spaceAuth=" + token);
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')) {
if(req.accepts('text/html')){
res.status(404).render('not_found', { title: 'Page Not Found.' });
} else {
res.status(404).json({});
}
}
}
});
} else {
res.status(429).json({"error": "Too Many Requests"});
}
});
});
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('/users/byteam/:team_id/join', (req, res) => {
if (!req.user) {
const q = {confirmation_token: req.query.confirmation_token, account_type: "email", team: req.params.team_id};
User.findOne(q, (err, user) => {
if (err) {
res.status(400).json({"error":"session.users"});
} else {
if (user) {
crypto.randomBytes(48, function(ex, buf) {
const token = buf.toString('hex');
var session = {
token: token,
ip: req.ip,
device: "web",
created_at: new Date()
};
if (!user.sessions)
user.sessions = [];
user.sessions.push(session);
user.confirmed_at = new Date();
user.confirmation_token = null;
user.save(function(err, result) {
// FIXME
const secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging";
const domain = (process.env.NODE_ENV == "production") ? ".spacedeck.com" : ".spacedecklocal.de";
res.cookie('sdsession', token, { domain: domain, httpOnly: true, secure: secure});
res.redirect("/spaces");
});
});
} else {
res.status(404).json({"error": "not found"});
}
}
});
} else {
res.redirect("/spaces");
}
});
router.get('/teams/:id/join', function(req, res, next) {
if (req.user) {
if (!req.user.team) {
Team.findOne({"_id": req.params.id}, function(err, team) {
if (team) {
const idx = team.invitation_codes.indexOf(req.query.code);
if (idx >= 0) {
const u = req.user;
u.team = team;
if(!u.confirmed_at) {
u.confirmed_at = new Date();
}
u.payment_plan_key = team.payment_plan_key;
u.save(function(err) {
if (err) res.status(400).json(err);
else {
team.invitation_condes = team.invitation_codes.slice(idx);
team.save(function(err) {
team.invitation_codes = null;
var finish = function(team, users) {
User.find({"_id": {"$in": team.admins}}).exec((err, admins) => {
if(admins) {
admins.forEach((admin) => {
mailer.sendMail(
admin.email,
req.i18n.__("team_new_member_subject", team.name),
req.i18n.__("team_new_member_body", u.email, team.name)
);
});
}
});
}
User.find({team: team}, function(err, users) {
finish(team, users);
res.redirect("/spaces");
});
});
}
});
} else {
res.redirect("/spaces?error=team_code_notfound");
}
} else {
res.redirect("/spaces?error=team_notfound");
}
});
} else {
res.redirect("/spaces?error=team_already");
}
} else res.redirect("/login");
});
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;

View File

@ -1,172 +0,0 @@
"use strict";
const db = require('./models/db.js');
require("log-timestamp");
const config = require('config');
const redis = require('./helpers/redis');
const websockets = require('./helpers/websockets');
const http = require('http');
const path = require('path');
const _ = require('underscore');
const favicon = require('serve-favicon');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const swig = require('swig');
const i18n = require('i18n-2');
const helmet = require('helmet');
const express = require('express');
const app = express();
const serveStatic = require('serve-static');
const isProduction = app.get('env') === 'production';
// workaround for libssl_conf.so error triggered by phantomjs
process.env['OPENSSL_CONF'] = '/dev/null';
console.log("Booting Spacedeck Open… (environment: " + app.get('env') + ")");
app.use(logger(isProduction ? 'combined' : 'dev'));
i18n.expressBind(app, {
locales: ["en", "de", "fr"],
defaultLocale: "en",
cookieName: "spacedeck_locale",
devMode: (app.get('env') == 'development')
});
swig.setDefaults({
varControls: ["[[", "]]"] // otherwise it's not compatible with vue.js
});
swig.setFilter('cdn', function(input, idx) {
return input;
});
app.engine('html', swig.renderFile);
app.set('view engine', 'html');
if (isProduction) {
app.set('views', path.join(__dirname, 'build', 'views'));
app.use(favicon(path.join(__dirname, 'build', 'assets', 'images', 'favicon.png')));
app.use(express.static(path.join(__dirname, 'build', 'assets')));
} else {
app.set('views', path.join(__dirname, 'views'));
app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.png')));
app.use(express.static(path.join(__dirname, 'public')));
}
app.use(bodyParser.json({
limit: '50mb'
}));
app.use(bodyParser.urlencoded({
extended: false,
limit: '50mb'
}));
app.use(cookieParser());
app.use(helmet.frameguard())
app.use(helmet.xssFilter())
app.use(helmet.hsts({
maxAge: 7776000000,
includeSubDomains: true
}))
app.disable('x-powered-by');
app.use(helmet.noSniff())
//app.use(require("./middlewares/error_helpers"));
app.use(require("./middlewares/session"));
//app.use(require("./middlewares/cors"));
app.use(require("./middlewares/i18n"));
app.use("/api", require("./middlewares/api_helpers"));
app.use('/api/spaces/:id', require("./middlewares/space_helpers"));
app.use('/api/spaces/:id/artifacts/:artifact_id', require("./middlewares/artifact_helpers"));
app.use('/api/users', require('./routes/api/users'));
app.use('/api/memberships', require('./routes/api/memberships'));
const spaceRouter = require('./routes/api/spaces');
app.use('/api/spaces', spaceRouter);
spaceRouter.use('/:id/artifacts', require('./routes/api/space_artifacts'));
spaceRouter.use('/:id/memberships', require('./routes/api/space_memberships'));
spaceRouter.use('/:id/messages', require('./routes/api/space_messages'));
spaceRouter.use('/:id/digest', require('./routes/api/space_digest'));
spaceRouter.use('/:id', require('./routes/api/space_exports'));
app.use('/api/sessions', require('./routes/api/sessions'));
//app.use('/api/webgrabber', require('./routes/api/webgrabber'));
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
//app.use(require('./middlewares/404'));
if (app.get('env') == 'development') {
app.set('view cache', false);
swig.setDefaults({cache: false});
} else {
app.use(require('./middlewares/500'));
}
module.exports = app;
// CONNECT TO DATABASE
db.init();
// START WEBSERVER
const port = 9666;
const server = http.Server(app).listen(port, () => {
if ("send" in process) {
process.send('online');
}
}).on('listening', () => {
const host = server.address().address;
const port = server.address().port;
console.log('Spacedeck Open listening at http://%s:%s', host, port);
}).on('error', (error) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
websockets.startWebsockets(server);
redis.connectRedis();
/*process.on('message', (message) => {
console.log("Process message:", message);
if (message === 'shutdown') {
console.log("Exiting Spacedeck.");
process.exit(0);
}
});*/

View File

@ -26,12 +26,12 @@
}
}
&.artifact-text.text-blank [contentEditable=true]:not(.text-editing) p:first-child::after {
/*&.artifact-text.text-blank [contentEditable=true]:not(.text-editing) p:first-child::after {
content: "Double click to edit";
opacity: 0.25;
}
/*&.artifact-text.text-blank [contentEditable=true].text-editing p:first-child::after {
&.artifact-text.text-blank [contentEditable=true].text-editing p:first-child::after {
content: "Type here";
opacity: 0.25;
}*/
@ -469,10 +469,11 @@
color: black;
//@include user-select(none);
white-space: normal;
font-size: 36px;
font-size: 18px;
&.artifact-zone {
background-color: rgba(0,0,0,0.05);
border: 1px solid rgba(46,204,113,1);
background-color: rgba(46,204,113,0.025);
border-radius: 10px;
&:after {display: none; }
.shape {display: none; }
@ -552,10 +553,6 @@ body:not(.present-mode) {
cursor: grab !important;
}
.tool-note {
cursor: crosshair !important;
}
.artifact.state-idle {
.progress, .progress-text {
display: none;

View File

@ -7,6 +7,12 @@
.btn-group.colors {
.btn {
// padding: 4px;
// background-clip: content-box;
// padding-right: 2px;
// &:last-child {
// padding-right: 4px;
// }
box-shadow: inset 0 0 30px 0px rgba(40,40,40,0.1);
}
}
@ -58,7 +64,7 @@
backface-visibility: hidden;
cursor: pointer;
background-color: $light;
color: $black;
color: $medium;;
@include user-select(none);
&:last-child {border: none;}
@ -76,9 +82,12 @@
&.btn-link {
background-color: transparent;
color: $medium;
color: $medium;;
}
&.facebook {background-color: $facebook !important; color: white !important;}
&.twitter {background-color: $twitter !important; color: white !important; }
&.btn-round {
border-radius: 100px !important;
}
@ -87,10 +96,21 @@
border-radius: 6px !important;
}
// &.close {
// position: absolute;
// top: 15px;
// right: 15px;
// z-index: 4000;
// font-size: 40px;
// }
&.btn-nude {
min-width: 0 !important;
// font-size: inherit !important;
padding: 0 !important;
// height: auto !important;
background-color: transparent;
color: $medium;
}
&.btn-nude + .btn-nude {
@ -103,7 +123,7 @@
&.btn-stroke {
box-shadow: inset 0 0 0 1px $dark;
color: $black;
color: $dark !important;
background-color: transparent;
&:active {
box-shadow: inset 0 0 0 1px white;
@ -112,8 +132,9 @@
}
&.btn-stroke-darken {
border: 1px solid $black;
color: $black;
//box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.1);
color: $medium;
background-color: transparent;
&:active {
//box-shadow: inset 0 0 0 1px $dark;
@ -242,18 +263,9 @@
&.btn-transparent {
background-color: transparent;
color: $dark;
&.active {
//color: $black !important;
color: $white;
background-color: $black;
}
&.open {
//color: $black !important;
color: $white;
background-color: $black;
border-radius: 0;
}
color: $medium;
&.active {color: $darker !important; }
&.open {color: white !important; }
}
&.btn-transparent-medium {
@ -301,7 +313,7 @@
&.btn-dark {
background-color: $dark ;
color: $white;
color: $medium;
}
&.btn-medium {
@ -469,6 +481,7 @@
&.btn-icon {
padding: 0px !important;
font-weight: bold;
max-width: 60px;
&.btn-xl { max-width: 80px; }
@ -495,6 +508,30 @@
}
}
&.btn-social {
position: relative;
&:hover .icon,
.number {
@include scale(0,0);
opacity: 0;
}
&:hover .number {
@include transition( all 0.1s 0.1s ease-in-out);
@include scale(1,1);
opacity: 1;
}
.number,
.icon {
@include transition( all 0.1s 0s ease-in-out);
position: absolute;
top: 0;
left: 0;
}
}
&.btn-md.btn-icon-labeled {
.icon:before {
line-height: 29px;
@ -530,6 +567,7 @@
.icon:before {line-height: 42px; }
.icon-label {
font-size: 11px;
text-transform: capitalize;
text-align: center;
margin: 8px 0;
display: block;
@ -542,7 +580,7 @@
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 0px;
font-weight: 300;
font-weight: bold;
}
&.hover {
@ -676,6 +714,7 @@
}
> * {
border-radius: 0 !important;
background-clip: padding-box;
width: 100%;
float: left;
@ -736,7 +775,7 @@
}
}
.btn-group {
//@include scale(0,0);
@include scale(0,0);
//@include transition( all 0.1s 0s ease-in-out);
position: absolute;
@ -748,7 +787,7 @@
margin-left: -12px;
.btn {
//@include scale(0,0);
@include scale(0,0);
//@include transition( all 0.1s 0.05s ease-in-out);
@ -940,7 +979,31 @@
}
}
// !btn-group
.btn-group.bottom-left > .btn {
border-radius: 0px;
&:first-child{
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
&.last,
&:last-child{
border-top-right-radius: 3px;
border-bottom-right-radius: 0px;
}
}
.btn-xyz {
position: relative;
display: inline-block;
line-height: 0px;
padding: 0px;
font-size: 0px;
vertical-align: middle;
white-space: nowrap;
@include clearfix;
min-height: 44px;
}
.btn-group {
position: relative;
@ -951,16 +1014,13 @@
vertical-align: middle;
white-space: nowrap;
//border: 1px solid $dark;
border-radius: 5px;
&.dark {
border-radius: $radius;
background-color: $dark;
color: $white;
color: $lighter;
.btn {
color: $white;
color: $lighter;
}
}

View File

@ -96,14 +96,15 @@
border-bottom-right-radius: $radius*3;
}
.dialog-account {
width: 600px;
margin: auto;
margin-top: 100px;
}
.dialog {
font-size: 13px;
ol, ul, p {
font-size: inherit;
}
> .btn-block:last-child {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
@ -111,21 +112,24 @@
border-bottom-right-radius: $radius*3;
}
position: absolute;
font-size: 15px;
border: 1px solid black;
box-shadow: 0 0 30px 1px rgba(0, 0, 0, 0.15);
border-radius: 5px;
min-width: 200px;
@include backface-visibility(hidden);
white-space: normal;
z-index: 1000;
position: absolute;
// white-space: normal;
opacity: 0;
@include transition(all 0.125s ease-in-out);
@include user-select(none);
@include transition( all 0.125s ease-in-out);
pointer-events: none;
background-color: $light;
color: $dark;
&.dark {
background-color: $dark;
}
color: $medium;
&.dark {background-color: $dark; }
border-radius: $radius*3;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.05), 0 2px 7px rgba(0, 0, 0, 0.1);
.dialog-tabs-wrapper {
overflow: hidden;
@ -146,13 +150,15 @@
&:hover span {color: $dark; }
&.open span {
background-color: white;
background-color: $light;
color: $dark;
opacity: 1;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.05), 0 2px 7px rgba(0, 0, 0, 0.1) !important;
border-bottom-right-radius: 0px !important;
border-bottom-left-radius: 0px !important;
border-top-left-radius: $radius*3;
border-top-right-radius: $radius*3;
}
&:first-child span {
@ -194,6 +200,7 @@
text-align: center;
}
.dialog-section {
&:first-child {border: none !important; }
border-top: 2px solid rgba(0,0,0,0.1);
@ -221,13 +228,4 @@
h4 .icon {
height: 38px;
}
// account dialog
&.dialog-freestanding {
margin: auto;
position: relative;
top: 150px;
border: none;
width: 800px;
}
}

View File

@ -43,6 +43,9 @@ $predelay: 0;
&.hover:hover,
&.open {
// &:before {opacity: 0.125; }
// pointer-events: auto;
background-color: $dark;
background-color: $light;
> * {
@ -108,8 +111,8 @@ $predelay: 0;
}
&:last-child > .btn{
border-top-right-radius: $radius;
border-bottom-right-radius: $radius;
border-top-right-radius: $radius ;
border-bottom-right-radius: $radius ;
}
}
}
@ -119,10 +122,6 @@ $predelay: 0;
position: relative;
vertical-align: middle;
a {
text-decoration: none;
}
&.dropdown-block {
display: block;
.dropdown-toggle {
@ -144,7 +143,8 @@ $predelay: 0;
&.light > .dropdown-menu,
&.light > .dialog {
background: white;
background: $light;
color: $medium;
}
> .dropdown-menu {
@ -189,6 +189,8 @@ $predelay: 0;
}
}
&.hover:hover > .dialog,
&.hover:hover > .dropdown-menu,
@ -204,7 +206,9 @@ $predelay: 0;
&.open {
> .dialog,
> .dropdown-menu {
//transform: translate3d(-50%, -50%, 100px) scale(1);
-webkit-transform: translate3d(-50%, -50%, 100px) scale(1);
-ms-transform: translate3d(-50%, -50%, 100px) scale(1);
transform: translate3d(-50%, -50%, 100px) scale(1);
}
}
@ -213,8 +217,10 @@ $predelay: 0;
left: 50%;
top: 50%;
margin-top: 0px;
//@include transform-origin(center center);
//transform: translate3d(-50%, -50%, 100px) scale(0.93,0.8);
@include transform-origin(center center);
-webkit-transform: translate3d(-50%, -50%, 100px) scale(0.93,0.8);
-ms-transform: translate3d(-50%, -50%, 100px) scale(0.93,0.8);
transform: translate3d(-50%, -50%, 100px) scale(0.93,0.8);
}
}
@ -224,8 +230,10 @@ $predelay: 0;
top: auto;
bottom: 100%;
margin-bottom: 16px;
//@include transform-origin(bottom left);
//transform: translate3d(-33%, 0%, 100px) scale(0.93,0.8);
@include transform-origin(bottom left);
-webkit-transform: translate3d(-33%, 0%, 100px) scale(0.93,0.8);
-ms-transform: translate3d(-33%, 0%, 100px) scale(0.93,0.8);
transform: translate3d(-33%, 0%, 100px) scale(0.93,0.8);
}
}
@ -235,8 +243,10 @@ $predelay: 0;
top: auto;
bottom: 100%;
margin-bottom: 16px;
//@include transform-origin(bottom center);
//transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
@include transform-origin(bottom center);
-webkit-transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
-ms-transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
}
}
@ -247,16 +257,10 @@ $predelay: 0;
top: 100%;
bottom: auto;
margin-top: -16px;
//@include transform-origin(top center);
//transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
}
}
&.top.left {
> .dialog,
> .dropdown-menu {
left: 70px;
margin-top: -60px;
@include transform-origin(top center);
-webkit-transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
-ms-transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
transform: translate3d(-50%, 0%, 100px) scale(0.93,0.8);
}
}
@ -266,18 +270,20 @@ $predelay: 0;
top: 100%;
bottom: auto;
left: auto;
right: 70px;
margin-top: -60px;
//@include transform-origin(top right);
//transform: translate3d(0%, 0%, 100px) scale(0.93,0.8);
right: 0;
margin-top: 16px;
@include transform-origin(top right);
-webkit-transform: translate3d(0%, 0%, 100px) scale(0.93,0.8);
-ms-transform: translate3d(0%, 0%, 100px) scale(0.93,0.8);
transform: translate3d(0%, 0%, 100px) scale(0.93,0.8);
}
&.hover:hover,
&.open {
> .dialog,
> .dropdown-menu {
//transform: translate3d(0%, 0%, 100px) scale(1);
-webkit-transform: translate3d(0%, 0%, 100px) scale(1);
-ms-transform: translate3d(0%, 0%, 100px) scale(1);
transform: translate3d(0%, 0%, 100px) scale(1);
}
}
@ -306,7 +312,9 @@ $predelay: 0;
> .dialog,
> .dropdown-menu {
//transform: translate3d(-50%, 0%, 100px) scale(1);
-webkit-transform: translate3d(-50%, 0%, 100px) scale(1);
-ms-transform: translate3d(-50%, 0%, 100px) scale(1);
transform: translate3d(-50%, 0%, 100px) scale(1);
}
}
}
@ -316,7 +324,9 @@ $predelay: 0;
&.open {
> .dialog,
> .dropdown-menu {
//transform: translate3d(-33%, 0%, 100px) scale(1) !important;
-webkit-transform: translate3d(-33%, 0%, 100px) scale(1) !important;
-ms-transform: translate3d(-33%, 0%, 100px) scale(1) !important;
transform: translate3d(-33%, 0%, 100px) scale(1) !important;
}
}
}
@ -324,7 +334,7 @@ $predelay: 0;
.dropdown {
/*&.options-3 {
&.options-3 {
&.option-1:after { margin-left: -68px;}
&.option-2:after { margin-left: -8px;}
&.option-3:after { margin-left: 52px;}
@ -338,9 +348,8 @@ $predelay: 0;
-webkit-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
}*/
}
/*
&:after {
@include transition( all 0.1s ease-in-out 0s);
content: "";
@ -353,24 +362,26 @@ $predelay: 0;
margin-left: -8px;
pointer-events: none !important;
left: 50%;
//transform: scale(0,0);
-webkit-transform: scale(0,0);
-ms-transform: scale(0,0);
transform: scale(0,0);
}
&.bottom:after, &.bottomleft:after {
//@include transform-origin(bottom center);
@include transform-origin(bottom center);
bottom: 100%;
border-bottom: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #303030;
border-left: 8px solid transparent;
}
*/
/*&.top:after {
&.top:after {
@include transform-origin(top center);
top: 100%;
border-bottom: 8px solid #303030;
border-right: 8px solid transparent;
border-top: 8px solid transparent;
border-left: 8px solid transparent;
}*/
}
}

View File

@ -254,6 +254,7 @@
// word-wrap: break-word;
.item {
box-shadow: 0 0 1pxrgba(0,0,0,0.1);
display: inline-block;
text-align: left;
padding-right: $folder-gutter*2;
@ -396,10 +397,7 @@
&:active { opacity: 0.95 !important; }
box-shadow: 0 0 30px 1px rgba(0, 0, 0, 0.15);
border: 1px solid black;
// ???
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.025), 0 2px 7px rgba(0, 0, 0, 0.025);
@include opacity(1);
color: $medium;
// color: white;
@ -478,6 +476,7 @@
left: 0px;
z-index: 100;
width: auto;
background-color: rgba(255,255,255,1);
.dropdown {
position: absolute;
@ -502,6 +501,30 @@
color: $dark;
text-align: left;
}
.item-social {
padding: 8px;
border-right: 2px solid rgba(0,0,0,0.025);
@include clearfix;
color: $medium;
.item-likes,
.item-comments,
.item-shares {
position: relative;
&:hover {
.icon {opacity: 0; }
.number {opacity: 1; }
}
.number {
position: absolute;
opacity: 0;
top: 0;
left: 0;
}
.icon {opacity: 0.5; }
}
}
}
.item-appendix {

View File

@ -28,6 +28,7 @@
line-height: 1.5;
width: 100%;
text-align: left;
color: $medium;
font-weight: normal;
cursor: pointer;
border-radius: $radius;

View File

@ -2,14 +2,24 @@
@import "mixins";
.input-select {
background-color: rgba(255,255,255,0.04);
background-image: url('images/select_arrow.gif');
// background-color: rgba(255,255,255,0.04);
// background-image: url('images/select_arrow.gif');
border-radius: $radius;
display: inline-block;
width: 100%;
}
@-moz-document url-prefix() {
select.input{
background-repeat: no-repeat;
background-position: right center;
cursor: pointer;
}
}
select {
-webkit-appearance:none;
// -moz-appearance:window;
appearance:none;
padding-left: 0px;
width: 100%;

View File

@ -23,6 +23,7 @@ input:invalid {
top: 0;
right: 0;
line-height: 1;
font-size: 10px;
margin: 12px;
color: red;
margin-right: 25px;
@ -112,26 +113,43 @@ select {
&.input-white {
background-color: white;
color: $medium;
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.05), inset 0 0px 4px rgba(0, 0, 0, 0.1);
}
&.input-light {
background-color: $light;
color: $medium;
}
&.input-dark {
background-color: $darker;
color: $medium;
}
&.input-lighten {
background-color: rgba(255,255,255,0.05);
color: $medium !important;
}
&.input-darken {
background-color: rgba(0,0,0,0.05);
color: $medium;
}
&.input-transparent {
background-color: transparent;
color: $medium;
}
// &:focus {color: white; }
&:invalid {
// background-color: rgba(198,101,84,0.05);
// color: rgba(198,101,84,0.75)
&:after {
}
}
@include input-focus();

View File

@ -69,27 +69,26 @@
}
.handles {
//border: 1px solid rgba(255,255,255,0.5);
// background-color: rgba(40,140,215,0.45);
border: 1px solid rgba(255,255,255,0.5);
position: absolute;
left: 0;
top: 0;
bottom: -1;
bottom: 0;
right: 0;
z-index: 800;
pointer-events: none;
background: rgba(255,255,255,0.1);
&:after{
border: 4px dotted #000000;
border: 1px dotted rgba(40,140,215,1);
content: "";
display: block;
position: absolute;
height: auto;
width: auto;
top: 0px;
left: 0px;
right: 0px;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
}
}
@ -98,7 +97,7 @@
border: 8px solid rgba(255,255,255,0.5);
&:after{
border: 8px dotted #000000;
border: 8px dotted rgba(40,140,215,1);
top: -4px;
left: -4px;
right: -4px;
@ -333,14 +332,15 @@
pointer-events:auto;
z-index: 2000;
position: absolute;
width: 30px !important;
height: 30px !important;
border-radius: 100%;
margin: -15px;
border: 1px solid black;
margin: -5px;
padding: 4px;
border: 1px solid rgba(0,0,0,0.25);
&:hover {
background-color: black;
background-color: rgba(255,255,255,0.5);
cursor: move;
}
}
@ -428,7 +428,14 @@
border-style: solid;
border-width: 10px;
border-color: transparent;
-webkit-background-clip: padding-box;
-moz-background-clip: padding-box;
background-clip: padding-box;
-webkit-transition: all .05s ease-in-out;
-moz-transition: all .05s ease-in-out;
-ms-transition: all .05s ease-in-out;
-o-transition: all .05s ease-in-out;
transition: all .05s ease-in-out;
}

View File

@ -5,6 +5,7 @@
.header-left,
.header-right {
position: absolute;
//@include transition( all 0.25s ease-in-out);
@include backface-visibility(hidden);
z-index: 3000;
top: 10px;
@ -26,21 +27,21 @@
.home {
margin-top: -20px;
margin-left: -20px;
// .icon {color: $dark; }
}
.header-left {
@include transform-origin(center left);
left: 0;
padding-left: 10px;
padding-left: 20px;
padding-top: 20px;
}
.header-right {
@include transform-origin(center right);
right: 0;
padding-right: 20px;
padding-top: 20px;
padding-right: 10px;
}
.header-center {
@include transform-origin(center center);
width: 100%;
left: 0;
right: 0;
@ -55,7 +56,7 @@
}
}
.header-left > * { margin-right: 10px; }
.header-right > * { margin-left: 10px; }
.header-right > * { margin-left: 5px; }
.header-right { font-size: 0;}
.title {
@ -89,3 +90,21 @@
opacity: 0.5;
}
}
.present-mode #space-header {
background-color: transparent !important;
}
#space-siblings {
background-color: rgba(245, 245, 245, 0.95);
padding: 35px;
max-height: 450px;
overflow-y: scroll;
margin-top: 54px;
border-bottom: 1px solid #eee;
.btn {
margin-bottom: 50px;
}
}

View File

@ -85,12 +85,3 @@
transform: rotateZ(45deg) translateX(-8px);
}
.icon-svg {
background-size: 26px;
background-position: center;
background-repeat: no-repeat;
}
.icon-sd6 {
background-image: url(/images/sd6-icon-white.svg);
}

View File

@ -1,52 +1,257 @@
@import "vars";
#landing-header {
background-color: white;
background-color: rgba(255,255,255,0.3);
height: 64px;
position: relative;
position: absolute;
top: 0;
left: 0;
right: 0;
}
#landing {
margin-top: 100px;
.landing-keyvisual-wrapper {
background-image: url("../images/sd5-keyvisual-compressed.jpg");
background-size: cover;
background-position: center;
padding-top: 40px;
padding-bottom: 40px;
}
section {
margin-left: 300px;
.landing-plans-wrapper {
background-image: url("../images/sd5-hero2-compressed.jpg");
background-size: cover;
background-position: center;
padding-top: 80px;
padding-bottom: 100px;
}
> * {
max-width: 600px;
.landing-box {
width: 800px;
margin: auto;
max-width: 90%;
background-color: white;
padding: 40px;
margin-bottom: 80px;
margin-top: 80px;
position: relative;
box-shadow: 0px 0px 50px rgba(0,0,0,0.2);
h1 {
margin-bottom: 20px;
}
&.black {
background-color: #222;
color: white;
padding: 20px;
text-align: center;
}
&.overlap {
position: absolute;
z-index: 2;
margin-top: -65px;
left: 50%;
top: 0px;
margin-left: -250px;
width: 500px;
}
&.screenshot {
width: 90%;
max-width: 90%;
padding: 20px;
box-shadow: none;
background-color: transparent;
img {
width: 100%;
position: absolute;
top: 0px;
left: 0px;
opacity: 0.3;
}
}
&.landing-box-left {
margin-left: 30px;
}
}
.lead-xxl {
}
.lead {
margin-bottom: 20px;
}
.lead-xl {
}
.plans-box {
background: linear-gradient(to bottom, #FEFFFF 25%,#D0D8E2 100%);
padding: 40px;
border-radius: 9px;
}
.landing-box.plans-box {
margin-top: 200px;
width: 900px;
}
.plans-table {
tr {
vertical-align: top;
}
th {
font-size: 42px;
padding-top: 40px;
text-align: center;
}
th.best-plan {
padding-top: 20px;
font-size: 48px;
padding-bottom: 0px;
}
td {
padding: 20px;
width: 30%;
p, li {
font-size: 18px;
}
li {
margin-bottom: 10px;
}
}
td.best-plan {
width: 40%;
p {
font-size: 22px;
}
}
td li {
list-style-type: none;
text-align: center;
}
ul {
margin: 0 !important;
padding: 0 !important;
}
.upgrade-buttons {
text-align:center;
margin-top:20px;
}
}
.logo-row {
position: relative;
padding: 80px;
background-color: white;
text-align: center;
width: 100%;
&.blue {
background-color: $blue;
color: white;
}
}
.logo-row div {
display: inline-block;
width: 200px;
}
.landing-row {
background-color: white;
padding-bottom: 80px;
padding-top: 40px;
}
#keyvisual {
border-radius: 20px;
box-shadow: 0px 0px 20px #eee;
width: 640px;
height: 420px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: url('/images/landing/spacedeck-screenshot1.jpg');
background-color: white;
margin: auto;
margin-top: 40px;
margin-bottom: 40px;
border: 1px solid #eee;
}
#legal {
.landing-box {
width: 800px;
}
}
.footer {
margin-left: 300px;
margin-top: 100px;
margin-bottom: 100px;
padding: 40px;
padding-bottom: 80px;
text-align: center;
color: $medium;
a {
margin-right: 20px;
}
}
@media screen and (max-width: 1000px) {
#landing {
section {
margin-left: 20px;
margin-right: 20px;
@media screen and (min-width: 801px) {
.plans-table-mobile {
display: none;
}
}
.footer {
margin-left: 20px;
margin-right: 20px;
}
@media screen and (max-width: 800px) {
ul.lead.lead-xl, p.lead.lead-xl, ol.lead.lead-xl {
font-size: 20px !important;
}
.header-right {
right: auto;
padding-left: 10px;
padding-right: 20px;
padding-top: 80px;
> span:first-child {
display: none;
}
}
#folder-wrapper {
padding-top: 128px;
.plans-table {
display: none;
}
.plans-table-mobile {
display: block;
tbody {
display: block;
width: 100%;
}
tr {
display: block;
width: 100%;
}
td, th {
display: block;
width: 100%;
}
ul, li {
width: 100%;
}
}
}

View File

@ -2,6 +2,7 @@
@import "mixins";
.wrapper {
//@include transition( all 0.25s ease-in-out);
position: relative;
margin: auto;
max-width: 1160px;

View File

@ -59,8 +59,8 @@
}
.close {
margin-left: 44px;
margin-bottom: 44px;
position: fixed;
margin: 44px 44px;
.icon {display: block; }
}
@ -135,6 +135,7 @@
outline: none;
display: inline-block;
text-align: left;
@include user-select(none);
border-radius: $radius*3;
background-color: $light !important;
@ -145,6 +146,7 @@
.modal-header {
padding: 30px 40px;
position: relative;
color: $medium;
}
.close-search {
@ -277,5 +279,25 @@
// Footer (for actions)
.modal-footer {
margin-top: 20px;
// border-bottom-left-radius: $radius;
// border-bottom-right-radius: $radius;
// background-color: $dark !important;
// padding: 40px;
// padding-top: 0px;
// text-align: right; // right align buttons
@include clearfix(); // clear it in case folks use .pull-* classes on buttons
// Properly space out buttons
// .btn + .btn {
// margin-left: 5px;
// margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
// }
// // but override that for button groups
// .btn-group .btn + .btn {
// margin-left: -1px;
// }
// // and override it for block buttons as well
// .btn-block + .btn-block {
// margin-left: 0;
// }
}

12
styles/normalize.scss vendored
View File

@ -1,5 +1,17 @@
/*! normalize.css v3.0.0 | MIT License | git.io/normalize */
//
// 1. Set default font family to sans-serif.
// 2. Prevent iOS text size adjust after orientation change, without disabling
// user zoom.
//
html {
font-family: sans-serif; // 1
-ms-text-size-adjust: 100%; // 2
-webkit-text-size-adjust: 100%; // 2
}
//
// Remove default margin.
//

View File

@ -27,5 +27,6 @@
right: 0;
z-index: 800;
pointer-events: none;
opacity: 0.25;
display: block;
}

View File

@ -6,18 +6,22 @@
li {
&.checked {
&:before {background-color: $medium !important; }
> a,
> span {
color: $medium;
}
}
&:hover {
&:before {background-color: $medium; }
> a,
> span {
background-color: rgba(0,0,0,0.025) !important;
}
}
&:before {background-color: $medium; }
> a,
> span {
color: $medium;
@ -26,7 +30,7 @@
}
.select-list {
&:empty:before {
&:empty:before{
position: absolute;
top: 50%;
left: 50%;
@ -41,14 +45,17 @@
opacity: 0.5;
}
-webkit-mask-image: -webkit-gradient(linear, left top, left 15px, from(rgba(0,0,0,0)), to(rgba(0,0,0,0.5)));
background-clip: padding-box;
//font-size: 15px;
//line-height: 14px;
font-size: 15px;
line-height: 14px;
list-style: none;
margin: 0px;
padding: 15px 0;
text-align: left;
// background-color: $dark;
color: $medium;
border-radius: $radius;
.divider + li span {border: none !important; }
@ -83,11 +90,15 @@
}
&:hover {
background-color: black;
// background-color: rgba(0,0,0,0.025);
&:before {
background-color: $medium;
display: block;
}
> a,
> span {
color: white;
color: $medium;
color: $dark;
}
}
@ -115,8 +126,9 @@
display: block;
cursor: pointer;
white-space: nowrap;
color: $medium;
margin: 0 25px;
padding: 10px 0px;
padding: 16px 3px;
// line-height: 50px;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -118,7 +118,7 @@
padding: 0 !important;
.wrapper {
border: 4px solid black;
border: 1px dotted rgba(128,128,128,0.5);
transition-duration: 0.25s;
transition-property: width, height, background-color;
@ -132,27 +132,32 @@
max-height: 100%;
position: relative;
overflow: scroll;
/** {
-moz-user-select: none !important; // firefox has selection problems
}*/
}
.snap-ruler-h {
pointer-events: none;
position: fixed;
z-index: 2000;
z-index: 0;
right: 0px;
height: 1px;
background-color: black;
background-color: rgba(0,0,0,0.5);
left: 0px;
}
.snap-ruler-v {
pointer-events: none;
position: fixed;
z-index: 2000;
z-index: 0;
top: 0px;
bottom: 0px;
width: 1px;
background-color: black;
background-color: rgba(0,0,0,0.5);
}
.cursor {
@ -222,12 +227,30 @@
}
#space {
// user-select: all;
/*-webkit-user-select: all;
-ms-user-select: all;
-moz-user-select: all;
user-select: all;*/
position: relative;
height: 100% !important;
//padding-top: 64px !important;
background-color: #eee;
}
#made-with {
position: fixed;
width: 100%;
bottom: 0;
padding: 12px;
opacity: 0.25;
a {color: $dark; }
p {
text-align: center;
font-size: 11px;
}
}
#baseline {
position: absolute;
width: 100%;
@ -275,8 +298,8 @@
.space-bounds {
position: absolute;
left: 0;
top: 0;
left: 0px;
top: 0px;
pointer-events: none;
background-size: cover;
background-repeat: no-repeat;

View File

@ -65,15 +65,10 @@
html,
body {
height:100%;
-webkit-tap-highlight-color: transparent;
background-color: white;
color: $black;
}
body {
max-width: 100%;
padding: 0px;
text-rendering: optimizeLegibility;
cursor: default;
background-color: $light;
color: $darker;
}
*[contenteditable="true"] {
@ -86,12 +81,70 @@ body {
@include box-sizing(border-box);
}
body {
max-width: 100%;
padding: 0px;
text-rendering: optimizeLegibility;
//@include user-select(none);
cursor: default;
}
.img img {
max-width: 100%;
height: auto;
}
/*.layer {
.plan {
color: $medium;
border-radius: $radius;
display: inline-block;
padding: 30px;
background-color: transparent;
border: 2px solid rgba(0,0,0,0.05);
width: 100%;
&.active {
background-color: white;
border: none;
}
h4 {
color: black;
margin-bottom: 0px;
}
p {
font-size: 13px;
line-height: 1.4;
margin-top: 5px;
margin-bottom: 5px;
}
ul {
list-style: none;
font-size: 10px;
margin: 0px;
padding: 0px;
border-top: 2px solid rgba(0,0,0,0.05);
padding-top: 20px;
margin-top: 20px;
margin-bottom: 20px;
li {padding-top: 2px; }
}
}
#startup {
background-position: center;
background-image:url(/images/diamond.svg);
background-repeat: no-repeat;
}
#home {
background-color: white;
}
.layer {
@include transition( all 0.2s ease-in-out);
@include backface-visibility(hidden);
position: absolute;
@ -119,7 +172,7 @@ body {
pointer-events: auto;
opacity: 1;
}
}*/
}
[draggable] {
-moz-user-select: none;

View File

@ -8,9 +8,10 @@
}
.table {
width: 100%;
color: $medium;;
font-family: $main-font;
border-radius: $radius;
border: 2px solid rgba(0,0,0,0.0125);
// border-radius: $radius;
// border: 2px solid rgba(0,0,0,0.0125) !important;
}
.table thead > tr > th:first-child,

View File

@ -19,23 +19,50 @@
}
margin: auto;
//text-align: center;
position: fixed;
top: 20px;
bottom: 0px;
//width: 100%;
z-index: 3000;
padding: 0;
padding: $gutter-b;
font-size: 0;
line-height: 0;
box-shadow: 0 0 30px 1px rgba(0, 0, 0, 0.15);
border: 1px solid black;
border-radius: 5px;
// FIXME questionable?
transition-duration: 0.15s;
transition-timing-function: ease-in-out;
transition-delay: initial;
transition-property: opacity, transform;
@include backface-visibility(hidden);
@include translate3d(0, 10px, 0);
pointer-events: none !important;
opacity: 0;
&.out {
@include translate3d(0, 10px, 0);
* {pointer-events: none !important; }
button, input, .dialog {
display: none;
}
}
&.in {
@include translate3d(0, 0, 0);
&.out {
@include translate3d(0, 10px, 0);
* {pointer-events: none !important; }
}
}
> * {
margin: 0 2px;
margin-top: 4px;
pointer-events: auto !important;
&.out {
margin: 0;
opacity: 0;
}
}
&.toolbar-vertical {
@ -160,6 +187,7 @@
}
.toolbar-properties {
bottom: 64px;
z-index: 0;
&.in {
@ -168,12 +196,12 @@
.icon-sm {
z-index: 110;
//background-color: #222;
background-color: #222;
border-radius: 50px;
}
.jewel {
border: 2px solid #888;
border: 2px solid rgba(255,255,255,0.5);
background-color: transparent;
color: #989898;
width: 36px;
@ -200,22 +228,5 @@
.toolbar-elements > .btn-group,
.toolbar-properties > .btn-group {
//box-shadow: 0 0 30px rgba(0,0,0,0.5);
background-color: $white;
}
.toolbar-elements {
left: 20px;
}
.toolbar-properties {
right: 30px;
}
.zoom-bar {
position: absolute;
bottom: 30px;
right: 30px;
box-shadow: 0 0 30px 1px rgba(0, 0, 0, 0.15);
border: 1px solid black;
box-shadow: 0 0 30px rgba(0,0,0,0.5);
}

View File

@ -33,6 +33,9 @@
@include translate3d(0, 0, 0);
// @include backface-visibility(hidden);
-webkit-perspective: 1000;
-moz-perspective: 1000;
-ms-perspective: 1000;
perspective: 1000;
.panel-toggles {
@ -96,6 +99,9 @@
display: table-cell;
vertical-align: middle;
// @include backface-visibility(hidden);
-webkit-perspective: 1000;
-moz-perspective: 1000;
-ms-perspective: 1000;
perspective: 1000;
z-index: 1000;

View File

@ -1,8 +1,6 @@
@import "vars";
@import "mixins";
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;900&display=swap');
body {
background-color: $light;
color: $medium;
@ -27,7 +25,7 @@ hr {
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
color: inherit;
font-family: inherit;
font-weight: 900;
font-weight: 500;
line-height: 1.3;
margin-top: 0px;
margin-bottom: 1em;
@ -48,7 +46,8 @@ strong {font-weight: 500; }
small {font-size: 75%; }
a {
color: black;
text-decoration: none;
color: $medium;
}
dl {

View File

@ -9,6 +9,13 @@ $green: #2ecc71;
$red: #ff5955;
$yellow: #f1c40f;
$light: #f5f5f5;
$lightish: #eee;
$facebook: #3e5b97;
$twitter: #2aa7de;
$color-1 : #4a2f7e; // purple
$color-2 : #9b59b6; // lilac
$color-3 : #3498db; // blue
@ -25,18 +32,15 @@ $black: #111; // black
$darker: #292929;
$dark: #222; // dark
$medium: #888; // medium
$light: #f5f5f5;
$lightish: #eee; // fixme
$lighter: #989898;
$white: #ffffff;
$sidebar-width: 280px;
$main-font: Inter;
$sec-font: Inter;
$main-font: Avenir W01;
$sec-font: Avenir W01;
$font-size: 20px;
$line-height: 1.5em;
$font-size: 18px;
$line-height: 24px;
$gutter-a: 10px;
$gutter-b: 20px;

View File

@ -1,34 +1,62 @@
{% extends 'layouts/outer.html' %}
{% block title %}Spacedeck{% endblock %}
{% block title %}[[ __("welcome") ]]{% endblock %}
{% block content %}
<div id="landing">
<section>
<h1>Work Together, Visually.</h1>
<p>
Whenever you need to lay out pictures, text notes, video and audio clips on a blank canvas,
Spacedeck can help you.
<div class="landing-keyvisual-wrapper">
<div class="landing-box">
<h2>[[__("landing_title")]]</h2>
<p class="lead">
<a href="/signup" class="btn btn-primary btn-block btn-xl">[[__("signup")]]</a>
</p>
<p>
Spacedeck is a browser based application. It is the right tool for you if you want to quickly put together a collage of your idea or concept, either for yourself or to share it with teammembers, clients or students. Changes are updated in realtime.
<p class="lead">
<a href="/login" class="btn btn-primary btn-block btn-xl">[[__("login")]]</a>
</p>
<p>
Spacedeck is not meant for creating polished designs, but it is a good fit for:
<p class="lead">
[[__("landing_claim")]]
</p>
<p class="lead">
[[__("landing_example")]]
</p>
<ul>
<li>Moodboards</li>
<li>Collages</li>
<li>Teaching (Virtual Blackboards)</li>
<li>Shared Whiteboards</li>
<li>Design Thinking</li>
<li class="lead">
[[__("landing_features_1") | safe ]]
</li>
<li class="lead">
[[__("landing_features_2") | safe ]]
</li>
<li class="lead">
[[__("landing_features_3") | safe ]]
</li>
<li class="lead">
[[__("landing_features_4") | safe ]]
</li>
<li class="lead">
[[__("landing_features_5") | safe ]]
</li>
<li class="lead">
[[__("landing_features_6") | safe ]]
</li>
<li class="lead">
[[__("landing_features_7") | safe ]]
</li>
</ul>
<img src="/images/sd6-screenshot.png" alt="Screenshot of Spacedeck 6.0">
<p>
The hosted version of Spacedeck 6.0 is currently in beta and invite only. You can also self-host and <a href="https://github.com/spacedeck/spacedeck-open">participate in the open source development</a>.
</p>
</section>
</div>
</div>
</div>
{% endblock %}

View File

@ -9,25 +9,39 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link href="[[ '/images/favicon.png' | cdn ]]" rel="icon" type="image/x-icon" />
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,700,600,800,300|Montserrat:400,700|EB+Garamond|Vollkorn|Lato|Roboto|Source+Code+Pro|Ubuntu|Raleway|Playfair+Display|Crimson+Text' rel='stylesheet' type='text/css'>
<link type="text/css" rel="stylesheet" href="https://fast.fonts.net/cssapi/ee1a3484-4d98-4f9f-9f55-020a7b37f3c5.css"/>
<link rel="stylesheet" href="[[ '/stylesheets/style.css' | cdn ]]">
<script> var csrf_token = '[[ csrf_token ]]'; </script>
<!--script src="[[ '/javascripts/jquery-2.1.4.min.js' | cdn ]]"></script-->
<script src="[[ '/javascripts/jquery-2.1.4.min.js' | cdn ]]"></script>
</head>
<body>
<!--[if lt IE 10]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<header id="landing-header" class="header">
<div class="header-left">
<a class="btn btn-transparent btn-nude" href="[[config.endpoint]]/"><img src="[[ '/images/sd6-logo-black.svg' | cdn ]]" width="190"></a>
<a class="btn btn-transparent btn-nude" href="[[config.endpoint]]/"><img src="[[ '/images/sd5-logo.svg' | cdn ]]" width="190"></a>
</div>
<div class="header-right pull-right">
{% if !user %}
<span class="btn-group dark round">
{% if (locale != "de") %}<a href="/t/de" rel="alternate" hreflang="de" class="btn btn-transparent btn-md">Deutsch</a>{% endif %}
{% if (locale != "en") %}<a href="/t/en" rel="alternate" hreflang="en" class="btn btn-transparent btn-md">English</a>{% endif %}
{% if (locale != "fr") %}<a href="/t/fr" rel="alternate" hreflang="fr" class="btn btn-transparent btn-md">Français</a>{% endif %}
</span>
<a class="btn btn-md btn-dark btn-round" href="/login">[[__("login")]]</a>
<a class="btn btn-md btn-dark btn-round" href="/signup">[[__("signup")]]</a>
<a class="btn btn-md btn-blue btn-round" href="/signup">[[__("signup")]]</a>
{% else %}
<a class="btn btn-md btn-dark btn-round" href="/spaces">[[__("spaces")]]</a>
<a class="btn btn-md btn-blue btn-round" href="/spaces">[[__("spaces")]]</a>
<a class="btn btn-md btn-dark btn-round" href="/logout">[[__("logout")]]</a>
{% endif %}
</div>
@ -38,11 +52,8 @@
<div class="footer">
<p>
<div class="col-xs-6">
&copy; 2020 <a href="https://mntre.com">MNT Research GmbH</a>, Fehlerstr. 8, 12161 Berlin, Germany<br>
&copy; 2011–2019 Spacedeck GmbH (in liquidation)<br>
Source Code: <a href="https://github.com/mntmn/spacedeck-open">https://github.com/mntmn/spacedeck-open</a>
<br>
Font: <a href="https://rsms.me/inter/">Inter by rsms</a>
<a href="/contact">[[ __("contact") ]]</a>
<span style="color:#888">&copy; 2011–2018 The Spacedeck Open Developers <a href="https://github.com/spacedeck/spacedeck-open">https://github.com/spacedeck/spacedeck-open</a></span>
</div>
</p>
</div>

View File

@ -1,30 +1,16 @@
<header id="dialog-header" class="header" v-if="(active_view == 'account' && user)" v-cloak>
<div v-cloak class="header-left pull-left">
<a class="btn btn-dark btn-md btn-round btn-icon" href="/spaces">
<span class="icon icon-svg icon-sd6"></span>
</a>
<h5>Edit Account</h5>
</div>
<div id="team" class="dialog in" style="padding:100px;z-index:20000;position:absolute;width:100%;min-height:100%;background-color:#eee" v-if="active_view == 'account' && user" v-cloak>
<div class="header-right pull-right">
<a class="btn btn-dark btn-md btn-round btn-icon" href="/spaces">
<span class="icon icon-cross-0"></span>
</a>
</div>
</header>
<div class="dialog-freestanding dialog in" v-if="active_view == 'account' && user" v-cloak>
<a href="/spaces" class="btn btn-round btn-icon btn-stroke-darken btn-md pull-right" style="position:absolute;top:30px;right:30px"><span class="icon icon-cross-0"></span></a>
<div class="dialog-tabs" style="margin:auto">
<div class="dialog-tab" v-bind:class="{open:account=='profile'}" v-on:click="account='profile'"><span>[[__("profile_caption")]]</span></div>
<div class="dialog-tab" v-bind:class="{open:account=='language'}" v-on:click="account='language'"><span>[[__("language_caption")]]</span></div>
<div class="dialog-tab" v-bind:class="{open:account=='notifications'}" v-on:click="account='notifications'"><span>[[__("notifications_caption")]]</span></div>
<div class="dialog-tab" v-bind:class="{open:account=='password'}" v-on:click="account='password'"><span>[[__("password_caption")]]</span></div>
<div class="dialog-tab" v-if="user.account_type=='email'" v-bind:class="{open:account=='password'}" v-on:click="account='password'"><span>[[__("password_caption")]]</span></div>
<div class="dialog-tab" v-bind:class="{open:account=='terminate'}" v-on:click="account='terminate'"><span>[[__("terminate_caption")]]</span></div>
</div>
<div class="dialog-section text-left">
<div class="dialog-section text-left" style="background-color:#f5f5f5;padding-top:40px;padding-bottom:40px">
<div class="collapse" v-bind:class="{in:account=='profile'}">
<div class="labels-inline relative" style="margin-bottom:40px">
<div class="form-group">
@ -78,7 +64,21 @@
v-on:change="user.email_changed=true"
placeholder="mail@example.com">
<button class="btn btn-md btn-dark" v-on:click=" save_user()" style="margin-top:20px">Save</button>
<button class="btn btn-md btn-darken" v-if="user.account_type=='email'" v-on:click=" save_user()" style="margin-top:20px">[[__("ok")]]</button>
</div>
<div class="form-group" v-if="!user.confirmed_at">
<p v-if="!user.confirmed_at && !account_confirmed_sent">[[__("confirmation_sent_long")]]</p>
<span
class="btn btn-xs btn-stroke-darken btn-round"
v-on:click=" confirm_again()"
v-if="!user.confirmed_at && !account_confirmed_sent"
>[[__("send_again")]]</span>
<p v-if="account_confirmed_sent">
<span class="icon icon-check"></span> <span>[[__("confirmation_sent_another")]]</span>
</p>
</div>
</div>
</div>
@ -86,15 +86,15 @@
<div class="collapse" v-bind:class="{in:account=='language'}">
<div class="modal-section">
<label class="radio" v-bind:class="{checked
: user.prefs_language=='en'}" v-on:click="save_user_language('en')">
: user.preferences.language=='en'}" v-on:click="save_user_language('en')">
<input type="radio" id="user-preferences_language" name="language" value="en"><span>English</span>
</label>
<hr/>
<label class="radio" v-bind:class="{checked: user.prefs_language=='de'}" v-on:click="save_user_language('de')">
<label class="radio" v-bind:class="{checked: user.preferences.language=='de'}" v-on:click="save_user_language('de')">
<input type="radio" id="user-preferences_language" name="language" value="de"><span>Deutsch</span>
</label>
<hr/>
<label class="radio" v-bind:class="{checked: user.prefs_language=='fr'}" v-on:click="save_user_language('fr')">
<label class="radio" v-bind:class="{checked: user.preferences.language=='fr'}" v-on:click="save_user_language('fr')">
<input type="radio" id="user-preferences_language" name="language" value="fr"><span>Français</span>
</label>
</div>
@ -104,8 +104,8 @@
<div class="modal-section labels-inline">
<div class="form-group">
<label class="checkbox"
v-bind:class="{checked: user.prefs_email_notifications}"
v-on:click="account_save_user_notifications(!user.prefs_email_notifications);">
v-bind:class="{checked: user.preferences.email_notifications}"
v-on:click="account_save_user_notifications(!user.preferences.email_notifications);">
<span>[[__('notifications_option_chat')]]</span>
</label>
</div>
@ -113,7 +113,7 @@
</div>
<div class="collapse" v-bind:class="{in:account=='password'}">
<h4>Change Password</h4>
<h4 class="modal-title">Change Password</h4>
<div class="modal-section labels-inline">
<div class="form-group">
<label class="label">[[__("current_password")]]</label>
@ -132,17 +132,23 @@
</div>
<div class="modal-footer">
<div class="btn-cluster">
<button
class="btn btn-dark btn-md"
class="btn btn-transparent btn-block"
v-on:click="save_user_password(password_change_current, password_change_new, password_change_new_confirmation);" >
[[__("change_password")]]
</button>
</div>
</div>
</div>
<div class="collapse" v-bind:class="{in:account=='terminate'}">
<h4>Terminate Account</h4>
<div class="modal-section labels-inline">
<div class="">
<p>[[__("terminate_warning")]]</p>
<p>[[__("terminate_warning2")]]</p>
</div>
<div class="labels-inline" v-if="user.account_type == 'email'">
<div class="form-group">
<label class="label">[[__("current_password")]]</label>
<input v-model="account_remove_password" class="input input-white no-b" type="password">
@ -153,15 +159,11 @@
<textarea class="input input-white no-b" v-model="account_remove_feedback"></textarea>
<p class="message">[[__("terminate_reason_caption")]]</p>
</div>
</div>
<div class="modal-section labels-inline">
<div class="center alert alert-danger" v-if="account_remove_error">{{account_remove_error}}</div>
</div>
<div class="modal-footer">
<button class="btn btn-stroke-darken btn-md" v-on:click="remove_account(account_remove_password, account_remove_feedback)">Terminate Account</button>
</div>
<button class="btn btn-transparent btn-block" v-on:click="remove_account(account_remove_password, account_remove_feedback)">[[__("terminate_terminate")]]</button>
</div>
</div>
</div>

View File

@ -1,23 +1,15 @@
<header id="folder-header" class="header" v-if="(active_view == 'folders' && active_folder)" v-cloak>
<div v-cloak class="header-left pull-left">
<a class="btn btn-dark btn-md btn-round btn-icon" href="/spaces">
<span class="icon icon-svg icon-sd6"></span>
<a class="btn btn-stroke-darken btn-md btn-round btn-icon" href="/spaces">
<span class="icon icon-home"></span>
</a>
<button v-if="logged_in && (active_space_role == 'editor' || active_space_role == 'admin')" class="btn btn-dark btn-md btn-round" v-on:click="create_space('space')">[[ __('create_space') ]]</button>
<button v-if="logged_in && (active_space_role == 'editor' || active_space_role == 'admin')" class="btn btn-primary btn-md btn-round" v-on:click="create_space('space')">[[ __('create_space') ]]</button>
<button v-if="logged_in && (active_space_role == 'editor' || active_space_role == 'admin')" class="btn btn-stroke-darken btn-md btn-round" v-on:click="create_space('folder')">
<span>[[ __('create_folder') ]]</span>
<span v-bind:class="{'disabled-pro':!is_pro(user)}">[[ __('create_folder') ]]</span>
</button>
</div>
<label class="relative compact-hidden">
<span class="icon icon-sm icon-zoom no-events absolute-top-left" style="margin: 5px;"></span>
<input id="folder-search"
type="search" name="search"
style="padding-left: 40px !important; margin-right: 10px;"
placeholder="[[ __('search') ]]"
class="input input-md input-white input-round no-b w-2"
v-model="folder_spaces_search" v-on:change="search_spaces">
</label>
<div class="header-right pull-right">
<div class="dropdown top light m-r-20 compact-hidden" v-bind:class="{open : active_dropdown=='folder_sorting'}">
<button class="btn btn-sm btn-nude" v-on:click="activate_dropdown('folder_sorting')">
<span>[[ __('sort_by') ]]</span>:
@ -41,15 +33,29 @@
v-on:click="set_folder_sorting('space_type',false)">
<span>[[ __('type') ]]</span>
</li>
</ul>
</div>
</div>
</div>
<div class="header-right pull-right">
<label class="relative compact-hidden">
<span class="icon icon-sm icon-zoom no-events absolute-top-left" style="margin: 5px;"></span>
<input id="folder-search"
type="search" name="search"
style="padding-left: 40px !important; margin-right: 10px;"
placeholder="[[ __('search') ]]"
class="input input-md input-white input-round no-b w-2"
v-model="folder_spaces_search" v-on:change="search_spaces">
</label>
<button class="btn btn-stroke-darken btn-md btn-round" v-if="!user.confirmed_at" v-on:click="confirm_again()">
<span v-if="!account_confirmed_sent">[[ __('email_unconfirmed') ]]</span>
<span v-if="account_confirmed_sent">[[ __('confirmation_sent') ]]</span>
</button>
<div class="dropdown top right light" v-bind:class="{open: active_dropdown=='account'}">
<button
class="profile-avatar btn btn-md btn-icon btn-dark btn-round"
class="profile-avatar btn btn-md btn-icon btn-darken btn-round"
v-bind:style="background_image_style([user.avatar_thumb_uri])"
v-bind:class="{'has-avatar-image':!!user.avatar_thumb_uri}" v-on:click="show_account();">
<span class="icon icon-user" v-if="logged_in && !user.avatar_thumb_uri"></span></button>
@ -70,6 +76,13 @@
</a>
</li>
<li v-on:click="activate_modal('support')">
<span>
<span class="icon icon-sm icon-info"></span>
<span>[[ __('support') ]]</span>
</span>
</li>
<li v-on:click="logout()">
<span>
<span class="icon icon-sm icon-logout"></span>
@ -81,12 +94,12 @@
</div>
<!--div class="btn-group dark round" id="meta-toggle" style="margin-right:10px">
<div class="btn-group dark round" id="meta-toggle" style="margin-right:10px">
<button class="btn btn-md btn-transparent btn-icon btn-icon" v-on:click="toggle_meta()">
<span class="jewel" style="color: white; background-color: red" v-if="meta_unseen>0">{{meta_unseen}}</span>
<span class="icon icon-menu"></span>
</button>
</div-->
</div>
</div>
</header>
@ -98,8 +111,10 @@
<div id="folder-breadcrumb">
<span v-for="item in active_space_path" class="btn btn-sm btn-transparent" v-sd-droppable="handle_folder_drop;item">
<a href="/{{item.space_type}}s/{{item._id}}">{{item.name}}</a>&nbsp; â–¶</span>
<a v-for="item in active_space_path" type="button" class="btn btn-sm btn-transparent" href="/{{item.space_type}}s/{{item._id}}" v-sd-droppable="handle_folder_drop;item">
<span>{{item.name}}</span>
<span class="seperator">/</span>
</a>
<a v-if="(active_space_role != 'admin')" type="button" class="btn btn-sm btn-transparent">
<span>{{active_folder.name}}</span>
@ -113,10 +128,12 @@
<ul class="select-list">
<li><span class="tile-rename" v-on:click="rename_folder(active_folder)">[[__("rename")]]</span></li>
<li v-if="active_space_role == 'admin'"><span class="tile-share" v-on:click="activate_access()">[[__("share")]]</span></li>
<li><a v-on:click=" open_url('[[config.endpoint]]/api/spaces/'+active_folder._id+'/list')" target="_blank">[[__("list")]]</a></li>
</ul>
</div>
</div>
<div v-if="active_folder._id == user.home_folder_id">
<span>[[ __('home') ]]</span>
</div>
@ -129,6 +146,7 @@
</div>
</div>
<div id="folder-empty" v-if="folder_spaces_filter">
<div v-if="active_profile_spaces | empty?">
<p><b>"{{folder_spaces_filter}}"</b> <br/>[[ __('search_no_results') ]]</p>
@ -156,6 +174,7 @@
<div class="dropdown-menu" role="menu">
<ul class="select-list">
<li v-on:click="duplicate_space(item)"><span><span class="icon icon-sm icon-duplicate"></span>[[ __('duplicate') ]]</span></li>
<li v-on:click="rename_space(item)"><span><span class="icon icon-sm icon-tag"></span>[[ __('rename') ]]</span></li>
<li v-on:click="delete_space(item)"><span><span class="icon icon-sm icon-trash"></span>[[ __('delete') ]]</span></li>
</ul>

View File

@ -1,11 +1,17 @@
<header id="landing-header" class="header" v-cloak v-if="(active_view == 'login' || active_view == 'signup' || active_view == 'password-reset' || active_view == 'password-confirm')">
<div class="header-left">
<a class="btn btn-transparent btn-nude" href="/"><img src="/images/sd6-logo-black.svg" width="190"></a>
<a class="btn btn-transparent btn-nude" href="/"><img src="/images/sd5-logo.svg" width="190"></a>
</div>
<div class="header-right pull-right">
<a v-if="active_view != 'login'" class="btn btn-md btn-dark btn-round" href="/login">[[__("login")]]</a>
<a v-if="active_view != 'signup'" class="btn btn-md btn-dark btn-round" href="/signup">[[__("signup")]]</a>
<span class="btn-group dark round">
{% if (locale != "de") %}<a href="/t/de?r={{active_view}}" class="btn btn-transparent btn-md">Deutsch</a>{% endif %}
{% if (locale != "en") %}<a href="/t/en?r={{active_view}}" class="btn btn-transparent btn-md">English</a>{% endif %}
{% if (locale != "fr") %}<a href="/t/fr?r={{active_view}}" class="btn btn-transparent btn-md">Français</a>{% endif %}
</span>
<a class="btn btn-md btn-dark btn-round" href="/login">[[__("login")]]</a>
<a class="btn btn-md btn-blue btn-round" href="/signup">[[__("signup")]]</a>
</div>
</header>
@ -17,7 +23,10 @@
<div id="login" v-bind:class="{active : active_view == 'login'}">
<div class="content">
<form v-on:submit="login_submit(user_forms_email, login_password, $event)">
<h3>Login</h3>
<span class="btn btn-xs btn-darken pull-right" v-on:click="login_google();">[[__("login_google")]]</span>
<h4>[[__("login")]]</h4>
<div class="tight">
<div class="form-group">
@ -28,15 +37,16 @@
</div>
</div>
<button type="submit" class="btn btn-dark btn-block">
<span v-show="!loading_user">Login</span>
<span v-show="loading_user">Logging in…</span>
<button type="submit" class="btn btn-primary btn-block">
<span v-show="!loading_user">[[__("login")]]</span>
<span v-show="loading_user">[[__("logging_in")]]</span>
</button>
<div class="center alert alert-danger" v-if="login_error">{{login_error}}</div>
<div style="margin-top:2em">
<a href="/password-reset">Forgot Password</a>
<div class="pull-right">
<a class="btn btn-xs btn-darken" href="/signup">[[__("signup")]]</a>&nbsp;
<a class="btn btn-xs btn-darken" href="/password-reset">[[__("reset_password")]]</a>
</div>
</form>
</div>
@ -44,12 +54,23 @@
<div id="signup" v-bind:class="{active : active_view == 'signup'}">
<div class="content">
<form v-on:submit="signup_submit($event, user_forms_name, user_forms_email, signup_password, signup_password_confirmation, signup_invite_code)">
<form v-on:submit="signup_submit($event, user_forms_name, user_forms_email, signup_password, signup_password_confirmation)">
<span class="btn btn-xs btn-darken pull-right" v-on:click="login_google();">[[__("login_google")]]</span>
<h4>[[__("signup")]]</h4>
<div class="tight">
<div class="form-group">
<input class="input" type="email" required id="user-email" v-model="user_forms_email" placeholder="[[__("email")]]" autofocus v-focus>
<input class="input" type="text" id="user-name" v-model="user_forms_name" placeholder="[[__("name")]]" v-focus autofocus>
</div>
</div>
<div class="tight">
<div class="form-group">
<input class="input" type="email" required id="user-email" v-model="user_forms_email" placeholder="[[__("email")]]">
</div>
<div class="form-group">
@ -57,33 +78,21 @@
</div>
<div class="form-group">
<input class="input" id="user-password-confirmation" required type="password" v-model="signup_password_confirmation" placeholder="Repeat Password">
<input class="input" id="user-password-confirmation" required type="password" v-model="signup_password_confirmation" placeholder="[[__("password_confirmation")]]">
</div>
</div>
<div class="tight">
<div class="form-group">
<input class="input" type="text" id="user-name" v-model="user_forms_name" placeholder="Pick a username">
</div>
<!--
Disabled textbox so people don't have to input beta code when registering.
Remember to also set invite_code to an empty string ("") in /config/default.conf!
<div class="form-group">
<input class="input" id="invite-code" required type="text" v-model="signup_invite_code" placeholder="Beta Invite Code">
</div>-->
<div style="margin-top: -7px; margin-bottom: 7px;"><small>By signing up you agree to our <a href="/terms" target="_blank">TOS</a> and <a href="/privacy" target="_blank">Privacy Policy.</a></small><br/>
</div>
<!--div style="margin-top: -7px; margin-bottom: 7px;"><small>By signing up you agree to our <a href="/terms" target="_blank">TOS</a> and <a href="/privacy" target="_blank">Privacy Policy.</a></small><br/>
</div-->
<button class="btn btn-dark btn-block">
<button class="btn btn-primary btn-block">
<span v-if="!creating_user">[[__("signup")]]</span>
<span v-if="creating_user">[[__("signing_up")]]</span>
</button>
<div class="center alert alert-danger" style="width:100%;" v-if="signup_error">{{signup_error}}</div>
<a class="btn btn-link btn-block" href="/login" style="margin-top: 20px">[[__("login")]]</a>
</form>
</div>
</div>
@ -98,12 +107,12 @@
</div>
</div>
<div class="text-center alert alert-danger" v-if="password_reset_error">{{password_reset_error}}</div>
<button class="btn btn-dark btn-block" v-on:click="password_reset_submit($event, reset_email)">[[__("reset_password")]]</button>
<button class="btn btn-primary btn-block" v-on:click="password_reset_submit($event, reset_email)">[[__("reset_password")]]</button>
</form>
</div>
<div class="content" v-if="password_reset_send==true">
<h4>Reset Password</h4>
Please check your email inbox.
<h4>[[__("password_confirmation")]]</h4>
[[__("password_check_inbox")]]
</div>
</div>
@ -114,16 +123,16 @@
<div class="tight">
<div class="form-group">
<input class="input" id="user-password" type="password" v-model="signup_password" placeholder="New Password">
<input class="input" id="user-password" type="password" v-model="signup_password" placeholder="[[__("password")]]">
</div>
<div class="form-group">
<input class="input" id="user-password" type="password" v-model="signup_password_confirmation" placeholder="Repeat Password">
<input class="input" id="user-password" type="password" v-model="signup_password_confirmation" placeholder="[[__("password_confirmation")]]">
</div>
</div>
<div class="text-center alert alert-danger" v-if="password_reset_confirm_error">{{password_reset_confirm_error}}</div>
<button class="btn btn-dark btn-block" v-on:click="password_reset_confirm($event, signup_password, signup_password_confirmation)">[[__("save")]]</button>
<button class="btn btn-primary btn-block" v-on:click="password_reset_confirm($event, signup_password, signup_password_confirmation)">[[__("save")]]</button>
</form>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More