diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..2e479a1
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,8 @@
+{
+ "directory": "public/lib",
+ "storage": {
+ "packages": ".bower-cache",
+ "registry": ".bower-registry"
+ },
+ "tmp": ".bower-tmp"
+}
diff --git a/.gitignore b/.gitignore
index 9d77abf..40d04da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,8 @@
workspaces/*
node_modules
+config.js
+.bower-*/
+public/lib
+.c9revisions
+.settings
+public/css/*
diff --git a/README.md b/README.md
index b687da9..d3d8da8 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,69 @@
-#Cloud9Hub
+# Cloud9Hub
-##What's this?
+## What's this?
It's a simple interface for the Cloud 9 open source edition to easily create, use and manage multiple workspaces.
[The Cloud9 service](https://c9.io) has a shiny and awesome dashboard interface where you can manage multiple workspaces,
however the [open source edition](https://github.com/ajaxorg/cloud9) is a single workspace instance of Cloud9.
As I like the possibility to easily start working on different workspaces, create or delete them, I created Cloud9Hub to do so.
-##What's Cloud9?
+## What's Cloud9?
A full-blown IDE in your browser. It has a full terminal integration, can run and deploy code of different languages (e.g. Ruby, node.js, PHP)
-and [lots more](https://c9.io/site/features/).
+and [lots more](http://en.wikipedia.org/wiki/Cloud9_IDE#Features).
-##Status Quo of Cloud9Hub
+## Status Quo of Cloud9Hub
Right now it can
* Create new workspaces
* Launch multiple workspace instances
* Kill them automatically after 15 minutes
* List available workspaces
* Delete workspaces
-
-It **can't**
* Manage multiple users
* Do authentication/sessions
-* Sense, that you're active and **not** kill your workspace after 15 minutes.
+* Sense, that you're active and will kill your workspace after 15-20 minutes of inactivity.
right now. These are the next steps for me to build (or you make a Pull Request with the features you want).
-##Installation
+## Installation
First you will need [node.js](http://nodejs.org/), at least v0.8.
-**Note, as of June, 12th 2013:** Cloud9 right now breaks when you try installing it with node > 0.8.x :(
+Then you can try the quick install or the manual way:
+
+### Quick install
+
+```shell
+curl https://raw.githubusercontent.com/AVGP/cloud9hub/master/install.sh | sh
+```
+
+This should install Cloud9 and Cloud9Hub into the current folder. If this succeeded, you can now go to the configuration section.
+
+### Manual installation
+1. Install [Cloud9](https://github.com/ajaxorg/cloud9) into some folder, say ``/var/awesomeness/cloud9``.
+**Note, the cloud9 is currently hardcoded to c9. when cloning cloud9, clone to c9 dir. If this isn't done, hub will crash.
+2. Then install Cloud9Hub into the parent folder above your cloud9 installation, so in my example``/var/awesomeness/cloud9hub` and run ``npm install``.
+
+# Configuration
+
+First things first: You need a Github application to provide the "Login with Github" feature, which is currently the only login mechanism.
+
+Go to [https://github.com/settings/applications/new](https://github.com/settings/applications/new) and create a new application. Note down the client ID and secret, you'll need them later.
+
+Now copy the ``config.js.example`` to ``config.js`` and edit the contents:
+
+- Add your Github client ID and secret
+- Change your BASE_URL to your server's address (do not include the port!)
-Install [Cloud9](https://github.com/ajaxorg/cloud9) into some folder, say ``/var/awesomeness/cloud9``.
-Then install Cloud9hub into the parent folder above your cloud9 installation, so in my example``/var/awesomeness/cloud9hub`.
-Start Cloud9 hub with ``node server``.
+## Firewall
+You will need ports 3000 and 5000 to however many connections will be taking place concurrently (each session is given a different port)
-##Running as a daemon
+## Running as a daemon
If you wish to, you can run it as a daemon, so that it stays alive.
To do so, I recommend [forever](https://npmjs.org/package/forever).
-##License
-[MIT License](http://opensource.org/licenses/MIT), baby.
+## License
+**This project:** [MIT License](http://opensource.org/licenses/MIT), baby.
+**Cloud9 itself:** [GPL](http://www.gnu.org/licenses/gpl.html)
-##WARNING
+## WARNING
This is highly insecure, experimental and it may bite.
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..96b9fc9
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,11 @@
+{
+ "name": "cloud9hub",
+ "version": "0.0.1",
+ "dependencies": {
+ "angular": "~1.2.15",
+ "topcoat": "~0.8.0",
+ "angular-route": "~1.2.15",
+ "fontawesome": "~4.0.3",
+ "flat-ui-official": "~2.1.3"
+ }
+}
diff --git a/config.js.example b/config.js.example
new file mode 100644
index 0000000..ee9ccab
--- /dev/null
+++ b/config.js.example
@@ -0,0 +1,16 @@
+exports.GITHUB_CLIENT_ID = '3909fc1ee9f8ed9e87f2';
+exports.GITHUB_CLIENT_SECRET = 'e5b3802ef773d8d32a8b29fe958d04e591be850d';
+
+// This allows everybody to sign up
+exports.PERMITTED_USERS = false;
+// This would only allow alice and bob to sign up
+//exports.PERMITTED_USERS = ['alice', 'bob'];
+
+// Add SSL Certificates to use Cloud9Hub over SSL
+// (Cloud9 Workspaces will still be unsecured standard HTTP)
+//exports.SSL = {
+// key: "/path/to/ssl.key",
+// cert: "/path/to/ssl.pem"
+//};
+
+exports.BASE_URL = 'http://localhost';
diff --git a/controllers/index.js b/controllers/index.js
new file mode 100644
index 0000000..f23046c
--- /dev/null
+++ b/controllers/index.js
@@ -0,0 +1,8 @@
+
+/*
+ * GET home page.
+ */
+
+exports.index = function(req, res){
+ res.render('index', { title: 'Express' });
+};
\ No newline at end of file
diff --git a/controllers/login.js b/controllers/login.js
new file mode 100644
index 0000000..7e76a0a
--- /dev/null
+++ b/controllers/login.js
@@ -0,0 +1,8 @@
+
+/*
+ * GET home page.
+ */
+
+exports.login = function(req, res){
+ res.render('login', {});
+};
\ No newline at end of file
diff --git a/controllers/workspaces.js b/controllers/workspaces.js
new file mode 100644
index 0000000..5faa5ad
--- /dev/null
+++ b/controllers/workspaces.js
@@ -0,0 +1,181 @@
+var fs = require('fs'),
+ path = require('path'),
+ rimraf = require('rimraf'),
+ _ = require('lodash'),
+ spawn = require('child_process').spawn;
+
+var respondInvalidWorkspace = function(res) {
+ res.status(400);
+ res.json({msg: "Invalid workspace name"});
+};
+
+var createWorkspace = function(params, req, res) {
+ var potentiallyBadPathName = params.name.split(path.sep);
+ var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
+
+ if(workspaceName === '..') {
+ respondInvalidWorkspace(res);
+ return;
+ }
+
+ fs.mkdir(__dirname + '/../workspaces/' + req.user + "/" + workspaceName, '0700', function(err) {
+ if(err) {
+ respondInvalidWorkspace(res);
+ return;
+ }
+
+ res.json({msg: "Workspace " + workspaceName + " was created."});
+ });
+}
+
+var createWorkspaceKillTimeout = function(req, workspaceProcess, workspaceName) {
+ var timeout = setTimeout(function() {
+ process.kill(-workspaceProcess.pid, 'SIGTERM');
+ req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = undefined;
+ console.info("Killed workspace " + workspaceName);
+ }, 900000); //Workspaces have a lifetime of 15 minutes
+
+ return timeout;
+};
+
+/*
+ * POST/GET create a new workspace
+ */
+exports.create = function(req, res) {
+ if(req.body.name) {
+ createWorkspace(req.body, req, res);
+ } else {
+ respondInvalidWorkspace(res);
+ }
+}
+
+/*
+ * GET workspaces listing.
+ */
+exports.list = function(req, res){
+ fs.readdir(__dirname + '/../workspaces/' + req.user, function(err, files) {
+ if(err) {
+ res.status(500);
+ res.json({error: err});
+ } else {
+ var workspaces = [];
+ for(var i=0; i< files.length; i++) {
+ // Skip hidden files
+ if(files[i][0] === '.') continue;
+
+ workspaces.push({name: files[i]})
+ }
+ res.json({workspaces: workspaces});
+ }
+ });
+};
+
+/**
+ * DELETE destroys a workspace
+ */
+exports.destroy = function(req, res) {
+ var potentiallyBadPathName = req.params.name.split(path.sep);
+ var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
+
+ if(workspaceName === '..') {
+ respondInvalidWorkspace(res);
+ return;
+ }
+
+ rimraf(__dirname + "/../workspaces/" + req.user + "/" + workspaceName, function(err) {
+ if(err) {
+ res.status("500");
+ res.json({msg: "Something went wrong :("});
+ return;
+ }
+ res.json({msg: "Successfully deleted " + workspaceName});
+ })
+};
+
+/*
+ * GET run a workspace
+ */
+ exports.run = function(req, res) {
+ var potentiallyBadPathName = req.params.name.split(path.sep);
+ var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
+
+ var isPortTaken = function(port, fn) {
+ console.log('checking if port', port, 'is taken');
+ var net = require('net')
+ var tester = net.createServer()
+ .once('error', function (err) {
+ if (err.code != 'EADDRINUSE') return fn(err)
+ console.log('port', port, 'seems to be taken');
+ fn(null, true)
+ })
+ .once('listening', function() {
+ tester.once('close', function() {
+ console.log('port', port, 'seems to be available');
+ fn(null, false)
+ })
+ .close()
+ })
+ .listen(port)
+ };
+
+ var getNextAvailablePort = function(callback){
+ var nextFreeWorkspacePort = req.app.get('nextFreeWorkspacePort');
+
+ if(nextFreeWorkspacePort > 10000) {
+ nextFreeWorkspacePort = 5000;
+ }
+
+ nextFreeWorkspacePort = nextFreeWorkspacePort + 1;
+ console.log('setting nextFreeWorkspacePort to', nextFreeWorkspacePort);
+ req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort);
+
+ isPortTaken(nextFreeWorkspacePort, function(err, taken){
+ if(taken){
+ getNextAvailablePort(callback);
+ } else {
+ req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort);
+ callback(nextFreeWorkspacePort);
+ }
+ });
+ };
+
+ if(workspaceName === '..') {
+ respondInvalidWorkspace(res);
+ return;
+ }
+
+ if(typeof req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] === 'undefined'){
+ getNextAvailablePort(function(nextFreePort){
+ console.log("Starting " + __dirname + '/../../c9/bin/cloud9.sh for workspace ' + workspaceName + " on port " + nextFreePort);
+
+ var workspace = spawn(__dirname + '/../../c9/bin/cloud9.sh', ['-w', __dirname + '/../workspaces/' + req.user + '/' + workspaceName, '-l', '0.0.0.0', '-p', nextFreePort], {detached: true});
+ workspace.stderr.on('data', function (data) {
+ console.log('stdERR: ' + data);
+ });
+
+ req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = {
+ killTimeout: createWorkspaceKillTimeout(req, workspace, workspaceName),
+ process: workspace,
+ name: workspaceName,
+ url: req.app.settings.baseUrl + ":" + nextFreePort,
+ user: req.user
+ };
+
+ res.json({msg: "Attempted to start workspace", user: req.user, url: req.app.settings.baseUrl + ":" + nextFreePort});
+ });
+ } else {
+ console.log("Found running workspace", req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url);
+ res.json({msg: "Found running workspace", user: req.user, url: req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url});
+ }
+
+ }
+
+/*
+ * POST to keep the workspace alive
+*/
+ exports.keepAlive = function(req, res) {
+ var workspace = req.app.get('runningWorkspaces')[req.user + '/' + req.params.name];
+ clearTimeout(workspace.killTimeout);
+ workspace.killTimeout = createWorkspaceKillTimeout(req, workspace.process, workspace.name);
+ res.send();
+ }
\ No newline at end of file
diff --git a/gruntfile.js b/gruntfile.js
new file mode 100644
index 0000000..9c9eb52
--- /dev/null
+++ b/gruntfile.js
@@ -0,0 +1,70 @@
+'use strict';
+
+module.exports = function(grunt) {
+ // Project Configuration
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ watch: {
+ options: {
+ livereload: true,
+ },
+ js: {
+ files: ['gruntfile.js', 'config.js', 'server.js', 'controllers/**', 'routes/**', 'public/js/**']
+ },
+ html: {
+ files: ['views/**', 'public/*.html', 'public/partials/**']
+ },
+ css: {
+ files: ['public/css/**']
+ },
+ sass: {
+ files: ['public/scss/**'],
+ tasks: ['sass'],
+ options: {
+ livereload: false
+ }
+ }
+ },
+ sass: {
+ dist: {
+ files: {
+ 'public/css/style.css': 'public/scss/style.scss'
+ }
+ }
+ },
+ nodemon: {
+ dev: {
+ script: 'server.js',
+ options: {
+ args: [],
+ ignore: ['public/**'],
+ ext: 'js',
+ nodeArgs: ['--debug'],
+ delayTime: 1,
+ env: {
+ PORT: 3105
+ },
+ cwd: __dirname
+ }
+ }
+ },
+ concurrent: {
+ tasks: ['nodemon', 'watch'],
+ options: {
+ logConcurrentOutput: true
+ }
+ }
+ });
+
+ //Load NPM tasks
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-nodemon');
+ grunt.loadNpmTasks('grunt-concurrent');
+ grunt.loadNpmTasks('grunt-sass');
+
+ //Making grunt default to force in order not to break the project.
+ grunt.option('force', true);
+
+ //Default task(s).
+ grunt.registerTask('default', ['sass', 'concurrent']);
+};
\ No newline at end of file
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..f589e72
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,29 @@
+#/bin/sh
+
+echo "Installing Cloud9..."
+echo "-----------------------"
+
+git clone https://github.com/ajaxorg/cloud9.git c9
+cd c9
+npm install
+npm install -g bower
+bower install
+cd ..
+
+echo "Success."
+echo ""
+echo "Installing Cloud9Hub..."
+echo "-----------------------"
+
+git clone https://github.com/AVGP/cloud9hub.git cloud9hub
+cd cloud9hub
+npm install
+echo "Success."
+
+echo "Last steps"
+echo "-----------------------"
+echo "1. Create a Github app."
+echo "2. Copy cloud9hub/config.js.example to cloud9hub/config.js"
+echo "3. Edit your cloud9hub/config.js"
+echo ""
+echo "Have a lot of fun!"
diff --git a/package.json b/package.json
index 1ccd0f7..2264b1d 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,24 @@
"start": "node server.js"
},
"dependencies": {
+ "connect-flash": "^0.1.1",
+ "consolidate": "^0.10.0",
"express": "3.2.6",
+ "forever": "~0.10.11",
+ "grunt": "~0.4.4",
+ "grunt-cli": "~0.1.13",
+ "grunt-concurrent": "^0.5.0",
+ "grunt-contrib-watch": "~0.6.1",
+ "grunt-env": "~0.4.1",
+ "grunt-nodemon": "~0.2.1",
+ "grunt-sass": "^0.11.0",
+ "jade": "*",
+ "lodash": "^2.4.1",
"passport": "0.1.17",
"passport-github": "0.1.5",
- "jade": "*",
+ "rimraf": "2.1.4",
"stylus": "*",
- "rimraf": "2.1.4"
+ "swig": "^1.3.2",
+ "view-helpers": "^0.1.4"
}
-}
\ No newline at end of file
+}
diff --git a/public/fonts/bello_script.eot b/public/fonts/bello_script.eot
new file mode 100755
index 0000000..3aae051
Binary files /dev/null and b/public/fonts/bello_script.eot differ
diff --git a/public/fonts/bello_script.svg b/public/fonts/bello_script.svg
new file mode 100755
index 0000000..78223e9
--- /dev/null
+++ b/public/fonts/bello_script.svg
@@ -0,0 +1,767 @@
+
+
+!"#$%&'()*+,-./0123456789:;å<>? @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ ` abcdefghijklmnopqrstuvwxyz|{}~
\ No newline at end of file
diff --git a/public/fonts/bello_script.ttf b/public/fonts/bello_script.ttf
new file mode 100755
index 0000000..cb5a43e
Binary files /dev/null and b/public/fonts/bello_script.ttf differ
diff --git a/public/fonts/bello_script.woff b/public/fonts/bello_script.woff
new file mode 100755
index 0000000..5e0d0e5
Binary files /dev/null and b/public/fonts/bello_script.woff differ
diff --git a/public/images/classy_fabric.png b/public/images/classy_fabric.png
new file mode 100644
index 0000000..ca3d267
Binary files /dev/null and b/public/images/classy_fabric.png differ
diff --git a/public/images/classy_fabric_@2X.png b/public/images/classy_fabric_@2X.png
new file mode 100644
index 0000000..13db1c1
Binary files /dev/null and b/public/images/classy_fabric_@2X.png differ
diff --git a/public/images/dark_leather.png b/public/images/dark_leather.png
new file mode 100644
index 0000000..3ce4b73
Binary files /dev/null and b/public/images/dark_leather.png differ
diff --git a/public/images/dark_leather_@2X.png b/public/images/dark_leather_@2X.png
new file mode 100644
index 0000000..a4a8768
Binary files /dev/null and b/public/images/dark_leather_@2X.png differ
diff --git a/public/images/debut_dark.png b/public/images/debut_dark.png
new file mode 100644
index 0000000..17a4d6b
Binary files /dev/null and b/public/images/debut_dark.png differ
diff --git a/public/images/debut_dark_@2X.png b/public/images/debut_dark_@2X.png
new file mode 100644
index 0000000..13565fb
Binary files /dev/null and b/public/images/debut_dark_@2X.png differ
diff --git a/public/images/denim.png b/public/images/denim.png
new file mode 100644
index 0000000..3b0bb7e
Binary files /dev/null and b/public/images/denim.png differ
diff --git a/public/images/denim_@2X.png b/public/images/denim_@2X.png
new file mode 100644
index 0000000..815a2c7
Binary files /dev/null and b/public/images/denim_@2X.png differ
diff --git a/public/index.html b/public/index.html
deleted file mode 100644
index a91de8d..0000000
--- a/public/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- Cloud9Hub
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/javascripts/workspace/workspaceController.js b/public/javascripts/workspace/workspaceController.js
deleted file mode 100644
index 30acb87..0000000
--- a/public/javascripts/workspace/workspaceController.js
+++ /dev/null
@@ -1,52 +0,0 @@
-var WorkspaceCtrl = function($scope, $http, $timeout) {
- $scope.workspaces = [];
- $scope.currentWorkspace = {name: null, url: '/welcome.html'};
-
- $http.get('/workspace')
- .success(function(data) { console.log(data); $scope.workspaces = data.workspaces; })
- .error(function(err) { alert(err.msg) });
-
- $scope.createWorkspace = function() {
- var wsName = window.prompt("Enter workspace name", "");
- $http.post('/workspace/', {name: wsName})
- .success(function(data) {
- alert(data.msg);
- $scope.workspaces.push({name: wsName});
- })
- .error(function(err) {
- alert("Error: " + err.msg);
- });
- }
-
- $scope.deleteWorkspace = function(name) {
- if(!window.confirm("Do you really want to delete this workspace? All your files in that workspace will be gone forever!")) return;
- $http.delete('/workspace/' + name)
- .success(function(data){
- console.log(data);
- alert(data.msg);
- for(var i=0;i<$scope.workspaces.length;i++) {
- if($scope.workspaces[i].name === name) {
- $scope.workspaces.splice(i,1);
- break;
- }
- }
- })
- .error(function(err) {
- console.log("ERR:", err);
- });
- }
-
- $scope.runWorkspace = function(name) {
- console.log(name);
- $http.get('/workspace/' + name).success(function(data) {
- console.log(data);
- $timeout(function() {
- $scope.currentWorkspace.url = data.url;
- }, 2000);
- }).error(function(err) {
- alert("Error: " + err);
- console.log(err);
- });
- $scope.currentWorkspace.name = name;
- }
-}
\ No newline at end of file
diff --git a/public/javascripts/app.js b/public/js/app.js
similarity index 68%
rename from public/javascripts/app.js
rename to public/js/app.js
index e1b6967..74c68e6 100644
--- a/public/javascripts/app.js
+++ b/public/js/app.js
@@ -1,4 +1,4 @@
-angular.module('c9hub', ['workspace']).config(function($routeProvider) {
+angular.module('c9hub', ['workspace', 'ngRoute']).config(function($routeProvider) {
$routeProvider.when('/', {templateUrl: "/partials/login.html"});
$routeProvider.when('/dashboard', {controller: WorkspaceCtrl, templateUrl: "/partials/workspace.html"});
});
\ No newline at end of file
diff --git a/public/js/workspace/workspaceController.js b/public/js/workspace/workspaceController.js
new file mode 100644
index 0000000..bcc46e0
--- /dev/null
+++ b/public/js/workspace/workspaceController.js
@@ -0,0 +1,94 @@
+var WorkspaceCtrl = function($scope, $http, $timeout, $sce) {
+ $scope.workspaces = [];
+ $scope.currentWorkspace = false; // {name: null, url: null, editing: false};
+ $scope.loadingWorkspace = false;
+ $scope.iframeSrc = '';
+
+ $http.get('/workspace')
+ .success(function(data) { console.log(data); $scope.workspaces = data.workspaces; })
+ .error(function(err) { alert(err.msg) });
+
+ var _sendKeepAlive = function() {
+ $http.post('/workspace/' + $scope.currentWorkspace.name + '/keepalive').success(function() {
+ $timeout(_sendKeepAlive, 300000);
+ });
+ };
+
+ $scope.startEditing = function() {
+ $scope.currentWorkspace.editing = true;
+ $scope.iframeSrc = $sce.trustAsResourceUrl($scope.currentWorkspace.url);
+ };
+
+ var createWorkspace = function() {
+ var wsName = $scope.currentWorkspace.name;
+ $scope.loadingWorkspace = true;
+ $http.post('/workspace/', {name: wsName})
+ .success(function(data) {
+ // alert(data.msg);
+ $scope.loadingWorkspace = false;
+ $scope.workspaces.push({name: wsName});
+ $scope.currentWorkspace = false;
+ })
+ .error(function(err) {
+ alert("Error: " + err.msg);
+ });
+ }
+
+ $scope.saveWorkspace = function(){
+ $scope.iframeSrc = '';
+ if(!$scope.currentWorkspace.url){
+ createWorkspace();
+ }
+ };
+
+ $scope.blankWorkspace = function() {
+ $scope.iframeSrc = '';
+ $scope.currentWorkspace = {
+ url: '',
+ name: '',
+ editing: false
+ }
+ };
+
+ $scope.deleteWorkspace = function(name) {
+ $scope.iframeSrc = '';
+ if(!window.confirm("Do you really want to delete this workspace? All your files in that workspace will be gone forever!")) return;
+ $http.delete('/workspace/' + name)
+ .success(function(data){
+ console.log(data);
+ alert(data.msg);
+
+ for(var i=0;i<$scope.workspaces.length;i++) {
+ if($scope.workspaces[i].name === name) {
+ $scope.workspaces.splice(i,1);
+ break;
+ }
+ }
+
+ $scope.currentWorkspace = false;
+ })
+ .error(function(err) {
+ console.log("ERR:", err);
+ $scope.currentWorkspace = false;
+ });
+ }
+
+ $scope.runWorkspace = function(name) {
+ $scope.iframeSrc = '';
+ $scope.loadingWorkspace = true;
+ $http.get('/workspace/' + name).success(function(data) {
+ console.log('data', data);
+ $scope.currentWorkspace = {};
+ $scope.currentWorkspace.name = name;
+ $scope.currentWorkspace.url = data.url;
+ $scope.currentWorkspace.user = data.user;
+ $scope.currentWorkspace.editing = false;
+ _sendKeepAlive();
+ $scope.loadingWorkspace = false;
+ }).error(function(err) {
+ $scope.loadingWorkspace = false;
+ alert("Error: " + err);
+ console.log(err);
+ });
+ }
+}
\ No newline at end of file
diff --git a/public/javascripts/workspace/workspaceModule.js b/public/js/workspace/workspaceModule.js
similarity index 100%
rename from public/javascripts/workspace/workspaceModule.js
rename to public/js/workspace/workspaceModule.js
diff --git a/public/partials/login.html b/public/partials/login.html
index 84a9f1f..d8fdd92 100644
--- a/public/partials/login.html
+++ b/public/partials/login.html
@@ -1,4 +1,4 @@
-Welcome to CloudHub!
-
- Please Log in with Github
+
Welcome to Cloud9Hub!
+
+ Sign in with Github
\ No newline at end of file
diff --git a/public/partials/workspace.html b/public/partials/workspace.html
index 6e25400..cecfe91 100644
--- a/public/partials/workspace.html
+++ b/public/partials/workspace.html
@@ -1,10 +1,57 @@
-
- Create new workspace
-
-
-
\ No newline at end of file
+
+
+
+
Cloud9Hub
+ Create workspace
+
+
+
+
+
+
+ Welcome to Cloud9Hub.
+ This part of the screen is the editor panel.
+ When you run a workspace, it will be started here.
+
+
+ On the left you see the workspace panel .
+ You can create, delete or run workspaces there.
+
+
+ To create a workspace, click the "Create workspace" button.
+ If you want to run a workspace, click on its name. The workspace will start automatically.
+ To delete a workspace, click the little "☠" to the right of a workspace item.
+
+
+
+
+
+ New Workspace
+ {{currentWorkspace.user}} / {{currentWorkspace.name}}
+
+
+
+
+
+
+
+
+
Loading...
+
+
\ No newline at end of file
diff --git a/public/scss/style.scss b/public/scss/style.scss
new file mode 100644
index 0000000..a0c35d7
--- /dev/null
+++ b/public/scss/style.scss
@@ -0,0 +1,262 @@
+@font-face {
+ font-family: 'Bello Script';
+ src: url('/fonts/bello_script.eot');
+ src: url('/fonts/bello_script.eot?#iefix') format('embedded-opentype'), url('/fonts/bello_script.svg#Bello Script') format('svg'), url('/fonts/bello_script.woff') format('woff'), url('/fonts/bello_script.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+/*
+ * Element definitions
+ */
+body {
+ padding: 0;
+ // font-family: source-sans-pro, Helvetica, Arial, sans-serif;
+ height: 100%;
+ background-color: #ECF0F1;
+}
+
+.container{
+ width: 100%;
+}
+
+.dashboard-row{
+ height: 100%;
+}
+
+aside{
+ padding: 15px 15px 15px 15px;
+ background: #34495E;
+ color: #fff;
+ height: 100%;
+
+ h1{
+ font-weight: 300;
+ text-align:center;
+ margin-top: 0;
+ }
+}
+
+.bello{
+ font-family: 'Bello Script';
+}
+
+a {
+ color: #288edf;
+}
+
+/*button {
+ background-color: #083;
+ border: outset 1px #0c8;
+ color: #fff;
+ border-radius: 6px;
+ padding: 2px;
+}*/
+
+/*
+ * Class definitions
+ */
+.active {
+ font-weight: bold;
+}
+.block {
+ margin-bottom: 1.2em;
+}
+.centered {
+ text-align: center;
+}
+
+.delete {
+ color: #eee;
+ display: inline-block;
+ float: right;
+ cursor: pointer;
+
+ i{
+ position: relative;
+ left: 2px;
+ }
+
+ &:hover i{
+ opacity: 1;
+ }
+}
+
+.frameWrapper {
+ height: 100%;
+}
+.hint {
+ font-style: oblique;
+ color: #27AE60;
+}
+.hScope {
+ height: 100%;
+}
+
+.menu-list {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.menu-list li {
+ font-weight: 300;
+ font-size: 1em;
+ padding: 1.5rem 1.5rem;
+ cursor: pointer;
+ background: #95A5A6;
+ border-top: 2px solid #7F8C8D;
+
+ transition: background-color 0.25s ease;
+ -moz-transition: background-color 0.25s ease;
+ -webkit-transition: background-color 0.25s ease;
+
+ span{
+ font-size: 1em;
+ }
+
+ i{
+ opacity: 0.2;
+ }
+
+ &:first-child{
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ border-top-width: 0px;
+ }
+
+ &:last-child{
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ border-bottom: 5px solid #7F8C8D;
+ }
+
+ &:hover{
+ background: #7F8C8D;
+ }
+
+ &:hover i{
+ opacity: 0.6;
+ }
+}
+
+.workspace {
+ border: none;
+}
+
+.workspace-wrapper {
+ height: 100%;
+ padding: 0 1em;
+ background: #FFF;
+}
+
+
+.workspace-bar{
+ background: #2C3E50;
+ padding: 1em;
+ margin: 0em -1em 2em -1em;
+}
+
+.workspace-form-footer{
+ padding-top: 1rem;
+ margin-top: 2rem;
+ text-align: right;
+}
+
+header{
+ position: relative;
+ margin-top: -1em;
+ margin-left: -1em;
+ margin-right: -1em;
+ padding: 2em 1em;
+ background-color: #34495E;
+
+ h1 {
+ font-weight: 100;
+ margin: 10px 0 0px;
+ font-size: 2em;
+ position: relative;
+ display: inline-block;
+ padding-right: 10px;
+ color: #fff;
+
+ span{
+ color: #aaa;
+ }
+ }
+}
+
+.loading-overlay{
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: rgb(44,62,80);
+ color: #fff;
+ font-size: 2em;
+ font-weight: 300;
+ text-align: center;
+
+ span{
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -73px;
+ margin-top: -18px;
+ }
+}
+
+.no-workspace{
+ color: #BDC3C7;
+ padding: 2rem;
+ border-radius: 10px;
+ width: 41em;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-top: -173px;
+ margin-left: -370px;
+
+ h1{
+ font-weight: normal;
+ margin: 0 0 1rem 0;
+ }
+
+ .block{
+ font-size: 1em;
+ }
+}
+
+.login-row{
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-top: -125px;
+ margin-left: -291.5px;
+}
+
+.login-col{
+ width: 583px;
+ height: 171px;
+}
+
+.login-title{
+ font-size: 6rem;
+ font-weight: normal;
+}
+
+.login-button{
+ //font-size: 1.25rem;
+ //padding: 1rem;
+}
+
+.workspace-iframe{
+ height: 100%;
+ padding: 0;
+ margin: 0 -1em;
+}
+
+.panel{
+ padding: 2rem;
+}
\ No newline at end of file
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
deleted file mode 100644
index 16e5187..0000000
--- a/public/stylesheets/style.css
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Element definitions
- */
-body {
- box-sizing: border-box;
- padding: 0;
- font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
- height: 100%;
- background-color: #f0f0f0;
-}
-a {
- color: #00b7ff;
-}
-aside {
- float: left;
- min-width: 100px;
- width: 10%;
-}
-button {
- background-color: #083;
- border: outset 1px #0c8;
- color: #fff;
- border-radius: 6px;
- padding: 2px;
-}
-/*
- * Class definitions
- */
-.active {
- font-weight: bold;
-}
-.block {
- margin-bottom: 1.2em;
-}
-.delete {
- font-weight: bold;
- color: #f00;
- display: inline-block;
- float: right;
-}
-.hint {
- font-style: oblique;
- color: #083;
-}
-.menu-list {
- margin: 0;
- padding: 0px 0;
- list-style-type: none;
-}
-.menu-list li {
- padding: 5px 0px 2px 0px;
- border-bottom: 1px dashed #083;
-}
-.menu-list li:before {
- content: "▹";
- padding-right: 4px;
-}
-.workspace {
- position: relative;
- border: none;
- box-sizing: border-box;
- min-width: 600px;
- width: 90%;
- height: 100%;
- min-height: 100%;
-}
diff --git a/public/stylesheets/style.styl b/public/stylesheets/style.styl
deleted file mode 100644
index d6b433d..0000000
--- a/public/stylesheets/style.styl
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Element definitions
- */
-body
- box-sizing: border-box;
- padding: 0
- font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
- height: 100%
- background-color: rgb(240, 240, 240);
-
-a
- color: #00B7FF
-
-aside
- float: left
- min-width: 100px
- width: 10%
-
-button
- background-color: #083
- border: outset 1px #0c8
- color: #fff
- border-radius: 6px
- padding: 2px
-
-/*
- * Class definitions
- */
-
-.active
- font-weight: bold
-
-.block
- margin-bottom: 1.2em
-
-.delete {
- font-weight: bold;
- color: red;
- display: inline-block;
- float: right;
-}
-
-.hint
- font-style: oblique
- color: #083
-
-.menu-list
- margin: 0
- padding: 0px 0
- list-style-type: none
-
-.menu-list li
- padding: 5px 0px 2px 0px
- border-bottom: 1px dashed #083
-.menu-list li:before
- content: "▹"
- padding-right: 4px
-
-.workspace
- position: relative
- border: none
- box-sizing: border-box
- min-width: 600px
- width: 90%
- height: 100%
- min-height: 100%
diff --git a/routes/index.js b/routes/index.js
index f296005..a1eae6f 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -1,8 +1,8 @@
+'use strict';
-/*
- * GET home page.
- */
+var index = require('../controllers/index');
+var authorization = require('./middlewares/authorization');
-exports.index = function(req, res){
- res.render('index', { title: 'Express' });
-};
\ No newline at end of file
+module.exports = function(app) {
+ app.get('/', authorization.redirectToLogin, index.index);
+}
\ No newline at end of file
diff --git a/routes/login.js b/routes/login.js
new file mode 100644
index 0000000..de843a5
--- /dev/null
+++ b/routes/login.js
@@ -0,0 +1,8 @@
+'use strict';
+
+var login = require('../controllers/login');
+var authorization = require('./middlewares/authorization');
+
+module.exports = function(app) {
+ app.get('/login', login.login);
+}
\ No newline at end of file
diff --git a/routes/middlewares/authorization.js b/routes/middlewares/authorization.js
new file mode 100644
index 0000000..3f4c2c9
--- /dev/null
+++ b/routes/middlewares/authorization.js
@@ -0,0 +1,18 @@
+'use strict';
+
+/**
+ * Generic require login routing middleware
+ */
+exports.requiresLogin = function(req, res, next) {
+ if (!req.isAuthenticated()) {
+ return res.send(401, 'User is not authorized');
+ }
+ next();
+};
+
+exports.redirectToLogin = function(req, res, next) {
+ if (!req.isAuthenticated()) {
+ return res.redirect('/login');
+ }
+ next();
+}
diff --git a/routes/workspace.js b/routes/workspace.js
index d09c8f4..4807de5 100644
--- a/routes/workspace.js
+++ b/routes/workspace.js
@@ -1,115 +1,12 @@
-var fs = require('fs'),
- path = require('path'),
- rimraf = require('rimraf'),
- spawn = require('child_process').spawn;
+'use strict';
-var respondInvalidWorkspace = function(res) {
- res.status(400);
- res.json({msg: "Invalid workspace name"});
-};
+var workspaces = require('../controllers/workspaces');
+var authorization = require('./middlewares/authorization');
-var createWorkspace = function(params, req, res) {
- var potentiallyBadPathName = params.name.split(path.sep);
- var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
-
- if(workspaceName === '..') {
- respondInvalidWorkspace(res);
- return;
- }
-
- fs.mkdir(__dirname + '/../workspaces/' + req.user + "/" + workspaceName, '0700', function(err) {
- if(err) {
- respondInvalidWorkspace(res);
- return;
- }
-
- res.json({msg: "Workspace " + workspaceName + " was created."});
- });
-}
-
-/*
- * POST/GET create a new workspace
- */
-exports.create = function(req, res) {
- if(req.body.name) {
- createWorkspace(req.body, req, res);
- } else {
- respondInvalidWorkspace(res);
- }
+module.exports = function(app) {
+ app.get('/workspace', authorization.requiresLogin, workspaces.list);
+ app.post('/workspace', authorization.requiresLogin, workspaces.create);
+ app.get('/workspace/:name', authorization.requiresLogin, workspaces.run);
+ app.post('/workspace/:name/keepalive', authorization.requiresLogin, workspaces.keepAlive);
+ app.delete('/workspace/:name', authorization.requiresLogin, workspaces.destroy);
}
-
-/*
- * GET workspaces listing.
- */
-exports.list = function(req, res){
- fs.readdir(__dirname + '/../workspaces/' + req.user, function(err, files) {
- if(err) {
- res.status(500);
- res.json({error: err});
- } else {
- var workspaces = [];
- for(var i=0; i< files.length; i++) {
- // Skip hidden files
- if(files[i][0] === '.') continue;
-
- workspaces.push({name: files[i]})
- }
- res.json({workspaces: workspaces});
- }
- });
-};
-
-/**
- * DELETE destroys a workspace
- */
-exports.destroy = function(req, res) {
- console.log(req.params.name);
-
- var potentiallyBadPathName = req.params.name.split(path.sep);
- var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
-
- if(workspaceName === '..') {
- respondInvalidWorkspace(res);
- return;
- }
-
- rimraf(__dirname + "/../workspaces/" + req.user + "/" + workspaceName, function(err) {
- console.log(err);
- if(err) {
- res.status("500");
- res.json({msg: "Something went wrong :("});
- return;
- }
- res.json({msg: "Successfully deleted " + workspaceName});
- })
-};
-
-/*
- * GET run a workspace
- */
- exports.run = function(req, res) {
- var potentiallyBadPathName = req.params.name.split(path.sep);
- var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1];
-
- if(workspaceName === '..') {
- respondInvalidWorkspace(res);
- return;
- }
-
- console.log("Starting " + __dirname + '/../../c9/bin/cloud9.sh for workspace ' + workspaceName + " on port " + req.nextFreePort);
-
- var workspace = spawn(__dirname + '/../../c9/bin/cloud9.sh', ['-w', __dirname + '/../workspaces/' + req.user + '/' + workspaceName, '-l', '0.0.0.0', '-p', req.nextFreePort], {detached: true});
- workspace.stdout.on('data', function (data) {
- console.log('stdout: ' + data);
- });
- workspace.stderr.on('data', function (data) {
- console.log('stdERR: ' + data);
- });
-
- setTimeout(function() {
- process.kill(-workspace.pid, 'SIGTERM');
- console.log("Killed workspace " + workspaceName);
- }, 900000); //Workspaces have a lifetime of 15 minutes
-
- res.json({msg: "Successfully started workspace", url: req.app.settings.baseUrl + ":" + req.nextFreePort});
- }
\ No newline at end of file
diff --git a/server.js b/server.js
index 7563fe5..44b5ffc 100644
--- a/server.js
+++ b/server.js
@@ -1,4 +1,3 @@
-
/**
* Module dependencies.
*/
@@ -6,64 +5,90 @@
var express = require('express')
, routes = require('./routes')
, workspace = require('./routes/workspace')
+ , index = require('./routes/index')
, fs = require('fs')
, path = require('path')
, http = require('http')
+ , https = require('https')
, path = require('path')
, passport = require('passport')
+ , flash = require('connect-flash')
+ , helpers = require('view-helpers')
+ , consolidate = require('consolidate')
, GithubStrategy = require('passport-github').Strategy;
+try {
+ var config = require(__dirname + '/config.js');
+} catch(e) {
+ console.error("No config.js found! Copy and edit config.example.js to config.js!");
+ process.exit(1);
+}
+// Load configurations
+// Set the node enviornment variable if not set before
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
var app = express();
-var nextFreeWorkspacePort = 5000;
+app.set('showStackError', true);
+// cache=memory or swig dies in NODE_ENV=production
+app.locals.cache = 'memory';
+// Prettify HTML
+app.locals.pretty = true;
+
+app.set('nextFreeWorkspacePort', 5000);
+
+app.engine('html', consolidate.swig);
+
+// Start the app by listening on
+var port = process.env.PORT || 3105;
// all environments
-app.set('port', 3000);
+app.set('port', port);
+app.set('view engine', 'html');
app.set('views', __dirname + '/views');
-app.set('view engine', 'jade');
-app.set('baseUrl', 'http://82.196.2.177');
+app.set('baseUrl', config.BASE_URL);
+app.set('runningWorkspaces', {});
//Auth
passport.use(new GithubStrategy({
- clientID: '3909fc1ee9f8ed9e87f2',
- clientSecret: 'e5b3802ef773d8d32a8b29fe958d04e591be850d',
+ clientID: config.GITHUB_CLIENT_ID,
+ clientSecret: config.GITHUB_CLIENT_SECRET,
callbackURL: app.get('baseUrl') + ':' + app.get('port') + '/auth/github/callback'
},
function(accessToken, refreshToken, profile, done) {
- console.log(profile);
- if(!fs.existsSync(__dirname + '/workspaces/' + path.basename(profile.id))) {
- if(!fs.mkdirSync(__dirname + '/workspaces/' + path.basename(profile.id), '0700')) {
- console.log("Okay, shit");
- return done(err, null);
+ var username = path.basename(profile.username.toLowerCase());
+ if(!fs.existsSync(__dirname + '/workspaces/' + path.basename(username))) {
+ if(config.PERMITTED_USERS !== false && config.PERMITTED_USERS.indexOf(username)) return done('Sorry, not allowed :(', null);
+
+ //Okay, that is slightly unintuitive: fs.mkdirSync returns "undefined", when successful..
+ if(fs.mkdirSync(__dirname + '/workspaces/' + path.basename(username), '0700') !== undefined) {
+ return done("Cannot create user", null);
} else {
- return done(null, path.basename(profile.id));
+ return done(null, username);
}
}
- return done(null, path.basename(profile.id));
+ return done(null, username);
}
));
//Middlewares
app.use(express.favicon());
-app.use(express.logger('dev'));
+// Only use logger for development environment
+if (process.env.NODE_ENV === 'development') {
+ app.use(express.logger('dev'));
+}
app.use(express.bodyParser());
app.use(express.methodOverride());
-app.use(express.cookieParser('your secret here'));
+app.use(express.cookieParser('cloud9hub secret'));
app.use(express.session());
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
-app.use(function(req, res, next) {
- console.log(req.path);
- if(/\/workspace\/.+/.test(req.path)) {
- req.nextFreePort = (nextFreeWorkspacePort++);
- }
- next();
-});
+app.use(flash());
+// Dynamic helpers
+app.use(helpers('Cloud9Hub'));
app.use(app.router);
-app.use(require('stylus').middleware(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));
// development only
@@ -83,30 +108,50 @@ app.get('/logout', function(req, res){
req.logout();
res.json('OK');
});
-// API
-app.get('/workspace', ensureAuthenticated, workspace.list);
-app.post('/workspace', ensureAuthenticated, workspace.create);
-app.get('/workspace/:name', ensureAuthenticated, workspace.run);
-app.delete('/workspace/:name', ensureAuthenticated, workspace.destroy);
-http.createServer(app).listen(app.get('port'), function(){
+// Bootstrap routes
+var routes_path = __dirname + '/routes';
+var walk = function(path) {
+ fs.readdirSync(path).forEach(function(file) {
+ var newPath = path + '/' + file;
+ var stat = fs.statSync(newPath);
+ if (stat.isFile()) {
+ if (/(.*)\.(js$|coffee$)/.test(file)) {
+ require(newPath)(app, passport);
+ }
+ // We skip the app/routes/middlewares directory as it is meant to be
+ // used and shared by routes as further middlewares and is not a
+ // route by itself
+ } else if (stat.isDirectory() && file !== 'middlewares') {
+ walk(newPath);
+ }
+ });
+};
+walk(routes_path);
+
+var server;
+
+if (config.SSL && config.SSL.key && config.SSL.cert) {
+ var sslOpts = {
+ key: fs.readFileSync(config.SSL.key),
+ cert: fs.readFileSync(config.SSL.cert)
+ };
+
+ server = https.createServer(sslOpts, app);
+} else {
+ server = http.createServer(app);
+}
+
+server.listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
//Helpers
passport.serializeUser(function(user, done) {
- console.log("SERIALIZE");
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
-
-
-function ensureAuthenticated(req, res, next) {
- if (req.isAuthenticated()) { return next(); }
- res.status(401);
- res.json({msg: "Please login first!"});
-}
diff --git a/views/includes/footer.html b/views/includes/footer.html
new file mode 100644
index 0000000..5f859e3
--- /dev/null
+++ b/views/includes/footer.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% if (process.env.NODE_ENV == 'development') %}
+
+
+{% endif %}
\ No newline at end of file
diff --git a/views/includes/header.html b/views/includes/header.html
new file mode 100644
index 0000000..d122e82
--- /dev/null
+++ b/views/includes/header.html
@@ -0,0 +1,12 @@
+
+ Cloud9Hub
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/index.html b/views/index.html
new file mode 100644
index 0000000..b03f625
--- /dev/null
+++ b/views/index.html
@@ -0,0 +1,4 @@
+{% extends 'layouts/default.html' %}
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/views/layouts/default.html b/views/layouts/default.html
new file mode 100644
index 0000000..a06929a
--- /dev/null
+++ b/views/layouts/default.html
@@ -0,0 +1,8 @@
+
+
+ {% include '../includes/header.html' %}
+
+ {% block content %}{% endblock %}
+ {% include '../includes/footer.html' %}
+
+
\ No newline at end of file
diff --git a/views/login.html b/views/login.html
new file mode 100644
index 0000000..1fea85b
--- /dev/null
+++ b/views/login.html
@@ -0,0 +1,11 @@
+{% extends 'layouts/default.html' %}
+{% block content %}
+
+{% endblock %}