Commit 7ff29265 authored by mntmn's avatar mntmn
Browse files

initial commit.

parents
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
var artifactId = req.params.artifact_id;
Artifact.findOne({
"_id": artifactId
}, (err, artifact) => {
if (err) {
res.status(400).json(err);
} else {
if (artifact) {
req['artifact'] = artifact;
next();
} else {
res.sendStatus(404);
}
}
});
};
\ No newline at end of file
'use strict';
require('../models/schema');
const config = require('config');
const url = require('url');
function respond(origin, req, res, next) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Max-Age', 60 * 60 * 24);
res.header('Access-Control-Expose-Headers', 'Accepts, Content-Type, X-Spacedeck-Space-Role, X-Spacedeck-Channel, X-Spacedeck-Spacepassword, X-Spacedeck-Auth, X-Spacedeck-Space-Auth');
res.header('Access-Control-Allow-Headers', 'Accepts, Accept-Language, Accept-Encoding, Accept-Language, Content-Type, X-Spacedeck-Space-Auth, X-Spacedeck-Space-Role, X-Spacedeck-Channel, X-Spacedeck-Spacepassword, X-Spacedeck-Auth');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
if (req.method == 'OPTIONS') {
res.sendStatus(204);
} else {
next();
}
}
module.exports = (req, res, next) => {
const origin = req.headers.origin;
if (origin) {
const parsedUrl = url.parse(origin, true, true);
// FIXME
if (parsedUrl.hostname == "cdn.spacedeck.com") {
res.header('Cache-Control', "max-age");
res.header('Expires', "30d");
res.removeHeader("Pragma");
respond(origin, req, res, next);
} else {
Team.getTeamForHost(parsedUrl.hostname, (err, team, subdomain) => {
if (team) {
respond(origin, req, res, next);
} else {
next();
}
});
}
} else {
next();
}
}
'use strict';
module.exports = (req, res, next) => {
res.bad_request = (msg) => {
if (req.accepts('text/html')) {
res.status(400).render('error', {
message: msg
});
} else {
res.status(400).json({
"error": msg
});
}
}
next();
}
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
req.i18n.setLocale(req.i18n.prefLocale);
if (req.cookies.spacedeck_locale) {
req.i18n.setLocaleFromCookie();
}
if (req.user && req.user.preferences.language) {
req.i18n.setLocale(req.user.preferences.language);
}
next();
}
\ No newline at end of file
'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 (!user) {
// FIXME
var domain = "localhost";
res.clearCookie('sdsession', {
domain: domain
});
if (req.accepts("text/html")) {
res.redirect("/");
} else if (req.accepts('application/json')) {
res.status(403).json({
"error": "token_not_found"
});
} else {
res.redirect("/");
}
} else {
req["token"] = token;
req["user"] = user;
next();
}
});
} else {
next();
}
}
'use strict';
require('../models/schema');
var config = require('config');
module.exports = (req, res, next) => {
let spaceId = req.params.id;
let finalizeReq = (space, role) => {
if (role === "none") {
res.status(403).json({
"error": "access denied"
});
} else {
req['space'] = space;
req['spaceRole'] = role;
res.header("x-spacedeck-space-role", req['spaceRole']);
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";
if (spaceAuth && (spaceAuth === space.edit_hash)) {
role = "editor";
} else {
if (space.access_mode == "public") {
role = "viewer";
} else {
role = "none";
}
}
if (req.user) {
rolePerUser(space, req.user, function(newRole) {
if (newRole == "admin" && (role == "editor" || role == "viewer")) {
finalizeReq(space, newRole);
} else if (newRole == "editor" && (role == "viewer")) {
finalizeReq(space, newRole);
} else {
finalizeReq(space, role);
}
});
} else {
finalizeReq(space, role);
}
};
var userMapping = {
'_id': 1,
'nickname': 1,
'email': 1
};
Space.findOne({
"_id": spaceId
}).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) {
finalizeAnonymousLogin(space, req["spaceAuth"]);
} else {
res.status(403).json({
"error": "password_wrong"
});
}
} else {
res.status(401).json({
"error": "password_required"
});
}
} else {
finalizeAnonymousLogin(space, req["spaceAuth"]);
}
} else {
// 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");
return;
}
if (req.user) {
rolePerUser(space, req.user, function(role) {
if (role == "none") {
finalizeAnonymousLogin(space, req["spaceAuth"]);
} else {
finalizeReq(space, role);
}
});
} else {
if (req.spaceAuth && space.edit_hash) {
finalizeAnonymousLogin(space, req["spaceAuth"]);
} else {
res.status(403).json({
"error": "auth_required"
});
}
}
}
} else {
res.status(404).json({
"error": "space_not_found"
});
}
}
});
}
'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();
}
});
}
'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"
});
}
}
\ No newline at end of file
'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();
}
\ No newline at end of file
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.actionSchema = mongoose.Schema({
space: {
type: Schema.Types.ObjectId,
ref: 'Space'
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
editor_name: String,
action: String,
object: Schema.Types.Mixed,
created_at: {
type: Date,
default: Date.now
},
updated_at: {
type: Date,
default: Date.now
}
});
module.exports.actionSchema.index({
space: 1,
created_at: 1
});
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
module.exports.artifactSchema = mongoose.model('Artifact', {
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}
});
'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
});
'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
});
'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
});
'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;
\ No newline at end of file
//'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);
'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;
'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:9000") { //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);
}
}
'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);
};
{
"name": "spacedeck-open",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "nodemon -e .js,.html bin/www",
"test": "mocha"
},
"dependencies": {
"archiver": "0.14.4",
"async": "1.3.0",
"aws-sdk": "2.1.39",
"basic-auth": "1.0.3",
"bcrypt": "*",
"body-parser": "~1.13.1",
"cheerio": "0.19.0",
"config": "1.14.0",
"cookie-parser": "~1.3.5",
"csurf": "1.8.3",
"debug": "~2.2.0",
"execSync": "latest",
"express": "~4.13.0",
"glob": "5.0.14",
"gm": "1.18.1",
"googleapis": "2.1.3",
"gulp": "^3.9.0",
"gulp-concat": "2.6.0",
"gulp-eslint": "*",
"gulp-express": "0.3.0",
"gulp-nodemon": "*",
"gulp-sass": "^2.0.3",
"gulp-uglify": "^1.5.1",
"gulp-util": "^3.0.6",
"helmet": "^1.1.0",
"i18n-2": "0.4.6",
"ioredis": "1.6.1",
"lodash": "^4.3.0",
"log-timestamp": "latest",
"md5": "2.0.0",
"moment": "2.10.6",
"mongoose": "4.4.3",
"morgan": "1.6.1",
"node-sass-middleware": "0.8.0",
"pdfkit": "0.7.1",
"validator": "5.2.0",
"node-phantom-simple": "2.2.4",
"phantomjs-prebuilt": "2.1.7",
"pm2": "latest",
"qr-image": "3.1.0",
"raven": "0.8.1",
"request": "2.60.0",
"sanitize-html": "^1.11.1",
"serve-favicon": "~2.3.0",
"swig": "1.4.2",
"slug": "0.9.1",
"underscore": "1.8.3",
"weak": "1.0.0",
"ws": "0.7.2"
},
"devDependencies": {
"express": "^4.13.3",
"gulp": "^3.9.0",
"gulp-clean": "^0.3.2",
"gulp-concat": "^2.6.0",
"gulp-eslint": "^1.1.0",
"gulp-express": "^0.3.0",
"gulp-fingerprint": "^0.3.2",
"gulp-nodemon": "^2.0.4",
"gulp-rev": "^6.0.1",
"gulp-rev-all": "^0.8.22",
"gulp-rev-replace": "^0.4.3",
"gulp-sass": "^2.1.0",
"gulp-uglify": "^1.4.2",
"mocha": "*",
"nodemon": "*",
"should": "^7.1.0",
"supertest": "^1.1.0",
"winston": "^1.0.1"
},
"description": "",
"main": "Gulpfile.js",
"directories": {
},
"repository": {
"type": "git",
"url": "https://github.com/spacedeck/spacedeck-open.git"
},
"keywords": [],
"author": "Lukas F. Hartmann, Martin Güther",
"license": "AGPLv3"
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment