diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..979bf6b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.DS_Store +.git +logs +*.log +scripts +pids +*.pid +*.seed +lib-cov +coverage +.grunt +.lock-wscript +build/Release +node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a614e97 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM spacedeck/docker-baseimage:latest +ENV NODE_ENV production + +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY package.json /usr/src/app/ +RUN npm install +RUN npm install gulp-rev-replace gulp-clean gulp-fingerprint gulp-rev gulp-rev-all gulp-rev-replace +RUN npm install -g --save-dev gulp + +COPY app.js Dockerfile Gulpfile.js LICENSE /usr/src/app/ +COPY config /usr/src/app/config +COPY helpers /usr/src/app/helpers +COPY locales /usr/src/app/locales +COPY middlewares /usr/src/app/middlewares +COPY models /usr/src/app/models +COPY public /usr/src/app/public +COPY routes /usr/src/app/routes +COPY styles /usr/src/app/styles +COPY views /usr/src/app/views + +RUN gulp all +RUN npm cache clean + +CMD [ "node", "app.js" ] + +EXPOSE 9666 + diff --git a/Gulpfile.js b/Gulpfile.js index af47a5d..e4aa47e 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -12,10 +12,9 @@ var uglify = require('gulp-uglify'); var fingerprint = require('gulp-fingerprint'); var rev = require('gulp-rev'); -var RevAll = require('gulp-rev-all'); +var revAll = require('gulp-rev-all'); gulp.task('rev', () => { - var revAll = new RevAll(); return gulp.src(['public/**']) .pipe(gulp.dest('build/assets')) .pipe(revAll.revision()) diff --git a/README.md b/README.md index 723f75f..60ad14a 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ We appreciate filed issues, pull requests and general discussion. Spacedeck uses the following major building blocks: +- Vue.js (Frontend) - Node.js 7.x (Backend / API) - MongoDB 3.x (Datastore) - Redis 3.x (Datastore for realtime channels) -- Vue.js (Frontend) It also has some binary dependencies for media conversion and PDF export: @@ -50,8 +50,14 @@ see: config/config.json export NODE_ENV=development npm start + open http://localhost:9666 - open http://localhost:9000 +# 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 diff --git a/app.js b/app.js index a6e67e1..004fb5a 100644 --- a/app.js +++ b/app.js @@ -50,7 +50,7 @@ swig.setFilter('cdn', function(input, idx) { app.engine('html', swig.renderFile); app.set('view engine', 'html'); -if (app.get('env') != 'development') { +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'))); @@ -84,7 +84,6 @@ app.use(helmet.noSniff()) app.use(require("./middlewares/templates")); app.use(require("./middlewares/error_helpers")); app.use(require("./middlewares/setuser")); -app.use(require("./middlewares/subdomain")); app.use(require("./middlewares/cors")); app.use(require("./middlewares/i18n")); app.use("/api", require("./middlewares/api_helpers")); @@ -129,11 +128,11 @@ if (app.get('env') == 'development') { module.exports = app; // CONNECT TO DATABASE -const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || 'localhost'; +const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || config.get('mongodb_host'); mongoose.connect('mongodb://' + mongoHost + '/spacedeck'); // START WEBSERVER -const port = 9000; +const port = 9666; const server = http.Server(app).listen(port, () => { diff --git a/config/default.json b/config/default.json index 6fd9f66..37a00aa 100644 --- a/config/default.json +++ b/config/default.json @@ -1,9 +1,20 @@ { - "endpoint": "http://localhost:9000", + //"endpoint": "http://localhost:9000", + "endpoint": "http://localhost:9666", "storage_region": "eu-central-1", + + //"storage_bucket": "sdeck-development", + //"storage_cdn": "http://localhost:9123/sdeck-development", + //"storage_endpoint": "http://storage:9000", + "storage_bucket": "my_spacedeck_bucket", "storage_cdn": "/storage", "storage_local_path": "./storage", + + "redis_mock": true, + "mongodb_host": "localhost", + "redis_host": "localhost", + "google_access" : "", "google_secret" : "", "admin_pass": "very_secret_admin_password", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..688f381 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/helpers/mailer.js b/helpers/mailer.js index 8da189d..15545d0 100644 --- a/helpers/mailer.js +++ b/helpers/mailer.js @@ -53,7 +53,7 @@ module.exports = { } } }, function(err, data) { - if(err) console.log('Email not sent:', err); + if (err) console.error("Error sending email:", err); else console.log("Email sent."); }); } diff --git a/helpers/phantom.js b/helpers/phantom.js index c195a52..ed897f5 100644 --- a/helpers/phantom.js +++ b/helpers/phantom.js @@ -32,31 +32,36 @@ module.exports = { }; phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) { - return browser.createPage(function (err, page) { - console.log("page created, opening ",space_url); + 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.advanced.width+"px", - height: space.advanced.height+"px" - }; - page.set('paperSize', psz); - } + if (type=="pdf") { + var psz = { + width: space.advanced.width+"px", + height: space.advanced.height+"px" + }; + page.set('paperSize', psz); + } - page.set('settings.resourceTimeout',timeout); - page.set('settings.javascriptEnabled',false); + page.set('settings.resourceTimeout',timeout); + page.set('settings.javascriptEnabled',false); - return page.open(space_url, function (err,status) { - page.render(export_path, function() { - on_success_called = true; - if (on_success) { - on_success(export_path); - } - page.close(); - browser.exit(); + return page.open(space_url, function (err,status) { + page.render(export_path, function() { + on_success_called = true; + if (on_success) { + on_success(export_path); + } + page.close(); + browser.exit(); + }); }); - }); - }); + }); + } + }, { onExit: on_exit }); diff --git a/helpers/redis.js b/helpers/redis.js index 9866ba4..69b9411 100644 --- a/helpers/redis.js +++ b/helpers/redis.js @@ -1,5 +1,7 @@ 'use strict'; +const config = require('config'); + // this is a mock version of the Redis API, // emulating Redis if it is not available locally var notRedis = { @@ -92,7 +94,12 @@ var notRedis = { module.exports = { connectRedis: function() { - this.connection = notRedis; + if (config.get("redis_mock")) { + this.connection = notRedis; + } else { + const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'sync'; + this.connection = new RedisConnection(6379, redisHost); + } }, getConnection: function() { this.connectRedis(); diff --git a/helpers/uploader.js b/helpers/uploader.js index 7fe1df1..958d12c 100644 --- a/helpers/uploader.js +++ b/helpers/uploader.js @@ -2,21 +2,40 @@ var fs = require('fs'); var config = require('config'); +var s3 = null; // use AWS S3 or local folder depending on config if (config.get("storage_local_path")) { var AWS = require('mock-aws-s3'); AWS.config.basePath = config.get("storage_local_path"); + s3 = new AWS.S3(); } else { var AWS = require('aws-sdk'); - AWS.config.region = config.get("storage_region"); + var storage_endpoint = config.get("storage_endpoint"); + const ep = new AWS.Endpoint(storage_endpoint); + + AWS.config.update(new AWS.Config({ + accessKeyId: process.env.MINIO_ACCESS_KEY, + secretAccessKey: process.env.MINIO_SECRET_KEY, + region: config.get("storage_region"), + s3ForcePathStyle: true, + signatureVersion: 'v4' + })); + s3 = new AWS.S3({ + endpoint: ep + }); } +s3.createBucket({ + Bucket: config.get("storage_bucket"), + ACL: "public-read", + GrantRead: "*" +}, (err,res) => { + console.log("createBucket",err,res); +}); + module.exports = { removeFile: (path, callback) => { - const s3 = new AWS.S3({ - region: config.get("storage_region") - }); const bucket = config.get("storage_bucket"); s3.deleteObject({ Bucket: bucket, Key: path @@ -34,7 +53,7 @@ module.exports = { callback({error:"missing path"}, null); return; } - console.log("[s3] uploading", localFilePath, " to ", fileName); + console.log("[storage] uploading", localFilePath, " to ", fileName); const bucket = config.get("storage_bucket"); const fileStream = fs.createReadStream(localFilePath); @@ -45,10 +64,6 @@ module.exports = { } }); fileStream.on('open', function () { - var s3 = new AWS.S3({ - region: config.get("storage_region") - }); - s3.putObject({ Bucket: bucket, Key: fileName, @@ -58,7 +73,7 @@ module.exports = { if (err){ console.error(err); callback(err); - }else { + } else { const url = config.get("storage_cdn") + "/" + fileName; console.log("[s3]" + localFilePath + " to " + url); callback(null, url); diff --git a/helpers/websockets.js b/helpers/websockets.js index 9394913..d541ede 100644 --- a/helpers/websockets.js +++ b/helpers/websockets.js @@ -1,21 +1,28 @@ 'use strict'; require('../models/schema'); +const config = require('config'); + const WebSocketServer = require('ws').Server; +const RedisConnection = require('ioredis'); const async = require('async'); const _ = require("underscore"); const mongoose = require("mongoose"); const crypto = require('crypto'); -var redis = require("./redis.js"); +const redisMock = require("./redis.js"); module.exports = { startWebsockets: function(server) { this.setupSubscription(); - this.state = redis.getConnection(); - if(!this.current_websockets) { + if (!this.current_websockets) { + if (config.get("redis_mock")) { + this.state = redisMock.getConnection(); + } else { + this.state = new RedisConnection(6379, process.env.REDIS_PORT_6379_TCP_ADDR || config.get("redis_host")); + } this.current_websockets = []; } @@ -118,9 +125,17 @@ module.exports = { }, setupSubscription: function() { - this.cursorSubscriber = redis.getConnection().subscribe(['cursors', 'users', 'updates'], function (err, count) { - console.log("[redis] websockets to " + count + " topics." ); - }); + if (config.get("redis_mock")) { + this.cursorSubscriber = redisMock.getConnection().subscribe(['cursors', 'users', 'updates'], function (err, count) { + console.log("[redis-mock] websockets subscribed to " + count + " topics." ); + }); + } else { + this.cursorSubscriber = new RedisConnection(6379, process.env.REDIS_PORT_6379_TCP_ADDR || config.get("redis_host")); + this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) { + console.log("[redis] websockets subscribed to " + count + " topics." ); + }); + } + this.cursorSubscriber.on('message', function (channel, rawMessage) { const msg = JSON.parse(rawMessage); const spaceId = msg.space_id; diff --git a/middlewares/setuser.js b/middlewares/setuser.js index 349aebf..56bcd78 100644 --- a/middlewares/setuser.js +++ b/middlewares/setuser.js @@ -5,27 +5,24 @@ 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) { - // FIXME - var domain = "localhost"; - res.clearCookie('sdsession', { - domain: domain - }); + res.clearCookie('sdsession'); if (req.accepts("text/html")) { - res.redirect("/"); + 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.redirect("/"); + res.send("Please clear your cookies and try again."); } - } else { req["token"] = token; req["user"] = user; diff --git a/models/team.js b/models/team.js index a1f3cc7..b35942c 100644 --- a/models/team.js +++ b/models/team.js @@ -45,7 +45,7 @@ module.exports.teamSchema.index({ module.exports.teamSchema.statics.getTeamForHost = (host, cb) => { - if (host != "127.0.0.1:9000") { //phantomjs check + if (host != "127.0.0.1:9666") { //phantomjs check let subDomainParts = host.split('.'); if (subDomainParts.length > 2) { diff --git a/routes/api/sessions.js b/routes/api/sessions.js index 3b63042..054e49c 100644 --- a/routes/api/sessions.js +++ b/routes/api/sessions.js @@ -5,6 +5,7 @@ require('../../models/schema'); var bcrypt = require('bcryptjs'); var crypo = require('crypto'); +var URL = require('url').URL; var express = require('express'); var router = express.Router(); @@ -40,11 +41,11 @@ router.post('/', function(req, res) { user.sessions.push(session); user.save(function(err, result) { - // FIXME - var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging"; - var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost"; + 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, secure: secure}); + res.cookie('sdsession', token, { domain: domain, httpOnly: true }); res.status(201).json(session); }); }); @@ -69,8 +70,7 @@ router.delete('/current', function(req, res, next) { }); user.sessions = newSessions; user.save(function(err, result) { - // FIXME - var domain = (process.env.NODE_ENV == "production") ? ".example.org" : "localhost"; + var domain = new URL(config.get('endpoint')).hostname; res.clearCookie('sdsession', { domain: domain }); res.sendStatus(204); }); diff --git a/routes/api/users.js b/routes/api/users.js index d8e995c..7e1a1aa 100644 --- a/routes/api/users.js +++ b/routes/api/users.js @@ -16,6 +16,7 @@ var fs = require('fs'); var request = require('request'); var gm = require('gm'); var validator = require('validator'); +var URL = require('url').URL; var express = require('express'); var router = express.Router(); @@ -182,8 +183,7 @@ router.get('/loginorsignupviagoogle', function(req, res) { var apiUrl = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=" + tokens.access_token; var finalizeLogin = function(session){ - var secure = process.env.NODE_ENV == "production" || process.env.NODE_ENV == "staging"; - res.cookie('sdsession', session.token, { httpOnly: true, secure: secure}); + res.cookie('sdsession', session.token, { httpOnly: true }); res.status(201).json(session); }; diff --git a/views/layouts/outer.html b/views/layouts/outer.html index 8753582..603d96a 100644 --- a/views/layouts/outer.html +++ b/views/layouts/outer.html @@ -53,7 +53,7 @@