├── .bowerrc ├── .gitignore ├── README.md ├── bower.json ├── config.js.example ├── controllers ├── index.js ├── login.js └── workspaces.js ├── gruntfile.js ├── install.sh ├── package.json ├── public ├── fonts │ ├── bello_script.eot │ ├── bello_script.svg │ ├── bello_script.ttf │ └── bello_script.woff ├── images │ ├── classy_fabric.png │ ├── classy_fabric_@2X.png │ ├── dark_leather.png │ ├── dark_leather_@2X.png │ ├── debut_dark.png │ ├── debut_dark_@2X.png │ ├── denim.png │ └── denim_@2X.png ├── js │ ├── app.js │ └── workspace │ │ ├── workspaceController.js │ │ └── workspaceModule.js ├── partials │ ├── login.html │ └── workspace.html ├── scss │ └── style.scss └── welcome.html ├── routes ├── index.js ├── login.js ├── middlewares │ └── authorization.js └── workspace.js ├── server.js ├── views ├── includes │ ├── footer.html │ └── header.html ├── index.html ├── layouts │ └── default.html └── login.html └── workspaces └── .gitkeep /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/lib", 3 | "storage": { 4 | "packages": ".bower-cache", 5 | "registry": ".bower-registry" 6 | }, 7 | "tmp": ".bower-tmp" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | workspaces/* 2 | node_modules 3 | config.js 4 | .bower-*/ 5 | public/lib 6 | .c9revisions 7 | .settings 8 | public/css/* 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud9Hub 2 | 3 | ## What's this? 4 | It's a simple interface for the Cloud 9 open source edition to easily create, use and manage multiple workspaces. 5 | [The Cloud9 service](https://c9.io) has a shiny and awesome dashboard interface where you can manage multiple workspaces, 6 | however the [open source edition](https://github.com/ajaxorg/cloud9) is a single workspace instance of Cloud9. 7 | 8 | As I like the possibility to easily start working on different workspaces, create or delete them, I created Cloud9Hub to do so. 9 | 10 | ## What's Cloud9? 11 | 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) 12 | and [lots more](http://en.wikipedia.org/wiki/Cloud9_IDE#Features). 13 | 14 | ## Status Quo of Cloud9Hub 15 | Right now it can 16 | * Create new workspaces 17 | * Launch multiple workspace instances 18 | * Kill them automatically after 15 minutes 19 | * List available workspaces 20 | * Delete workspaces 21 | * Manage multiple users 22 | * Do authentication/sessions 23 | * Sense, that you're active and will kill your workspace after 15-20 minutes of inactivity. 24 | 25 | right now. These are the next steps for me to build (or you make a Pull Request with the features you want). 26 | 27 | ## Installation 28 | First you will need [node.js](http://nodejs.org/), at least v0.8. 29 | 30 | Then you can try the quick install or the manual way: 31 | 32 | ### Quick install 33 | 34 | ```shell 35 | curl https://raw.githubusercontent.com/AVGP/cloud9hub/master/install.sh | sh 36 | ``` 37 | 38 | This should install Cloud9 and Cloud9Hub into the current folder. If this succeeded, you can now go to the configuration section. 39 | 40 | ### Manual installation 41 | 1. Install [Cloud9](https://github.com/ajaxorg/cloud9) into some folder, say ``/var/awesomeness/cloud9``. 42 | **Note, the cloud9 is currently hardcoded to c9. when cloning cloud9, clone to c9 dir. If this isn't done, hub will crash. 43 | 2. Then install Cloud9Hub into the parent folder above your cloud9 installation, so in my example``/var/awesomeness/cloud9hub` and run ``npm install``. 44 | 45 | # Configuration 46 | 47 | First things first: You need a Github application to provide the "Login with Github" feature, which is currently the only login mechanism. 48 | 49 | 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. 50 | 51 | Now copy the ``config.js.example`` to ``config.js`` and edit the contents: 52 | 53 | - Add your Github client ID and secret 54 | - Change your BASE_URL to your server's address (do not include the port!) 55 | 56 | ## Firewall 57 | You will need ports 3000 and 5000 to however many connections will be taking place concurrently (each session is given a different port) 58 | 59 | ## Running as a daemon 60 | If you wish to, you can run it as a daemon, so that it stays alive. 61 | 62 | To do so, I recommend [forever](https://npmjs.org/package/forever). 63 | 64 | ## License 65 | **This project:** [MIT License](http://opensource.org/licenses/MIT), baby. 66 | **Cloud9 itself:** [GPL](http://www.gnu.org/licenses/gpl.html) 67 | 68 | ## WARNING 69 | This is highly insecure, experimental and it may bite. 70 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud9hub", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "angular": "~1.2.15", 6 | "topcoat": "~0.8.0", 7 | "angular-route": "~1.2.15", 8 | "fontawesome": "~4.0.3", 9 | "flat-ui-official": "~2.1.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config.js.example: -------------------------------------------------------------------------------- 1 | exports.GITHUB_CLIENT_ID = '3909fc1ee9f8ed9e87f2'; 2 | exports.GITHUB_CLIENT_SECRET = 'e5b3802ef773d8d32a8b29fe958d04e591be850d'; 3 | 4 | // This allows everybody to sign up 5 | exports.PERMITTED_USERS = false; 6 | // This would only allow alice and bob to sign up 7 | //exports.PERMITTED_USERS = ['alice', 'bob']; 8 | 9 | // Add SSL Certificates to use Cloud9Hub over SSL 10 | // (Cloud9 Workspaces will still be unsecured standard HTTP) 11 | //exports.SSL = { 12 | // key: "/path/to/ssl.key", 13 | // cert: "/path/to/ssl.pem" 14 | //}; 15 | 16 | exports.BASE_URL = 'http://localhost'; 17 | -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.index = function(req, res){ 7 | res.render('index', { title: 'Express' }); 8 | }; -------------------------------------------------------------------------------- /controllers/login.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * GET home page. 4 | */ 5 | 6 | exports.login = function(req, res){ 7 | res.render('login', {}); 8 | }; -------------------------------------------------------------------------------- /controllers/workspaces.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | rimraf = require('rimraf'), 4 | _ = require('lodash'), 5 | spawn = require('child_process').spawn; 6 | 7 | var respondInvalidWorkspace = function(res) { 8 | res.status(400); 9 | res.json({msg: "Invalid workspace name"}); 10 | }; 11 | 12 | var createWorkspace = function(params, req, res) { 13 | var potentiallyBadPathName = params.name.split(path.sep); 14 | var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1]; 15 | 16 | if(workspaceName === '..') { 17 | respondInvalidWorkspace(res); 18 | return; 19 | } 20 | 21 | fs.mkdir(__dirname + '/../workspaces/' + req.user + "/" + workspaceName, '0700', function(err) { 22 | if(err) { 23 | respondInvalidWorkspace(res); 24 | return; 25 | } 26 | 27 | res.json({msg: "Workspace " + workspaceName + " was created."}); 28 | }); 29 | } 30 | 31 | var createWorkspaceKillTimeout = function(req, workspaceProcess, workspaceName) { 32 | var timeout = setTimeout(function() { 33 | process.kill(-workspaceProcess.pid, 'SIGTERM'); 34 | req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = undefined; 35 | console.info("Killed workspace " + workspaceName); 36 | }, 900000); //Workspaces have a lifetime of 15 minutes 37 | 38 | return timeout; 39 | }; 40 | 41 | /* 42 | * POST/GET create a new workspace 43 | */ 44 | exports.create = function(req, res) { 45 | if(req.body.name) { 46 | createWorkspace(req.body, req, res); 47 | } else { 48 | respondInvalidWorkspace(res); 49 | } 50 | } 51 | 52 | /* 53 | * GET workspaces listing. 54 | */ 55 | exports.list = function(req, res){ 56 | fs.readdir(__dirname + '/../workspaces/' + req.user, function(err, files) { 57 | if(err) { 58 | res.status(500); 59 | res.json({error: err}); 60 | } else { 61 | var workspaces = []; 62 | for(var i=0; i< files.length; i++) { 63 | // Skip hidden files 64 | if(files[i][0] === '.') continue; 65 | 66 | workspaces.push({name: files[i]}) 67 | } 68 | res.json({workspaces: workspaces}); 69 | } 70 | }); 71 | }; 72 | 73 | /** 74 | * DELETE destroys a workspace 75 | */ 76 | exports.destroy = function(req, res) { 77 | var potentiallyBadPathName = req.params.name.split(path.sep); 78 | var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1]; 79 | 80 | if(workspaceName === '..') { 81 | respondInvalidWorkspace(res); 82 | return; 83 | } 84 | 85 | rimraf(__dirname + "/../workspaces/" + req.user + "/" + workspaceName, function(err) { 86 | if(err) { 87 | res.status("500"); 88 | res.json({msg: "Something went wrong :("}); 89 | return; 90 | } 91 | res.json({msg: "Successfully deleted " + workspaceName}); 92 | }) 93 | }; 94 | 95 | /* 96 | * GET run a workspace 97 | */ 98 | exports.run = function(req, res) { 99 | var potentiallyBadPathName = req.params.name.split(path.sep); 100 | var workspaceName = potentiallyBadPathName[potentiallyBadPathName.length-1]; 101 | 102 | var isPortTaken = function(port, fn) { 103 | console.log('checking if port', port, 'is taken'); 104 | var net = require('net') 105 | var tester = net.createServer() 106 | .once('error', function (err) { 107 | if (err.code != 'EADDRINUSE') return fn(err) 108 | console.log('port', port, 'seems to be taken'); 109 | fn(null, true) 110 | }) 111 | .once('listening', function() { 112 | tester.once('close', function() { 113 | console.log('port', port, 'seems to be available'); 114 | fn(null, false) 115 | }) 116 | .close() 117 | }) 118 | .listen(port) 119 | }; 120 | 121 | var getNextAvailablePort = function(callback){ 122 | var nextFreeWorkspacePort = req.app.get('nextFreeWorkspacePort'); 123 | 124 | if(nextFreeWorkspacePort > 10000) { 125 | nextFreeWorkspacePort = 5000; 126 | } 127 | 128 | nextFreeWorkspacePort = nextFreeWorkspacePort + 1; 129 | console.log('setting nextFreeWorkspacePort to', nextFreeWorkspacePort); 130 | req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort); 131 | 132 | isPortTaken(nextFreeWorkspacePort, function(err, taken){ 133 | if(taken){ 134 | getNextAvailablePort(callback); 135 | } else { 136 | req.app.set('nextFreeWorkspacePort', nextFreeWorkspacePort); 137 | callback(nextFreeWorkspacePort); 138 | } 139 | }); 140 | }; 141 | 142 | if(workspaceName === '..') { 143 | respondInvalidWorkspace(res); 144 | return; 145 | } 146 | 147 | if(typeof req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] === 'undefined'){ 148 | getNextAvailablePort(function(nextFreePort){ 149 | console.log("Starting " + __dirname + '/../../c9/bin/cloud9.sh for workspace ' + workspaceName + " on port " + nextFreePort); 150 | 151 | var workspace = spawn(__dirname + '/../../c9/bin/cloud9.sh', ['-w', __dirname + '/../workspaces/' + req.user + '/' + workspaceName, '-l', '0.0.0.0', '-p', nextFreePort], {detached: true}); 152 | workspace.stderr.on('data', function (data) { 153 | console.log('stdERR: ' + data); 154 | }); 155 | 156 | req.app.get('runningWorkspaces')[req.user + '/' + workspaceName] = { 157 | killTimeout: createWorkspaceKillTimeout(req, workspace, workspaceName), 158 | process: workspace, 159 | name: workspaceName, 160 | url: req.app.settings.baseUrl + ":" + nextFreePort, 161 | user: req.user 162 | }; 163 | 164 | res.json({msg: "Attempted to start workspace", user: req.user, url: req.app.settings.baseUrl + ":" + nextFreePort}); 165 | }); 166 | } else { 167 | console.log("Found running workspace", req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url); 168 | res.json({msg: "Found running workspace", user: req.user, url: req.app.get('runningWorkspaces')[req.user + '/' + workspaceName].url}); 169 | } 170 | 171 | } 172 | 173 | /* 174 | * POST to keep the workspace alive 175 | */ 176 | exports.keepAlive = function(req, res) { 177 | var workspace = req.app.get('runningWorkspaces')[req.user + '/' + req.params.name]; 178 | clearTimeout(workspace.killTimeout); 179 | workspace.killTimeout = createWorkspaceKillTimeout(req, workspace.process, workspace.name); 180 | res.send(); 181 | } -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | // Project Configuration 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | watch: { 8 | options: { 9 | livereload: true, 10 | }, 11 | js: { 12 | files: ['gruntfile.js', 'config.js', 'server.js', 'controllers/**', 'routes/**', 'public/js/**'] 13 | }, 14 | html: { 15 | files: ['views/**', 'public/*.html', 'public/partials/**'] 16 | }, 17 | css: { 18 | files: ['public/css/**'] 19 | }, 20 | sass: { 21 | files: ['public/scss/**'], 22 | tasks: ['sass'], 23 | options: { 24 | livereload: false 25 | } 26 | } 27 | }, 28 | sass: { 29 | dist: { 30 | files: { 31 | 'public/css/style.css': 'public/scss/style.scss' 32 | } 33 | } 34 | }, 35 | nodemon: { 36 | dev: { 37 | script: 'server.js', 38 | options: { 39 | args: [], 40 | ignore: ['public/**'], 41 | ext: 'js', 42 | nodeArgs: ['--debug'], 43 | delayTime: 1, 44 | env: { 45 | PORT: 3105 46 | }, 47 | cwd: __dirname 48 | } 49 | } 50 | }, 51 | concurrent: { 52 | tasks: ['nodemon', 'watch'], 53 | options: { 54 | logConcurrentOutput: true 55 | } 56 | } 57 | }); 58 | 59 | //Load NPM tasks 60 | grunt.loadNpmTasks('grunt-contrib-watch'); 61 | grunt.loadNpmTasks('grunt-nodemon'); 62 | grunt.loadNpmTasks('grunt-concurrent'); 63 | grunt.loadNpmTasks('grunt-sass'); 64 | 65 | //Making grunt default to force in order not to break the project. 66 | grunt.option('force', true); 67 | 68 | //Default task(s). 69 | grunt.registerTask('default', ['sass', 'concurrent']); 70 | }; -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | echo "Installing Cloud9..." 4 | echo "-----------------------" 5 | 6 | git clone https://github.com/ajaxorg/cloud9.git c9 7 | cd c9 8 | npm install 9 | npm install -g bower 10 | bower install 11 | cd .. 12 | 13 | echo "Success." 14 | echo "" 15 | echo "Installing Cloud9Hub..." 16 | echo "-----------------------" 17 | 18 | git clone https://github.com/AVGP/cloud9hub.git cloud9hub 19 | cd cloud9hub 20 | npm install 21 | echo "Success." 22 | 23 | echo "Last steps" 24 | echo "-----------------------" 25 | echo "1. Create a Github app." 26 | echo "2. Copy cloud9hub/config.js.example to cloud9hub/config.js" 27 | echo "3. Edit your cloud9hub/config.js" 28 | echo "" 29 | echo "Have a lot of fun!" 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud9-hub", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "dependencies": { 9 | "connect-flash": "^0.1.1", 10 | "consolidate": "^0.10.0", 11 | "express": "3.2.6", 12 | "forever": "~0.10.11", 13 | "grunt": "~0.4.4", 14 | "grunt-cli": "~0.1.13", 15 | "grunt-concurrent": "^0.5.0", 16 | "grunt-contrib-watch": "~0.6.1", 17 | "grunt-env": "~0.4.1", 18 | "grunt-nodemon": "~0.2.1", 19 | "grunt-sass": "^0.11.0", 20 | "jade": "*", 21 | "lodash": "^2.4.1", 22 | "passport": "0.1.17", 23 | "passport-github": "0.1.5", 24 | "rimraf": "2.1.4", 25 | "stylus": "*", 26 | "swig": "^1.3.2", 27 | "view-helpers": "^0.1.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/fonts/bello_script.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/fonts/bello_script.eot -------------------------------------------------------------------------------- /public/fonts/bello_script.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/fonts/bello_script.ttf -------------------------------------------------------------------------------- /public/fonts/bello_script.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/fonts/bello_script.woff -------------------------------------------------------------------------------- /public/images/classy_fabric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/classy_fabric.png -------------------------------------------------------------------------------- /public/images/classy_fabric_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/classy_fabric_@2X.png -------------------------------------------------------------------------------- /public/images/dark_leather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/dark_leather.png -------------------------------------------------------------------------------- /public/images/dark_leather_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/dark_leather_@2X.png -------------------------------------------------------------------------------- /public/images/debut_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/debut_dark.png -------------------------------------------------------------------------------- /public/images/debut_dark_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/debut_dark_@2X.png -------------------------------------------------------------------------------- /public/images/denim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/denim.png -------------------------------------------------------------------------------- /public/images/denim_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/public/images/denim_@2X.png -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('c9hub', ['workspace', 'ngRoute']).config(function($routeProvider) { 2 | $routeProvider.when('/', {templateUrl: "/partials/login.html"}); 3 | $routeProvider.when('/dashboard', {controller: WorkspaceCtrl, templateUrl: "/partials/workspace.html"}); 4 | }); -------------------------------------------------------------------------------- /public/js/workspace/workspaceController.js: -------------------------------------------------------------------------------- 1 | var WorkspaceCtrl = function($scope, $http, $timeout, $sce) { 2 | $scope.workspaces = []; 3 | $scope.currentWorkspace = false; // {name: null, url: null, editing: false}; 4 | $scope.loadingWorkspace = false; 5 | $scope.iframeSrc = ''; 6 | 7 | $http.get('/workspace') 8 | .success(function(data) { console.log(data); $scope.workspaces = data.workspaces; }) 9 | .error(function(err) { alert(err.msg) }); 10 | 11 | var _sendKeepAlive = function() { 12 | $http.post('/workspace/' + $scope.currentWorkspace.name + '/keepalive').success(function() { 13 | $timeout(_sendKeepAlive, 300000); 14 | }); 15 | }; 16 | 17 | $scope.startEditing = function() { 18 | $scope.currentWorkspace.editing = true; 19 | $scope.iframeSrc = $sce.trustAsResourceUrl($scope.currentWorkspace.url); 20 | }; 21 | 22 | var createWorkspace = function() { 23 | var wsName = $scope.currentWorkspace.name; 24 | $scope.loadingWorkspace = true; 25 | $http.post('/workspace/', {name: wsName}) 26 | .success(function(data) { 27 | // alert(data.msg); 28 | $scope.loadingWorkspace = false; 29 | $scope.workspaces.push({name: wsName}); 30 | $scope.currentWorkspace = false; 31 | }) 32 | .error(function(err) { 33 | alert("Error: " + err.msg); 34 | }); 35 | } 36 | 37 | $scope.saveWorkspace = function(){ 38 | $scope.iframeSrc = ''; 39 | if(!$scope.currentWorkspace.url){ 40 | createWorkspace(); 41 | } 42 | }; 43 | 44 | $scope.blankWorkspace = function() { 45 | $scope.iframeSrc = ''; 46 | $scope.currentWorkspace = { 47 | url: '', 48 | name: '', 49 | editing: false 50 | } 51 | }; 52 | 53 | $scope.deleteWorkspace = function(name) { 54 | $scope.iframeSrc = ''; 55 | if(!window.confirm("Do you really want to delete this workspace? All your files in that workspace will be gone forever!")) return; 56 | $http.delete('/workspace/' + name) 57 | .success(function(data){ 58 | console.log(data); 59 | alert(data.msg); 60 | 61 | for(var i=0;i<$scope.workspaces.length;i++) { 62 | if($scope.workspaces[i].name === name) { 63 | $scope.workspaces.splice(i,1); 64 | break; 65 | } 66 | } 67 | 68 | $scope.currentWorkspace = false; 69 | }) 70 | .error(function(err) { 71 | console.log("ERR:", err); 72 | $scope.currentWorkspace = false; 73 | }); 74 | } 75 | 76 | $scope.runWorkspace = function(name) { 77 | $scope.iframeSrc = ''; 78 | $scope.loadingWorkspace = true; 79 | $http.get('/workspace/' + name).success(function(data) { 80 | console.log('data', data); 81 | $scope.currentWorkspace = {}; 82 | $scope.currentWorkspace.name = name; 83 | $scope.currentWorkspace.url = data.url; 84 | $scope.currentWorkspace.user = data.user; 85 | $scope.currentWorkspace.editing = false; 86 | _sendKeepAlive(); 87 | $scope.loadingWorkspace = false; 88 | }).error(function(err) { 89 | $scope.loadingWorkspace = false; 90 | alert("Error: " + err); 91 | console.log(err); 92 | }); 93 | } 94 | } -------------------------------------------------------------------------------- /public/js/workspace/workspaceModule.js: -------------------------------------------------------------------------------- 1 | var workspace = angular.module('workspace', []); 2 | 3 | workspace.controller('WorkspaceCtrl', ['$scope', '$http', '$timeout', WorkspaceCtrl]); -------------------------------------------------------------------------------- /public/partials/login.html: -------------------------------------------------------------------------------- 1 |

Welcome to Cloud9Hub!

2 |

3 |   Sign in with Github 4 |

-------------------------------------------------------------------------------- /public/partials/workspace.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 |
16 |
17 | Welcome to Cloud9Hub.
18 | This part of the screen is the editor panel.
19 | When you run a workspace, it will be started here. 20 |
21 |
22 | On the left you see the workspace panel.
23 | You can create, delete or run workspaces there.
24 |
25 |
26 | To create a workspace, click the "Create workspace" button.
27 | If you want to run a workspace, click on its name. The workspace will start automatically.
28 | To delete a workspace, click the little "☠" to the right of a workspace item. 29 |
30 |
31 | 32 |
33 |
34 |

New Workspace

35 |

{{currentWorkspace.user}} / {{currentWorkspace.name}}

36 |
37 |
38 | Start Editing 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 | 49 |
50 | 51 |
52 | 53 |
54 | 55 |
Loading...
56 |
57 |
-------------------------------------------------------------------------------- /public/scss/style.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Bello Script'; 3 | src: url('/fonts/bello_script.eot'); 4 | 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'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | /* 10 | * Element definitions 11 | */ 12 | body { 13 | padding: 0; 14 | // font-family: source-sans-pro, Helvetica, Arial, sans-serif; 15 | height: 100%; 16 | background-color: #ECF0F1; 17 | } 18 | 19 | .container{ 20 | width: 100%; 21 | } 22 | 23 | .dashboard-row{ 24 | height: 100%; 25 | } 26 | 27 | aside{ 28 | padding: 15px 15px 15px 15px; 29 | background: #34495E; 30 | color: #fff; 31 | height: 100%; 32 | 33 | h1{ 34 | font-weight: 300; 35 | text-align:center; 36 | margin-top: 0; 37 | } 38 | } 39 | 40 | .bello{ 41 | font-family: 'Bello Script'; 42 | } 43 | 44 | a { 45 | color: #288edf; 46 | } 47 | 48 | /*button { 49 | background-color: #083; 50 | border: outset 1px #0c8; 51 | color: #fff; 52 | border-radius: 6px; 53 | padding: 2px; 54 | }*/ 55 | 56 | /* 57 | * Class definitions 58 | */ 59 | .active { 60 | font-weight: bold; 61 | } 62 | .block { 63 | margin-bottom: 1.2em; 64 | } 65 | .centered { 66 | text-align: center; 67 | } 68 | 69 | .delete { 70 | color: #eee; 71 | display: inline-block; 72 | float: right; 73 | cursor: pointer; 74 | 75 | i{ 76 | position: relative; 77 | left: 2px; 78 | } 79 | 80 | &:hover i{ 81 | opacity: 1; 82 | } 83 | } 84 | 85 | .frameWrapper { 86 | height: 100%; 87 | } 88 | .hint { 89 | font-style: oblique; 90 | color: #27AE60; 91 | } 92 | .hScope { 93 | height: 100%; 94 | } 95 | 96 | .menu-list { 97 | margin: 0; 98 | padding: 0; 99 | list-style-type: none; 100 | } 101 | 102 | .menu-list li { 103 | font-weight: 300; 104 | font-size: 1em; 105 | padding: 1.5rem 1.5rem; 106 | cursor: pointer; 107 | background: #95A5A6; 108 | border-top: 2px solid #7F8C8D; 109 | 110 | transition: background-color 0.25s ease; 111 | -moz-transition: background-color 0.25s ease; 112 | -webkit-transition: background-color 0.25s ease; 113 | 114 | span{ 115 | font-size: 1em; 116 | } 117 | 118 | i{ 119 | opacity: 0.2; 120 | } 121 | 122 | &:first-child{ 123 | border-top-left-radius: 5px; 124 | border-top-right-radius: 5px; 125 | border-top-width: 0px; 126 | } 127 | 128 | &:last-child{ 129 | border-bottom-left-radius: 5px; 130 | border-bottom-right-radius: 5px; 131 | border-bottom: 5px solid #7F8C8D; 132 | } 133 | 134 | &:hover{ 135 | background: #7F8C8D; 136 | } 137 | 138 | &:hover i{ 139 | opacity: 0.6; 140 | } 141 | } 142 | 143 | .workspace { 144 | border: none; 145 | } 146 | 147 | .workspace-wrapper { 148 | height: 100%; 149 | padding: 0 1em; 150 | background: #FFF; 151 | } 152 | 153 | 154 | .workspace-bar{ 155 | background: #2C3E50; 156 | padding: 1em; 157 | margin: 0em -1em 2em -1em; 158 | } 159 | 160 | .workspace-form-footer{ 161 | padding-top: 1rem; 162 | margin-top: 2rem; 163 | text-align: right; 164 | } 165 | 166 | header{ 167 | position: relative; 168 | margin-top: -1em; 169 | margin-left: -1em; 170 | margin-right: -1em; 171 | padding: 2em 1em; 172 | background-color: #34495E; 173 | 174 | h1 { 175 | font-weight: 100; 176 | margin: 10px 0 0px; 177 | font-size: 2em; 178 | position: relative; 179 | display: inline-block; 180 | padding-right: 10px; 181 | color: #fff; 182 | 183 | span{ 184 | color: #aaa; 185 | } 186 | } 187 | } 188 | 189 | .loading-overlay{ 190 | width: 100%; 191 | height: 100%; 192 | position: absolute; 193 | top: 0; 194 | left: 0; 195 | background: rgb(44,62,80); 196 | color: #fff; 197 | font-size: 2em; 198 | font-weight: 300; 199 | text-align: center; 200 | 201 | span{ 202 | position: absolute; 203 | top: 50%; 204 | left: 50%; 205 | margin-left: -73px; 206 | margin-top: -18px; 207 | } 208 | } 209 | 210 | .no-workspace{ 211 | color: #BDC3C7; 212 | padding: 2rem; 213 | border-radius: 10px; 214 | width: 41em; 215 | position: absolute; 216 | left: 50%; 217 | top: 50%; 218 | margin-top: -173px; 219 | margin-left: -370px; 220 | 221 | h1{ 222 | font-weight: normal; 223 | margin: 0 0 1rem 0; 224 | } 225 | 226 | .block{ 227 | font-size: 1em; 228 | } 229 | } 230 | 231 | .login-row{ 232 | position: absolute; 233 | left: 50%; 234 | top: 50%; 235 | margin-top: -125px; 236 | margin-left: -291.5px; 237 | } 238 | 239 | .login-col{ 240 | width: 583px; 241 | height: 171px; 242 | } 243 | 244 | .login-title{ 245 | font-size: 6rem; 246 | font-weight: normal; 247 | } 248 | 249 | .login-button{ 250 | //font-size: 1.25rem; 251 | //padding: 1rem; 252 | } 253 | 254 | .workspace-iframe{ 255 | height: 100%; 256 | padding: 0; 257 | margin: 0 -1em; 258 | } 259 | 260 | .panel{ 261 | padding: 2rem; 262 | } -------------------------------------------------------------------------------- /public/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Cloud9Hub

8 |
9 | Welcome to Cloud9Hub.
10 | This part of the screen is the editor panel.
11 | When you run a workspace, it will be started here. 12 |
13 |
14 | On the left you see the workspace panel.
15 | You can create, delete or run workspaces there.
16 |
17 |
18 | To create a workspace, click the "Create workspace" button.
19 | If you want to run a workspace, click on its name. The workspace will start automatically.
20 | To delete a workspace, click the little "☠" to the right of a workspace item. 21 |
22 | 23 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var index = require('../controllers/index'); 4 | var authorization = require('./middlewares/authorization'); 5 | 6 | module.exports = function(app) { 7 | app.get('/', authorization.redirectToLogin, index.index); 8 | } -------------------------------------------------------------------------------- /routes/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var login = require('../controllers/login'); 4 | var authorization = require('./middlewares/authorization'); 5 | 6 | module.exports = function(app) { 7 | app.get('/login', login.login); 8 | } -------------------------------------------------------------------------------- /routes/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Generic require login routing middleware 5 | */ 6 | exports.requiresLogin = function(req, res, next) { 7 | if (!req.isAuthenticated()) { 8 | return res.send(401, 'User is not authorized'); 9 | } 10 | next(); 11 | }; 12 | 13 | exports.redirectToLogin = function(req, res, next) { 14 | if (!req.isAuthenticated()) { 15 | return res.redirect('/login'); 16 | } 17 | next(); 18 | } 19 | -------------------------------------------------------------------------------- /routes/workspace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var workspaces = require('../controllers/workspaces'); 4 | var authorization = require('./middlewares/authorization'); 5 | 6 | module.exports = function(app) { 7 | app.get('/workspace', authorization.requiresLogin, workspaces.list); 8 | app.post('/workspace', authorization.requiresLogin, workspaces.create); 9 | app.get('/workspace/:name', authorization.requiresLogin, workspaces.run); 10 | app.post('/workspace/:name/keepalive', authorization.requiresLogin, workspaces.keepAlive); 11 | app.delete('/workspace/:name', authorization.requiresLogin, workspaces.destroy); 12 | } 13 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var express = require('express') 6 | , routes = require('./routes') 7 | , workspace = require('./routes/workspace') 8 | , index = require('./routes/index') 9 | , fs = require('fs') 10 | , path = require('path') 11 | , http = require('http') 12 | , https = require('https') 13 | , path = require('path') 14 | , passport = require('passport') 15 | , flash = require('connect-flash') 16 | , helpers = require('view-helpers') 17 | , consolidate = require('consolidate') 18 | , GithubStrategy = require('passport-github').Strategy; 19 | try { 20 | var config = require(__dirname + '/config.js'); 21 | } catch(e) { 22 | console.error("No config.js found! Copy and edit config.example.js to config.js!"); 23 | process.exit(1); 24 | } 25 | 26 | // Load configurations 27 | // Set the node enviornment variable if not set before 28 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 29 | 30 | var app = express(); 31 | 32 | app.set('showStackError', true); 33 | // cache=memory or swig dies in NODE_ENV=production 34 | app.locals.cache = 'memory'; 35 | // Prettify HTML 36 | app.locals.pretty = true; 37 | 38 | app.set('nextFreeWorkspacePort', 5000); 39 | 40 | app.engine('html', consolidate.swig); 41 | 42 | // Start the app by listening on 43 | var port = process.env.PORT || 3105; 44 | 45 | // all environments 46 | app.set('port', port); 47 | app.set('view engine', 'html'); 48 | app.set('views', __dirname + '/views'); 49 | app.set('baseUrl', config.BASE_URL); 50 | app.set('runningWorkspaces', {}); 51 | 52 | //Auth 53 | passport.use(new GithubStrategy({ 54 | clientID: config.GITHUB_CLIENT_ID, 55 | clientSecret: config.GITHUB_CLIENT_SECRET, 56 | callbackURL: app.get('baseUrl') + ':' + app.get('port') + '/auth/github/callback' 57 | }, 58 | function(accessToken, refreshToken, profile, done) { 59 | var username = path.basename(profile.username.toLowerCase()); 60 | if(!fs.existsSync(__dirname + '/workspaces/' + path.basename(username))) { 61 | if(config.PERMITTED_USERS !== false && config.PERMITTED_USERS.indexOf(username)) return done('Sorry, not allowed :(', null); 62 | 63 | //Okay, that is slightly unintuitive: fs.mkdirSync returns "undefined", when successful.. 64 | if(fs.mkdirSync(__dirname + '/workspaces/' + path.basename(username), '0700') !== undefined) { 65 | return done("Cannot create user", null); 66 | } else { 67 | return done(null, username); 68 | } 69 | } 70 | return done(null, username); 71 | } 72 | )); 73 | 74 | //Middlewares 75 | app.use(express.favicon()); 76 | // Only use logger for development environment 77 | if (process.env.NODE_ENV === 'development') { 78 | app.use(express.logger('dev')); 79 | } 80 | app.use(express.bodyParser()); 81 | app.use(express.methodOverride()); 82 | app.use(express.cookieParser('cloud9hub secret')); 83 | app.use(express.session()); 84 | // Initialize Passport! Also use passport.session() middleware, to support 85 | // persistent login sessions (recommended). 86 | app.use(passport.initialize()); 87 | app.use(passport.session()); 88 | app.use(flash()); 89 | // Dynamic helpers 90 | app.use(helpers('Cloud9Hub')); 91 | app.use(app.router); 92 | app.use(express.static(path.join(__dirname, 'public'))); 93 | 94 | // development only 95 | if ('development' == app.get('env')) { 96 | app.use(express.errorHandler()); 97 | } 98 | 99 | //Auth requests 100 | app.get('/auth/github', passport.authenticate('github'), function(req, res) {}); 101 | app.get('/auth/github/callback', 102 | passport.authenticate('github', { failureRedirect: '/'}), 103 | function(req, res) { 104 | res.redirect('/#/dashboard'); 105 | }); 106 | 107 | app.get('/logout', function(req, res){ 108 | req.logout(); 109 | res.json('OK'); 110 | }); 111 | 112 | // Bootstrap routes 113 | var routes_path = __dirname + '/routes'; 114 | var walk = function(path) { 115 | fs.readdirSync(path).forEach(function(file) { 116 | var newPath = path + '/' + file; 117 | var stat = fs.statSync(newPath); 118 | if (stat.isFile()) { 119 | if (/(.*)\.(js$|coffee$)/.test(file)) { 120 | require(newPath)(app, passport); 121 | } 122 | // We skip the app/routes/middlewares directory as it is meant to be 123 | // used and shared by routes as further middlewares and is not a 124 | // route by itself 125 | } else if (stat.isDirectory() && file !== 'middlewares') { 126 | walk(newPath); 127 | } 128 | }); 129 | }; 130 | walk(routes_path); 131 | 132 | var server; 133 | 134 | if (config.SSL && config.SSL.key && config.SSL.cert) { 135 | var sslOpts = { 136 | key: fs.readFileSync(config.SSL.key), 137 | cert: fs.readFileSync(config.SSL.cert) 138 | }; 139 | 140 | server = https.createServer(sslOpts, app); 141 | } else { 142 | server = http.createServer(app); 143 | } 144 | 145 | server.listen(app.get('port'), function(){ 146 | console.log('Express server listening on port ' + app.get('port')); 147 | }); 148 | 149 | //Helpers 150 | 151 | passport.serializeUser(function(user, done) { 152 | done(null, user); 153 | }); 154 | 155 | passport.deserializeUser(function(obj, done) { 156 | done(null, obj); 157 | }); 158 | -------------------------------------------------------------------------------- /views/includes/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if (process.env.NODE_ENV == 'development') %} 19 | 20 | 21 | {% endif %} -------------------------------------------------------------------------------- /views/includes/header.html: -------------------------------------------------------------------------------- 1 | 2 | Cloud9Hub 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/default.html' %} 2 | {% block content %} 3 |
4 | {% endblock %} -------------------------------------------------------------------------------- /views/layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include '../includes/header.html' %} 4 | 5 | {% block content %}{% endblock %} 6 | {% include '../includes/footer.html' %} 7 | 8 | -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/default.html' %} 2 | {% block content %} 3 |
4 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /workspaces/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AVGP/cloud9hub/d67b514b5cb18a3e4504c593cbe1bdd76fc29103/workspaces/.gitkeep --------------------------------------------------------------------------------