diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..352c1d41 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "public/vendor" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index da23d0d4..410c585a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +lib/credentials.js +lib/credentials.json +public/vendor +dist/ + # Logs logs *.log diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..377f417d --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,156 @@ +module.exports = function(grunt) { + require('load-grunt-tasks')(grunt); + + // Configuration + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + bump: { + options: { + files: ['package.json'], + pushTo: 'origin', + commitFiles: ['package.json'] + } + }, + + watch: { + js: { + files: ['public/js/*.js', 'public/js/**/*.js'], + tasks: ['newer:copy:js'] + }, + styles: { + files: ['public/css/{,**/}*.less'], + tasks: ['newer:less:development'] + }, + livereload: { + files: [ + 'dist/{,**/}*.{css,js,png,jpg,jpeg,gif,webp,svg,html}' + ], + options: { + livereload: true + } + } + }, + + less: { + development: { + expand: true, + cwd: 'public/css', + dest: 'dist/css', + src: '**/*', + ext: '.css' + }, + production: { + expand: true, + cwd: 'public/css', + dest: 'dist/css', + src: '**/*', + ext: '.css', + options: { + cleancss: true + } + } + }, + + clean: { + clean: { + files: [{ + dot: true, + src: [ + 'dist' + ] + }] + } + }, + + // Copies remaining files to places other tasks can use + copy: { + js: { + expand: true, + cwd: 'public/js', + dest: 'dist/js/', + src: '{,**/}*.js' + }, + img: { + expand: true, + cwd: 'public/img', + dest: 'dist/img/', + src: '{,**/}*.{png,jpg,jpeg,gif}' + }, + vendor: { + files: [{ + expand: true, + cwd: 'public/vendor', + dest: 'dist/vendor', + src: ['**/*.js', '**/*.css', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.woff', '**/*.ttf', '**/*.svg', '**/*.eot'] + }] + }, + fonts: { + expand: true, + cwd: 'public/fonts', + dest: 'dist/fonts/', + src: '{,**/}*.{woff,ttf,svg,eot}' + } + }, + + // Run some tasks in parallel to speed up the build process + concurrent: { + options: { + limit: 6 + }, + server: [ + 'copy:js', + 'copy:vendor', + 'copy:img', + 'copy:fonts' + ], + watch: { + tasks: [ + 'nodemon:dev', + 'watch' + ], + options: { + logConcurrentOutput: true + } + } + }, + + uglify: { + dist: { + files: [{ + expand: true, + cwd: 'public/js', + src: ['**/*.js', '*.js'], + dest: 'dist/js' + }] + } + }, + + nodemon: { + dev: { + script: 'lib/app.js', + logConcurrentOutput: true, + options: { + cwd: __dirname, + watch: ['lib/'] + } + } + } + }); + + grunt.registerTask('serve', [ + 'clean:clean', + 'concurrent:server', + 'less:development', + 'concurrent:watch' + ]); + + grunt.registerTask('build', [ + 'clean:clean', + 'concurrent:server', + 'less:production' + ]); + + grunt.registerTask('default', [ + 'build' + ]); +}; diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..3811e452 --- /dev/null +++ b/bower.json @@ -0,0 +1,16 @@ +{ + "name": "semaphore", + "version": "0.0.0", + "dependencies": { + "angular": "latest", + "requirejs": "latest", + "fontawesome": "latest", + "jquery": "latest", + "bootstrap": "latest", + "moment": "latest", + "d3": "latest", + "async": "latest", + "angular-couch-potato": "~0.1.1", + "angular-ui-router": "~0.2.10" + } +} diff --git a/lib/app.js b/lib/app.js new file mode 100644 index 00000000..9db8981b --- /dev/null +++ b/lib/app.js @@ -0,0 +1,126 @@ +var config = require('./config'); + +var newrelic = { + getBrowserTimingHeader: function () {} +}; +if (config.production && config.credentials.use_analytics) { + newrelic = require('newrelic'); +} + +var express = require('express') + , routes = require('./routes') + , http = require('http') + , path = require('path') + , mongoose = require('mongoose') + , util = require('./util') + , session = require('express-session') + , RedisStore = require('connect-redis')(session) + , passport = require('passport') + , auth = require('./auth') + , bugsnag = require('bugsnag') + , socketPassport = require('passport.socketio') + , bodyParser = require('body-parser') + +var app = exports.app = express(); + +if (config.production) { + require('newrelic'); +} + +var releaseStage = config.production ? "production" : "development"; + +bugsnag.register("c0c7568710bb46d4bf14b3dad719dbbe", { + notifyReleaseStages: ["production"], + releaseStage: releaseStage +}); + +mongoose.connect(config.credentials.db, config.credentials.db_options); + +var sessionStore = new RedisStore({ + host: config.credentials.redis_host, + port: config.credentials.redis_port, + ttl: 604800000, + pass: config.credentials.redis_key +}); + +var db = mongoose.connection +db.on('error', console.error.bind(console, 'Mongodb Connection Error:')); +db.once('open', function callback () { + if (!config.is_testing) console.log("Mongodb connection established") +}); + +// all environments +app.enable('trust proxy'); +app.set('port', process.env.PORT || 3000); // Port +app.set('views', __dirname + '/views'); +app.set('view engine', 'jade'); // Templating engine +app.set('app version', config.version); // App version +app.set('x-powered-by', false); + +app.set('view cache', config.production); + +app.locals.newrelic = newrelic; +config.configure(app); + +app.use(function(req, res, next) { + res.set('x-frame-options', 'SAMEORIGIN'); + res.set('x-xss-protection', '1; mode=block'); + next(); +}); + +app.use(require('serve-static')(path.join(__dirname, '..', 'dist'))); +app.use(require('morgan')(config.production ? 'combined' : 'dev')); + +app.use(bugsnag.requestHandler); +app.use(bodyParser.urlencoded({ + extended: true +})); +app.use(bodyParser.json()); +app.use(require('cookie-parser')()); +app.use(session({ + secret: "#semaphore", + name: 'semaphore', + store: sessionStore, + proxy: true, + saveUninitialized: false, + resave: false, + cookie: { + secure: config.credentials.is_ssl, + maxAge: 604800000 + } +})); + +app.use(passport.initialize()); +app.use(passport.session()); + +// Custom middleware +app.use(function(req, res, next) { + res.locals.user = req.user; + res.locals.loggedIn = res.locals.user != null; + + next(); +}); + +// routes +routes.router(app); + +app.use(bugsnag.errorHandler); + +var server = http.createServer(app) +server.listen(app.get('port'), function(){ + console.log('Semaphore listening on port ' + app.get('port')); +}); +exports.io = io = require('socket.io').listen(server) + +config.init(); + +io.use(socketPassport.authorize({ + cookieParser: require('cookie-parser'), + secret: "#semaphore", + key: 'semaphore', + store: sessionStore, + passport: passport, + fail: function(data, message, error, accept) { + accept(false); + } +})) diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 00000000..70cf225d --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,13 @@ +var passport = require('passport') + , models = require('./models') + , bugsnag = require('bugsnag') + +passport.serializeUser(function(user, done) { + done(null, user._id); +}); + +passport.deserializeUser(function(id, done) { + models.User.findOne({ + _id: id + }, done); +}) \ No newline at end of file diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 00000000..0f863173 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,75 @@ +var fs = require('fs') + , mailer = require('nodemailer') + +try { + var credentials = require('./credentials.json') +} catch (e) { + console.log("\nNo credentials.json File!\n") + process.exit(1); +} + +exports.credentials = credentials; + +exports.version = require('../package.json').version; +exports.hash = 'dirty'; +exports.production = process.env.NODE_ENV == "production"; +exports.port = process.env.PORT || credentials.port; +exports.path = __dirname; + +if (process.platform.match(/^win/) == null) { + try { + var spawn_process = require('child_process').spawn + var readHash = spawn_process('git', ['rev-parse', '--short', 'HEAD']); + readHash.stdout.on('data', function (data) { + exports.hash = data.toString().trim(); + require('./app').app.locals.versionHash = exports.hash; + }) + } catch (e) { + console.log("\n~= Unable to obtain git commit hash =~\n") + } +} + +exports.configure = function (app) { + app.locals.pretty = exports.production // Pretty HTML outside production mode + app.locals.version = exports.version; + app.locals.versionHash = exports.hash; + app.locals.production = exports.production; + app.locals.use_analytics = credentials.use_analytics; +} + +// Create SMTP transport method +exports.transport_enabled = credentials.smtp.user.length > 0; +exports.transport = null; + +if (exports.transport_enabled) { + var smtp = require('nodemailer-smtp-transport'); + + exports.transport = mailer.createTransport(smtp({ + service: "Mandrill", + auth: credentials.smtp, + port: 2525 // should bypass any port restrictions + })); +} + +exports.init = function () { + var models = require('./models'); + + models.User.findOne({ + email: 'admin@semaphore.local' + }).exec(function (err, admin) { + if (!admin) { + console.log("Creating Admin user admin@semaphore.local!"); + + admin = new models.User({ + email: 'admin@semaphore.local', + username: 'semaphore', + name: 'Administrator' + }); + models.User.hashPassword('CastawayLabs', function (hash) { + admin.password = hash; + + admin.save(); + }); + } + }) +} \ No newline at end of file diff --git a/lib/credentials.example.json b/lib/credentials.example.json new file mode 100644 index 00000000..f5b846cf --- /dev/null +++ b/lib/credentials.example.json @@ -0,0 +1,22 @@ +{ + "redis_port": 6379, + "redis_host": "127.0.0.1", + "redis_key": "", + "use_analytics": false, + "is_ssl": false, + "newrelic_key": "", + "bugsnag_key": "", + "smtp": { + "user": "", + "pass": "" + }, + "db": "mongodb://127.0.0.1/semaphore", + "db_options": { + "auto_reconnect": true, + "native_parser": true, + "server": { + "auto_reconnect": true + } + }, + "port": 3000 +} \ No newline at end of file diff --git a/lib/models/User.js b/lib/models/User.js new file mode 100644 index 00000000..23b6915f --- /dev/null +++ b/lib/models/User.js @@ -0,0 +1,34 @@ +var bcrypt = require('bcrypt') + +var mongoose = require('mongoose') +var ObjectId = mongoose.Schema.ObjectId; + +var schema = mongoose.Schema({ + created: { + type: Date, + default: Date.now + }, + username: String, + name: String, + email: String, + password: String +}); + +schema.index({ + email: 1 +}); + +schema.statics.hashPassword = function(password, cb) { + bcrypt.hash(password, 10, function(err, hash) { + cb(hash); + }); +} + +schema.methods.comparePassword = function (password, cb) { + bcrypt.compare(password, this.password, function(err, res) { + // res is boolean + cb(res); + }) +} + +module.exports = mongoose.model('User', schema); \ No newline at end of file diff --git a/lib/models/index.js b/lib/models/index.js new file mode 100644 index 00000000..92c108a6 --- /dev/null +++ b/lib/models/index.js @@ -0,0 +1,7 @@ +var manifest = [ + 'User' +]; + +manifest.forEach(function (model) { + module.exports[model] = require('./'+model); +}); \ No newline at end of file diff --git a/lib/routes/apps.js b/lib/routes/apps.js new file mode 100644 index 00000000..a45ea325 --- /dev/null +++ b/lib/routes/apps.js @@ -0,0 +1,9 @@ + + +exports.httpRouter = function () { + +} + +exports.router = function () { + +} \ No newline at end of file diff --git a/lib/routes/auth.js b/lib/routes/auth.js new file mode 100644 index 00000000..727d8cae --- /dev/null +++ b/lib/routes/auth.js @@ -0,0 +1,171 @@ +var passport = require('passport') + , models = require('../models') + , validator = require('validator') + , util = require('../util') + , config = require('../config') + , async = require('async') + , express = require('express') + , mongoose = require('mongoose') + +exports.unauthorized = function (app, template) { + // Unrestricted -- non-authorized people can access! + template([ + 'login' + ], { + prefix: 'auth' + }); + + var auth = express.Router(); + + auth.post('/password', doLogin) + .get('/loggedin', isLoggedIn) + .get('/logout', doLogout) + + app.use('/auth', auth); +} + +exports.router = function (app) { + // Restricted -- only authorized people can access! + app.post('/auth/register', doRegister) +} + +function isLoggedIn (req, res) { + res.send({ + hasSession: req.user != null, + isLoggedIn: res.locals.loggedIn + }); +} + +function doLogin (req, res) { + var auth = req.body.auth; + var isValid = true; + + if (!validator.isLength(auth, 4)) { + isValid = false; + } + + // validate password + if (!validator.isLength(req.body.password, 6)) { + isValid = false; + } + + if (!isValid) { + return authCallback(false, null, req, res); + } + + var query = { + email: auth.toLowerCase() + }; + + models.User.findOne(query, function(err, user) { + if (err) { + throw err; + } + + user.comparePassword(req.body.password, function (matches) { + if (!matches) { + isValid = false; + } + + authCallback(isValid, user, req, res); + }); + }) +} + +function authCallback (isValid, user, req, res) { + if (!isValid) { + res.send(400, { + message: "Nope. Incorrect Credentials!" + }); + + return; + } + + req.login(user, function(err) { + if (err) throw err; + + res.send(201) + }) +} + +function doRegister (req, res) { + var errs = { + name: false, + email: false, + password: false, + username: false + }; + + var userObject = req.body.user; + if (!(userObject && typeof userObject === 'object')) { + return res.send(400, { + message: 'Invalid Request' + }); + } + + var email = userObject.email; + if (email) { + email = email.toLowerCase(); + } + var password = userObject.password; + var username = userObject.username; + var name = userObject.name; + + errs.email = !validator.isEmail(email); + errs.username = !validator.isLength(username, 3, 15); + errs.name = !validator.isLength(name, 4, 50); + + if (!(username && username.match(/^[a-zA-Z0-9_-]{3,15}$/) && validator.isAscii(username))) { + // Errornous username + errs.username = true; + } + + // validate password + errs.password = !validator.isLength(password, 8, 100); + + if (!(errs.username == false && errs.password == false && errs.name == false && errs.email == false)) { + res.send(400, { + fields: errs, + message: '' + }); + + return; + } + + // Register + var user = new models.User({ + email: email, + username: username, + name: name + }); + + models.User.hashPassword(password, function (hash) { + user.password = hash; + + user.save(); + + // log in now + req.login(user, function(err) { + if (err) throw err; + + res.send({ + message: "Registration Successful", + user_id: user._id + }); + }); + }); +} + +function doLogout (req, res) { + req.logout(); + req.session.destroy(); + + res.format({ + json: function() { + res.send(201) + }, + html: function() { + res.redirect('/') + } + }) +} \ No newline at end of file diff --git a/lib/routes/index.js b/lib/routes/index.js new file mode 100644 index 00000000..5f10879f --- /dev/null +++ b/lib/routes/index.js @@ -0,0 +1,43 @@ +var util = require('../util') + , auth = require('./auth') + , apps = require('./apps') + +exports.router = function(app) { + var templates = require('../templates')(app); + templates.route([ + auth, + // apps + ]); + + templates.add('homepage') + templates.setup(); + + app.get('/', layout); + app.all('*', util.authorized); + + // Handle HTTP reqs + apps.httpRouter(app); + + // only json beyond this point + app.get('*', function(req, res, next) { + res.format({ + json: function() { + next() + }, + html: function() { + layout(req, res); + } + }); + }); + + auth.router(app); + apps.router(app); +} + +function layout (req, res) { + if (res.locals.loggedIn) { + res.render('layout') + } else { + res.render('auth'); + } +} \ No newline at end of file diff --git a/lib/templates.js b/lib/templates.js new file mode 100644 index 00000000..9a24a51d --- /dev/null +++ b/lib/templates.js @@ -0,0 +1,67 @@ +// By Matej Kramny +// Please leave this comment here. + +module.exports = function(app) { + var self = this; + self.routes = []; + self.app = app + + self.route = function (controller) { + if (!(controller instanceof Array)) { + controller = [controller]; + } + + for (c in controller) { + controller[c].unauthorized(self.app, self.add.bind(self)); + } + } + + self.makeRoute = function(route, view) { + return { + route: route, + view: view + } + } + + self.add = function (routes, opts) { + var args = arguments; + + var prefix = opts ? opts.prefix : null; + if (!prefix) prefix = ''; + else prefix += '/'; + + if (typeof routes === 'string') { + self.routes.push(self.makeRoute(prefix+routes, prefix+routes)); + return; + } + if (Object.prototype.toString.call(routes) == '[object Object]') { + self.routes.push(routes); + return; + } + + for (var i = 0; i < routes.length; i++) { + var r; + if (typeof routes[i] == 'string') { + r = self.makeRoute(prefix+routes[i], prefix+routes[i]); + } else if (routes[i] instanceof Array) { + r = self.makeRoute(prefix+routes[i][0], routes[i][1]); + } else { + r = routes[i] + } + + self.routes.push(r); + } + } + + self.setup = function () { + for (var i = 0; i < routes.length; i++) { + app.get('/view/'+routes[i].route, self.getView.bind(routes[i])); + } + } + + self.getView = function (req, res) { + res.render(this.view); + } + + return self; +} \ No newline at end of file diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 00000000..2966cab1 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,16 @@ +exports.authorized = function (req, res, next) { + if (res.locals.loggedIn) { + next() + } else { + res.format({ + html: function() { + res.render('auth'); + }, + json: function() { + res.send(403, { + message: "Unauthorized" + }) + } + }) + } +} diff --git a/lib/views/auth.jade b/lib/views/auth.jade new file mode 100644 index 00000000..f28dd167 --- /dev/null +++ b/lib/views/auth.jade @@ -0,0 +1,32 @@ +doctype +html + head + meta(http-equiv="Content-Type", content="text/html; charset=utf-8;") + meta(name="viewport" content="width=device-width, initial-scale=1.0") + block meta + title(ng-bind-template="{{ pageTitle }} - Semaphore") Semaphore + link(href="/favicon.ico" type="image/x-icon" rel="icon") + link(href="/favicon.ico" type="image/x-icon" rel="shortcut icon") + + //- all css goes here + block css + + link(rel="stylesheet" href="/css/semaphore.css") + + body + .container-fluid(style="margin-top: 100px;") + .row + .col-sm-6.col-sm-offset-3 + block content + ui-view(autoscroll="false") + p.lead.text-center + i.fa.fa-spin.fa-cog + | Loading... + + block js + script(src="/vendor/requirejs/require.js" data-main="/js/semaphore_auth.js") + if use_analytics + != newrelic.getBrowserTimingHeader() + + //- page-specific js + block addonjs \ No newline at end of file diff --git a/lib/views/auth/login.jade b/lib/views/auth/login.jade new file mode 100644 index 00000000..c0d33e92 --- /dev/null +++ b/lib/views/auth/login.jade @@ -0,0 +1,21 @@ +.panel.panel-default + .panel-heading + h3.panel-title Semaphore Log In + .panel-body + form.form-horizontal + .form-group(ng-if="status.length > 0") + .col-sm-7.col-sm-offset-4 + span(ng-bind="status") + + .form-group + label.control-label.col-sm-4 Email + .col-sm-7 + input.form-control(type="email" ng-model="user.auth" placeholder="Email Address") + .form-group + label.control-label.col-sm-4 Password + .col-sm-7 + input.form-control(type="password" ng-model="user.password" placeholder="Password") + + .form-group + .col-sm-7.col-sm-offset-4 + button.btn.btn-default(ng-click="authenticate(user)") Log in \ No newline at end of file diff --git a/lib/views/homepage.jade b/lib/views/homepage.jade new file mode 100644 index 00000000..8bb858f0 --- /dev/null +++ b/lib/views/homepage.jade @@ -0,0 +1 @@ +h1 Hello there! \ No newline at end of file diff --git a/lib/views/layout.jade b/lib/views/layout.jade new file mode 100644 index 00000000..7805605f --- /dev/null +++ b/lib/views/layout.jade @@ -0,0 +1,39 @@ +doctype +html + head + meta(http-equiv="Content-Type", content="text/html; charset=utf-8;") + meta(name="viewport" content="width=device-width, initial-scale=1.0") + block meta + title(ng-bind-template="{{ pageTitle }} - Semaphore") Semaphore + link(href="/favicon.ico" type="image/x-icon" rel="icon") + link(href="/favicon.ico" type="image/x-icon" rel="shortcut icon") + + //- all css goes here + block css + + link(rel="stylesheet" href="/css/semaphore.css") + + body + .container-fluid + .row + .col-sm-3.col-lg-2 + ul.nav + h2.text-center: a(ui-sref="homepage") Semaphore + + li(ng-repeat="playbook in playbooks") + a(ui-sref="playbook({ pid: playbook._id })") {{ playbook.name }} + + .col-sm-9.col-lg-10 + block content + ui-view(autoscroll="false") + p.lead.text-center + i.fa.fa-spin.fa-cog + | Loading... + + block js + script(src="/vendor/requirejs/require.js" data-main="/js/semaphore.js") + if use_analytics + != newrelic.getBrowserTimingHeader() + + //- page-specific js + block addonjs \ No newline at end of file diff --git a/newrelic.js b/newrelic.js new file mode 100644 index 00000000..d635fe1a --- /dev/null +++ b/newrelic.js @@ -0,0 +1,15 @@ +var config = require('./lib/config') + +exports.config = { + app_name : ['Semaphore'], + license_key : config.credentials.newrelic_key, + logging : { + level : 'trace' + }, + rules: { + ignore: [ + '^/socket.io/.*/*-polling', + '^/ping$' + ] + } +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..ff568a8d --- /dev/null +++ b/package.json @@ -0,0 +1,76 @@ +{ + "name": "semaphore", + "version": "0.0.0", + "description": "Open Source Alternative to Ansible Tower", + "main": "bin/semaphore.js", + "scripts": { + "test": "mocha -R spec", + "start": "node bin/semaphore.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/CastawayLabs/semaphore.git" + }, + "keywords": [ + "ansible" + ], + "author": "Matej Kramny ", + "license": "MIT", + "bugs": { + "url": "https://github.com/CastawayLabs/semaphore/issues" + }, + "homepage": "https://github.com/CastawayLabs/semaphore", + "dependencies": { + "async": "latest", + "bcrypt": "^0.8.0", + "body-parser": "1.4.3", + "bower": "latest", + "bugsnag": "latest", + "connect-mongo": "latest", + "connect-redis": "latest", + "cookie-parser": "latest", + "express": "latest", + "express-session": "latest", + "grunt": "latest", + "grunt-bump": "latest", + "grunt-cli": "latest", + "grunt-concurrent": "latest", + "grunt-contrib-clean": "latest", + "grunt-contrib-concat": "latest", + "grunt-contrib-connect": "latest", + "grunt-contrib-copy": "latest", + "grunt-contrib-cssmin": "latest", + "grunt-contrib-jade": "latest", + "grunt-contrib-jshint": "latest", + "grunt-contrib-less": "latest", + "grunt-contrib-uglify": "latest", + "grunt-contrib-watch": "latest", + "grunt-newer": "latest", + "grunt-nodemon": "latest", + "hiredis": "latest", + "jade": "latest", + "load-grunt-tasks": "latest", + "moment": "latest", + "mongodb": "latest", + "mongoose": "latest", + "morgan": "^1.2.2", + "mysql": "latest", + "newrelic": "latest", + "nodemailer": "latest", + "nodemailer-smtp-transport": "latest", + "passport": "latest", + "passport.socketio": "latest", + "ratelimiter": "^1.0.1", + "redis": "latest", + "request": "^2.40.0", + "serve-static": "latest", + "socket.io": "latest", + "socket.io-client": "latest", + "speakeasy": "latest", + "validator": "latest" + }, + "devDependencies": { + "should": "latest", + "supertest": "latest" + } +} diff --git a/public/css/semaphore.less b/public/css/semaphore.less new file mode 100644 index 00000000..b8c3fc92 --- /dev/null +++ b/public/css/semaphore.less @@ -0,0 +1,13 @@ +@import '../vendor/bootstrap/less/variables.less'; +@import '../vendor/fontawesome/less/variables.less'; +@import '//fonts.googleapis.com/css?family=Roboto:300,400,400italic,700,500,700italic'; +@import '//fonts.googleapis.com/css?family=Source+Code+Pro:300,400,500,700:latin'; + +@fa-font-path: "../vendor/fontawesome/fonts"; + +@font-family-sans-serif: Roboto, Arial, sans-serif; +@font-family-monospace: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +@font-family-base: @font-family-sans-serif; + +@import '../vendor/bootstrap/less/bootstrap.less'; +@import '../vendor/fontawesome/less/font-awesome.less'; diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 00000000..a6765049 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,36 @@ +define([ + 'angular', + 'couchPotato' +], function(angular, couchPotato, ga) { + var app = angular.module('semaphore', ['scs.couch-potato', 'ui.router']); + + couchPotato.configureApp(app); + + app.run(function($rootScope, $window, $couchPotato) { + app.lazy = $couchPotato; + + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl){ + if (newUrl.match(/\&no_router/)) { + event.preventDefault(); + $window.location.href = newUrl.replace(/\&no_router/, ''); + } + }); + + $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { + if (toState.pageTitle) { + $rootScope.pageTitle = "Loading " + toState.pageTitle; + } else { + $rootScope.pageTitle = "Loading.."; + } + }); + $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams){ + if (toState.pageTitle) { + $rootScope.pageTitle = toState.pageTitle; + } else { + $rootScope.pageTitle = "Semaphore Page"; + } + }) + }); + + return app; +}) \ No newline at end of file diff --git a/public/js/controllers/auth/login.js b/public/js/controllers/auth/login.js new file mode 100644 index 00000000..cdae36f0 --- /dev/null +++ b/public/js/controllers/auth/login.js @@ -0,0 +1,35 @@ +define([ + 'app' +], function(app) { + app.registerController('SignInCtrl', ['$scope', '$rootScope', '$http', '$state', function($scope, $rootScope, $http, $state) { + $scope.status = ""; + $scope.user = { + auth: "", + password: "" + }; + + $scope.authenticate = function(user) { + $scope.status = "Authenticating.."; + + var pwd = user.password; + user.password = ""; + + $http.post('/auth/password', { + auth: user.auth, + password: pwd + }).success(function(data, status) { + $scope.status = "Login Successful"; + window.location = "/"; + }).error(function (data, status) { + if (status == 400) { + // Login Failed + $scope.status = data.message; + + return; + } + + $scope.status = status + ' Request Failed. Try again later.'; + }); + } + }]); +}); \ No newline at end of file diff --git a/public/js/routes/auth.js b/public/js/routes/auth.js new file mode 100644 index 00000000..8bd741a0 --- /dev/null +++ b/public/js/routes/auth.js @@ -0,0 +1,19 @@ +define([ + 'app' +], function(app) { + app.config(function($stateProvider, $urlRouterProvider, $locationProvider, $couchPotatoProvider) { + $locationProvider.html5Mode(true); + + $urlRouterProvider.otherwise('/'); + + $stateProvider.state('login', { + url: '/', + pageTitle: "Sign In", + templateUrl: "/view/auth/login", + controller: "SignInCtrl", + resolve: { + dummy: $couchPotatoProvider.resolveDependencies(['controllers/auth/login']) + } + }) + }); +}); \ No newline at end of file diff --git a/public/js/routes/routes.js b/public/js/routes/routes.js new file mode 100644 index 00000000..76d983b7 --- /dev/null +++ b/public/js/routes/routes.js @@ -0,0 +1,35 @@ +define([ + 'app', + 'services/user' +], function(app) { + app.config(function($stateProvider, $urlRouterProvider, $locationProvider, $couchPotatoProvider) { + $locationProvider.html5Mode(true); + + $urlRouterProvider.otherwise(''); + + $stateProvider + .state('homepage', { + url: '/', + pageTitle: 'Homepage', + templateUrl: "/view/homepage" + }) + + .state('logout', { + url: '/logout', + pageTitle: 'Log Out', + controller: function($scope) { + window.location = "/logout"; + } + }) + }) + .run(function($rootScope, $state, $stateParams, $http, user) { + $rootScope.$state = $state + $rootScope.$stateParams = $stateParams + + user.getUser(function() {}) + + $http.get('/playbooks').success(function(data, status) { + $rootScope.playbooks = data.playbooks; + }) + }) +}) \ No newline at end of file diff --git a/public/js/semaphore.js b/public/js/semaphore.js new file mode 100644 index 00000000..0ed5c350 --- /dev/null +++ b/public/js/semaphore.js @@ -0,0 +1,34 @@ +require.config({ + paths: { + angular: '../vendor/angular/angular.min', + uiRouter: '../vendor/angular-ui-router/release/angular-ui-router.min', + jquery: '../vendor/jquery/dist/jquery.min', + moment: '../vendor/moment/moment', + bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min', + couchPotato: '../vendor/angular-couch-potato/dist/angular-couch-potato' + }, + shim: { + angular: { + exports: 'angular' + }, + uiRouter: { + deps: ['angular'] + }, + bootstrap: ['jquery'] + } +}); + +require([ + 'jquery', + 'angular', + 'couchPotato', + 'uiRouter', + 'app', + 'routes/routes' +], function($, angular) { + var $html = angular.element(document.getElementsByTagName('html')[0]); + + angular.element().ready(function() { + angular.bootstrap($html, ['semaphore']) + }); +}); \ No newline at end of file diff --git a/public/js/semaphore_auth.js b/public/js/semaphore_auth.js new file mode 100644 index 00000000..ad75a687 --- /dev/null +++ b/public/js/semaphore_auth.js @@ -0,0 +1,34 @@ +require.config({ + paths: { + angular: '../vendor/angular/angular.min', + uiRouter: '../vendor/angular-ui-router/release/angular-ui-router.min', + jquery: '../vendor/jquery/dist/jquery.min', + moment: '../vendor/moment/moment', + bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min', + couchPotato: '../vendor/angular-couch-potato/dist/angular-couch-potato' + }, + shim: { + angular: { + exports: 'angular' + }, + uiRouter: { + deps: ['angular'] + }, + bootstrap: ['jquery'] + } +}); + +require([ + 'jquery', + 'angular', + 'couchPotato', + 'uiRouter', + 'app', + 'routes/auth' +], function($, angular) { + var $html = angular.element(document.getElementsByTagName('html')[0]); + + angular.element().ready(function() { + angular.bootstrap($html, ['semaphore']) + }); +}); \ No newline at end of file diff --git a/public/js/services/user.js b/public/js/services/user.js new file mode 100644 index 00000000..a129a145 --- /dev/null +++ b/public/js/services/user.js @@ -0,0 +1,15 @@ +define([ + 'app' +], function(app) { + app.service('user', function($http, $rootScope) { + var self = this; + + self.getUser = function(cb) { + $http.get('/profile').success(function(data) { + $rootScope.user = self.user = data.user; + + cb(); + }); + } + }); +}); \ No newline at end of file