diff --git a/app/models/_user.js b/app/models/_user.js new file mode 100644 index 00000000..e39f92c1 --- /dev/null +++ b/app/models/_user.js @@ -0,0 +1,102 @@ +var schemas = require("./schemas.js"); +var _ = require("lodash"); +var fs = require('fs'); +var userHomeDirectory = require('user-home'); +var createIfNotExist = require("create-if-not-exist"); + +var User = function (data) { + this.data = data; +} + +var usersPath = userHomeDirectory + '/users.json'; + +User.prototype.data = {} + +User.prototype.changeName = function (name) { + this.data.name = name; +} + +User.prototype.get = function (name) { + return this.data[name]; +} + +User.prototype.set = function (name, value) { + this.data[name] = value; +} + +User.prototype.sanitize = function (data) { + data = data || {}; + schema = schemas.user; + return _.pick(_.defaults(data, schema), _.keys(schema)); +} + +User.prototype.save = function (callback) { + var self = this; + this.data = this.data; + var result = saveUser({github: this.data.github}, this.data); + callback(null, result); +} + +User.findById = function (id, callback) { + var data = getUsers({github: id}); + if (err) return callback(err); + callback(null, new User(data)); +} + +User.findOne = function(findBy, callback) { + var users = getUsers(findBy); + if(!users || users.length === 0) { + callback(); + } else { + callback(undefined, users); + } + +} + +User.getAllUsers = function() { + return getUsers(); +} + +function getUsers(findBy) { + createIfNotExist(usersPath, '{ "users":[] }'); + var users = JSON.parse(fs.readFileSync(usersPath, 'utf8')).users; + if (!findBy) { + return users; + } + return _.find(users, findBy); +} + +function saveUser(findBy, newUserData) { + if (!findBy || !newUserData) { + return; + } + + var users = getUsers(); + + var match = _.find(users, findBy); + if (match) { + var index = _.indexOf(users, match); + users.splice(index, 1, newUserData); + } else { + users.push(newUserData); + } + + var usersFile = { + "users": users + }; + + fs.writeFile(usersPath, + JSON.stringify(usersFile, null, 2), + 'utf8', + function(err) { + if (err) { + res.json(err); + console.error('users couldn\'t be saved: ' + err); + } else { + console.info('users saved: ' + usersPath); + } + } + ); +} + +module.exports = User; \ No newline at end of file diff --git a/app/models/schemas.js b/app/models/schemas.js new file mode 100644 index 00000000..94e9930e --- /dev/null +++ b/app/models/schemas.js @@ -0,0 +1,9 @@ +var schemas = { + user: { + id: null, + name: null, + password: null + } +} + +module.exports = schemas; \ No newline at end of file diff --git a/app/routes/api.js b/app/routes/api.js index 038dde89..a630e4e8 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -6,10 +6,48 @@ module.exports = function(app, express) { require('../models/userHomeDirectoryService.js'), downloadService = require('../models/downloadService.js'), quotesService = require('../models/quotesService.js'), - settingsService = require('../models/settingsService.js'); + settingsService = require('../models/settingsService.js'), + jwt = require('jsonwebtoken'); + + var config = { + TOKEN_SECRET: 'kibibitIsAwesome' + }; var apiRouter = express.Router(); + // route middleware to verify a token + apiRouter.use(function(req, res, next) { + // check header or url parameters or post parameters for token + var token = req.body.token || req.params.token || req.headers['x-access-token']; + // decode token + if (token) { + // verifies secret and checks exp + jwt.verify(token, config.TOKEN_SECRET, function(err, decoded) { + if (err) { + console.info('failed to authenticate', err); + return res.status(403).send({ + success: false, + message: 'Failed to authenticate token.' + }); + } else { + // if everything is good, save to request for use in other routes + req.decoded = decoded; + console.info('user authenticated successfully'); + next(); + } + }); + } else { + // if there is no token + // return an HTTP response of 403 (access forbidden) and an error message + console.info('No token provided'); + return res.status(403).send({ + success: false, + message: 'No token provided.' + }); + } + // next() used to be here + }); + apiRouter.get('/', function(req, res) { res.json({ message: 'hooray! welcome to our api!' diff --git a/app/routes/auth.js b/app/routes/auth.js new file mode 100644 index 00000000..a0942b55 --- /dev/null +++ b/app/routes/auth.js @@ -0,0 +1,102 @@ +module.exports = function(app, express) { + + var qs = require('querystring'); + var express = require('express'); + var jwt = require('jwt-simple'); + var request = require('request'); + var fs = require('fs'); + var User = require('../models/_user'); + var moment = require('moment'); + + var authRouter = express.Router(); + + var config = { + TOKEN_SECRET: 'kibibitIsAwesome' + }; + + //var users = userHomeDirectory + '/.users.json'; + + authRouter.route('/github') + .post(function(req, res) { + var accessTokenUrl = 'https://github.com/login/oauth/access_token'; + var userApiUrl = 'https://api.github.com/user'; + var params = { + code: req.body.code, + client_id: req.body.clientId, + client_secret: 'f0c57fb762f6fe9e7472eb23a8de902265bd5f63', + redirect_uri: req.body.redirectUri + }; + + // Step 1. Exchange authorization code for access token. + request.get({ url: accessTokenUrl, qs: params }, function(err, response, accessToken) { + accessToken = qs.parse(accessToken); + var headers = { 'User-Agent': 'Satellizer' }; + + // Step 2. Retrieve profile information about the current user. + request.get({ url: userApiUrl, qs: accessToken, headers: headers, json: true }, function(err, response, profile) { + + // Step 3a. Link user accounts. + if (req.header('Authorization')) { + User.findOne({ github: profile.id }, function(err, existingUser) { + if (existingUser) { + return res.status(409).send({ message: 'There is already a GitHub account that belongs to you' }); + } + var token = req.header('Authorization').split(' ')[1]; + var payload = jwt.decode(token, config.TOKEN_SECRET); + User.findById(payload.sub, function(err, user) { + if (!user) { + return res.status(400).send({ message: 'User not found' }); + } + user.set('github', profile.id); + user.set('picture', user.picture || profile.avatar_url); + user.set('displayName', user.login || user.displayName || profile.name); + user.save(function() { + var token = createJWT(user); + res.send({ token: token }); + }); + }); + }); + } else { + // Step 3b. Create a new user account or return an existing one. + User.findOne({ github: profile.id }, function(err, existingUser) { + if (existingUser) { + var token = createJWT(existingUser); + return res.send({ token: token }); + } + var user = new User({}); + user.set('github', profile.id); + user.set('picture', profile.avatar_url); + user.set('displayName', profile.login || profile.name) + user.save(function() { + var token = createJWT(user); + res.send({ token: token }); + }); + }); + } + }); + }); + }); + + authRouter.route('/getUser') + .get(function(req, res) { + res.json({ + "user": User.getAllUsers()[0] + }); + }); + + /* + |-------------------------------------------------------------------------- + | Generate JSON Web Token + |-------------------------------------------------------------------------- + */ + function createJWT(user) { + var payload = { + sub: user._id, + iat: moment().unix(), + exp: moment().add(14, 'days').unix() + }; + return jwt.encode(payload, config.TOKEN_SECRET); + } + + return authRouter; +}; \ No newline at end of file diff --git a/bower.json b/bower.json index b37d3d11..c0eee9e3 100644 --- a/bower.json +++ b/bower.json @@ -23,7 +23,9 @@ "opentype.js": "^0.6.3", "underscore": "^1.8.3", "angular-material-icons": "^0.7.1", - "svg-morpheus": "^0.3.2" + "svg-morpheus": "^0.3.2", + "satellizer": "^0.15.4", + "angular-strap": "^2.3.9" }, "resolutions": { "angular": "1.5.8" diff --git a/package.json b/package.json index fa36b226..453a673b 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,17 @@ "body-parser": "^1.14.2", "colors": "~1.1.2", "compression": "^1.6.2", + "create-if-not-exist": "0.0.2", "express": "^4.13.4", "helmet": "^2.1.1", + "jsonwebtoken": "^7.1.9", + "jwt-simple": "^0.5.0", + "lodash": "^4.15.0", "mime-types": "^2.1.11", + "moment": "^2.14.1", + "octonode": "^0.7.6", + "querystring": "^0.2.0", + "request": "^2.74.0", "scribe-js": "^2.0.4", "serve-favicon": "~2.3.0", "user-home": "^2.0.0" diff --git a/public/app/app.js b/public/app/app.js index 145661a0..7093f146 100644 --- a/public/app/app.js +++ b/public/app/app.js @@ -15,12 +15,32 @@ angular.module('kibibitCodeEditor', 'jsonFormatter', 'ngclipboard', 'ng.deviceDetector', - 'ngMdIcons']) + 'ngMdIcons', + 'satellizer']) + +// application configuration to integrate token into requests +.config(function($httpProvider) { + // attach our auth interceptor to the http requests + $httpProvider.interceptors.push('AuthInterceptor'); +}) .config(['$compileProvider', function($compileProvider) { $compileProvider.debugInfoEnabled(false); }]) +.config(function($authProvider) { + $authProvider.github({ + clientId: 'e4c42e628b792e23f268' + }); + + $authProvider.tokenName = 'token'; + $authProvider.tokenPrefix = 'kibibit'; + $authProvider.tokenHeader = 'Authorization'; + $authProvider.tokenType = 'Bearer'; + $authProvider.storageType = 'localStorage'; + //$authProvider.redirectUri = window.location.origin; +}) + .config(['ScrollBarsProvider', function(ScrollBarsProvider) { // the following settings are defined for all scrollbars unless the // scrollbar has local scope configuration diff --git a/public/app/app.routes.js b/public/app/app.routes.js index 36ee7238..c6dc1f30 100644 --- a/public/app/app.routes.js +++ b/public/app/app.routes.js @@ -2,18 +2,44 @@ angular.module('app.routes', ['ngRoute']) .config(function($routeProvider, $locationProvider) { + /** + * Helper auth functions + */ + var skipIfLoggedIn = function($q, $auth) { + var deferred = $q.defer(); + if ($auth.isAuthenticated()) { + deferred.reject(); + } else { + deferred.resolve(); + } + return deferred.promise; + }; + + var loginRequired = function($q, $location, $auth) { + var deferred = $q.defer(); + if ($auth.isAuthenticated()) { + deferred.resolve(); + } else { + $location.path('/login'); + } + return deferred.promise; + }; + $routeProvider // route for the home page - .when('/', { - templateUrl: 'app/views/home.html' - }) + .when('/', { + templateUrl: 'app/views/home.html' + }) - // login page + // login page .when('/login', { templateUrl: 'app/views/login.html', controller: 'mainController', - controllerAs: 'login' + controllerAs: 'login', + resolve: { + skipIfLoggedIn: skipIfLoggedIn + } }) // show all users diff --git a/public/app/components/menuBar/menuBar.js b/public/app/components/menuBar/menuBar.js index bb8a08b9..3458271f 100644 --- a/public/app/components/menuBar/menuBar.js +++ b/public/app/components/menuBar/menuBar.js @@ -79,13 +79,7 @@ angular.module('kibibitCodeEditor') var editor = vm.settings.currentEditor; var selectionText = editor.getSelectedText(); var selection = vm.settings.currentEditor.selection.getRange(); - var camelCased = selectionText.replace( - /[_-\s]([a-zA-Z])/g, - function(g) { - return g[1].toUpperCase(); - } - ); - camelCased = camelCased[0].toLowerCase() + camelCased.substring(1); + var camelCased = _.camelCase(selectionText); vm.settings.currentEditor.session.replace( selection, camelCased); @@ -96,16 +90,7 @@ angular.module('kibibitCodeEditor') var editor = vm.settings.currentEditor; var selectionText = editor.getSelectedText(); var selection = vm.settings.currentEditor.selection.getRange(); - var kebabCased = selectionText.replace( - /[_-\s]([a-zA-Z])|([a-z])([A-Z])/g, - function(g, singleLetter, firstLetter, secondLetter) { - return secondLetter ? - firstLetter + '-' + secondLetter.toLowerCase() : - '-' + singleLetter.toLowerCase(); - } - ); - - kebabCased = kebabCased.toLowerCase(); + var kebabCased = _.kebabCase(selectionText); vm.settings.currentEditor.session.replace( selection, kebabCased); @@ -116,16 +101,8 @@ angular.module('kibibitCodeEditor') var editor = vm.settings.currentEditor; var selectionText = editor.getSelectedText(); var selection = vm.settings.currentEditor.selection.getRange(); - var snakeCased = selectionText.replace( - /[_-\s]([a-zA-Z])|([a-z])([A-Z])/g, - function(g, singleLetter, firstLetter, secondLetter) { - return secondLetter ? - firstLetter + '_' + secondLetter.toLowerCase() : - '_' + singleLetter.toLowerCase(); - } - ); + var snakeCased = _.snakeCase(selectionText); - snakeCased = snakeCased.toLowerCase(); vm.settings.currentEditor.session.replace( selection, snakeCased); diff --git a/public/app/components/menuBar/menuBarTemplate.html b/public/app/components/menuBar/menuBarTemplate.html index ff2cd3d5..f020583a 100644 --- a/public/app/components/menuBar/menuBarTemplate.html +++ b/public/app/components/menuBar/menuBarTemplate.html @@ -1,6 +1,9 @@
+ + Logout +
Login