├── .bowerrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── bower.json ├── dist ├── libs.js └── scripts.js ├── fast-typing.gif ├── gulpfile.js ├── index.js ├── karma.conf.js ├── modules ├── activities │ ├── public │ │ ├── activities.controllers.js │ │ ├── activities.js │ │ ├── activities.routes.js │ │ ├── activities.services.js │ │ └── views │ │ │ └── activities.html │ └── server │ │ ├── controllers │ │ └── activities.js │ │ ├── main.js │ │ ├── models │ │ └── activities.js │ │ └── routes │ │ └── activities.js ├── chats │ ├── public │ │ ├── chats.controllers.js │ │ ├── chats.js │ │ ├── chats.routes.js │ │ ├── chats.services.js │ │ └── views │ │ │ ├── chat-dialog.html │ │ │ └── chatsList.html │ └── server │ │ ├── controllers │ │ └── chats.js │ │ ├── main.js │ │ ├── models │ │ └── chats.js │ │ └── routes │ │ └── chats.js ├── notifications │ └── public │ │ ├── notifications.controllers.js │ │ ├── notifications.js │ │ ├── notifications.services.js │ │ └── views │ │ └── activities.html ├── posts │ ├── public │ │ ├── posts.controllers.js │ │ ├── posts.js │ │ ├── posts.routes.js │ │ ├── posts.services.js │ │ └── views │ │ │ ├── feed.html │ │ │ └── post-single.html │ └── server │ │ ├── controllers │ │ └── posts.js │ │ ├── main.js │ │ ├── models │ │ └── posts.js │ │ ├── routes │ │ └── posts.js │ │ └── tests │ │ └── posts.test.js ├── streams │ ├── public │ │ ├── streams.controllers.js │ │ ├── streams.js │ │ ├── streams.routes.js │ │ ├── streams.services.js │ │ └── views │ │ │ └── streamsList.html │ └── server │ │ ├── controllers │ │ └── streams.js │ │ ├── main.js │ │ ├── models │ │ └── streams.js │ │ └── routes │ │ └── streams.js └── users │ ├── public │ ├── users.controllers.js │ ├── users.js │ ├── users.routes.js │ ├── users.services.js │ └── views │ │ ├── activating.html │ │ ├── change-password.html │ │ ├── login.html │ │ ├── profile.html │ │ ├── user-list.html │ │ ├── users-dialog.html │ │ ├── users-invite-dialog.html │ │ └── users-pwd-dialog.html │ └── server │ ├── controllers │ └── users.js │ ├── main.js │ ├── models │ └── users.js │ ├── routes │ └── users.js │ └── tests │ └── users.test.js ├── package.json ├── public ├── app.js ├── css │ └── main.css ├── images │ ├── preloader.gif │ ├── spacer.gif │ └── user.jpg ├── index.html └── src │ └── main.scss ├── system ├── config │ ├── development.js │ └── production.js ├── helpers │ ├── auth.js │ ├── emailing.js │ ├── events.js │ ├── json.js │ └── notifications.js ├── index.js ├── models │ └── settings.js ├── public │ ├── index.js │ ├── settings │ │ ├── settings.controllers.js │ │ ├── settings.js │ │ ├── settings.routes.js │ │ ├── settings.services.js │ │ └── views │ │ │ └── settings.html │ ├── templates │ │ └── notification.html │ ├── utils.js │ └── views │ │ └── search.html ├── routes │ ├── search.js │ └── settings.js └── tests │ └── system.spec.js └── tools └── test ├── assets.json └── mocha-req.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | public/bower_components 28 | public/uploads 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | /**/.DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | 6 | before_install: 7 | - npm install -g gulp 8 | - npm install -g bower 9 | 10 | install: 11 | - npm install 12 | - bower cache clean 13 | - bower install 14 | 15 | before_script: 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | 19 | services: 20 | - mongodb 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Riten Vagadiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![travis-build](https://api.travis-ci.org/ritenv/atwork.svg) 2 | 3 | | | 4 | ,---.|--- . . .,---.,---.|__/ 5 | ,---|| | | || || | \ 6 | `---^`---' `-'-'`---'` ` ` 7 | 8 | **Project atwork** is an open-source social network for your workplace 9 | (Under development) 10 | 11 | ## Idea 12 | 13 | **Project atwork** provides a simplistic, open-source social network with pluggable modules to customize the experience different workplaces desire. It can be easily hosted, customized and white-labeled. 14 | 15 | ## Stack 16 | 17 | **Project atwork** is built entirely in Javascript, with a goal to bring the best of javascript technologies together in a seamless, customizable and modular approach. Below technology stack is being used: 18 | 19 | 1. [MongoDB](http://mongodb.org/): The leading NoSQL database. 20 | 2. [Express](http://expressjs.com/): Fast, unopinionated, minimalist web framework for Node.js. 21 | 3. [AngularJS](): HTML enhanced for web apps. 22 | 4. [Node.js](http://nodejs.org/): A platform built on Chrome's JavaScript runtime. 23 | 24 | ## Contribution 25 | 26 | Contributions are very much welcome! 27 | 28 | ![contribute](fast-typing.gif) 29 | 30 | I am looking for developers to help me out in carrying this project forward - so if you're interested, check out the issues section and you can pick any that interests you in order to get started. If you have any questions, just get in touch via the twitter handle [@ritenv](http://twitter.com/@ritenv). 31 | 32 | ## Background 33 | 34 | **Project atwork** is being developed by [@ritenv](http://twitter.com/@ritenv) as a hobby project. If you have any ideas, suggestions or things you'd like to see in this app, PM me directly at [@ritenv](http://twitter.com/@ritenv). -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atwork", 3 | "main": "index.js", 4 | "version": "0.0.0", 5 | "homepage": "https://github.com/matterial/atwork", 6 | "authors": [ 7 | "Riten Vagadiya " 8 | ], 9 | "moduleType": [ 10 | "node" 11 | ], 12 | "license": "MIT", 13 | "private": true, 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "public/bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "q": "1.0.1", 23 | "fontawesome": "~4.3.0", 24 | "ng-file-upload": "~3.0.2", 25 | "socket.io-client": "~1.3.5", 26 | "jquery": "~2.1.3", 27 | "lodash": "~3.8.0", 28 | "angular": "~1.3.15", 29 | "angular-material": "~0.9.0", 30 | "angular-route": "~1.3.15", 31 | "angular-messages": "~1.3.15", 32 | "angular-resource": "~1.3.15", 33 | "angular-loading-bar": "~0.7.1", 34 | "angular-mocks": "~1.3.15" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /fast-typing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritenv/atwork/3259d509ad56e994d9181e2a4f227ac879601e8f/fast-typing.gif -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sass = require('gulp-sass'); 3 | var uglify = require('gulp-uglify'); 4 | var concat = require('gulp-concat'); 5 | 6 | gulp.task('uglify', function() { 7 | return gulp.src([ 8 | /** 9 | * Load system files first 10 | */ 11 | 'system/public/utils.js', 12 | 'system/public/index.js', 13 | 'system/public/settings/settings.js', 14 | 'system/public/settings/settings.controllers.js', 15 | 'system/public/settings/settings.routes.js', 16 | 'system/public/settings/settings.services.js', 17 | 18 | /** 19 | * Load the main module 20 | */ 21 | 'modules/**/public/!(*.test|*.controllers|*.services|*.routes).js', 22 | 23 | /** 24 | * Load the controllers, services and routes 25 | */ 26 | 'modules/**/public/**/*.controllers.js', 27 | 'modules/**/public/**/*.services.js', 28 | 'modules/**/public/**/*.routes.js', 29 | 30 | './public/app.js' 31 | ]) 32 | .pipe(uglify()) 33 | .pipe(concat('scripts.js')) 34 | .pipe(gulp.dest('./dist')); 35 | }); 36 | 37 | gulp.task('uglifylibs', function() { 38 | return gulp.src([ 39 | './public/bower_components/jquery/dist/jquery.min.js', 40 | './public/bower_components/angular/angular.js', 41 | './public/bower_components/angular-aria/angular-aria.js', 42 | './public/bower_components/angular-animate/angular-animate.js', 43 | './public/bower_components/angular-material/angular-material.js', 44 | './public/bower_components/angular-route/angular-route.js', 45 | './public/bower_components/angular-messages/angular-messages.js', 46 | './public/bower_components/angular-resource/angular-resource.js', 47 | './public/bower_components/angular-loading-bar/build/loading-bar.min.js', 48 | './public/bower_components/lodash/lodash.min.js', 49 | 50 | './public/bower_components/q/q.js', 51 | './public/bower_components/ng-file-upload/angular-file-upload.js' 52 | ]) 53 | .pipe(uglify()) 54 | .pipe(concat('libs.js')) 55 | .pipe(gulp.dest('dist')); 56 | }); 57 | 58 | gulp.task('pack', ['uglifylibs', 'uglify'], function() { 59 | return true; 60 | }); 61 | 62 | gulp.task('sass', function () { 63 | gulp.src('./public/src/*.scss') 64 | .pipe(sass()) 65 | .pipe(gulp.dest('./public/css')); 66 | }); 67 | 68 | gulp.task('default', ['sass', 'pack']); 69 | 70 | var watcher = gulp.watch(['public/**/*.scss', 'modules/**/public/**/*.js'], ['default', 'pack']); 71 | watcher.on('change', function(event) { 72 | console.log('File ' + event.path + ' was ' + event.type + ', running tasks...'); 73 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /********************************** 2 | | | 3 | ,---.|--- . . .,---.,---.|__/ 4 | ,---|| | | || || | \ 5 | `---^`---' `-'-'`---'` ` ` 6 | **********************************/ 7 | 8 | /** 9 | * Load the system 10 | */ 11 | var System = require('./system'); 12 | 13 | /** 14 | * Boot up 15 | */ 16 | System.boot(); 17 | 18 | /** 19 | * Export the module 20 | * @type {Object} 21 | */ 22 | module.exports = System; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Karma configuration 4 | module.exports = function(config) { 5 | var _ = require('lodash'), 6 | basePath = '.', 7 | assets = require(basePath + '/tools/test/assets.json'); 8 | 9 | config.set({ 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: basePath, 13 | 14 | // frameworks to use 15 | frameworks: ['mocha'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: _.flatten(_.values(assets.core.js)).concat([ 19 | /** 20 | * Load system files first 21 | */ 22 | 'system/public/utils.js', 23 | 'system/public/index.js', 24 | 25 | /** 26 | * Load the main module 27 | */ 28 | 'modules/**/public/!(*.test|*.controllers|*.services|*.routes).js', 29 | 30 | /** 31 | * Load the controllers, services and routes 32 | */ 33 | 'modules/**/public/**/*.controllers.js', 34 | 'modules/**/public/**/*.services.js', 35 | 'modules/**/public/**/*.routes.js', 36 | 37 | "node_modules/mocha/mocha.js", 38 | "node_modules/chai/chai.js", 39 | 40 | 'system/**/*.spec.js' 41 | ]), 42 | 43 | // list of files to exclude 44 | exclude: [], 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 48 | reporters: ['progress', 'coverage'], 49 | 50 | // coverage 51 | preprocessors: { 52 | // source files that you want to generate coverage for 53 | // do not include tests or libraries 54 | // (these files will be instrumented by Istanbul) 55 | 'modules/**/public/**/!(*.test).js': ['coverage'] 56 | }, 57 | 58 | coverageReporter: { 59 | type: 'html', 60 | dir: 'test/coverage/' 61 | }, 62 | 63 | // web server port 64 | port: 9876, 65 | 66 | // enable / disable colors in the output (reporters and logs) 67 | colors: true, 68 | 69 | // level of logging 70 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 71 | logLevel: config.LOG_INFO, 72 | 73 | // enable / disable watching file and executing tests whenever any file changes 74 | autoWatch: true, 75 | 76 | // Start these browsers, currently available: 77 | // - Chrome 78 | // - ChromeCanary 79 | // - Firefox 80 | // - Opera 81 | // - Safari (only Mac) 82 | // - PhantomJS 83 | // - IE (only Windows) 84 | browsers: ['PhantomJS'], 85 | 86 | // If browser does not capture in given timeout [ms], kill it 87 | captureTimeout: 60000, 88 | 89 | // Continuous Integration mode 90 | // if true, it capture browsers, run tests and exit 91 | singleRun: true 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /modules/activities/public/activities.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.activities') 4 | .controller('ActivitiesCtrl', [ 5 | '$scope', 6 | '$rootScope', 7 | '$routeParams', 8 | '$timeout', 9 | 'appPosts', 10 | 'appActivities', 11 | 'appAuth', 12 | 'appToast', 13 | 'appStorage', 14 | 'appLocation', 15 | 'appWebSocket', 16 | function($scope, $rootScope, $routeParams, $timeout, appPosts, appActivities, appAuth, appToast, appStorage, appLocation, appWebSocket) { 17 | $scope.lastUpdated = 0; 18 | $scope.newActivitiesCount = 0; 19 | $scope.actions = []; 20 | var userId = $routeParams.userId; 21 | 22 | var activitiesData = appActivities.get({userId: userId, timestamp: $scope.lastUpdated}, function() { 23 | $scope.actions = activitiesData.res.records ? activitiesData.res.records.concat($scope.actions) : $scope.actions; 24 | $scope.lastUpdated = Date.now(); 25 | }); 26 | } 27 | ]) 28 | ; 29 | -------------------------------------------------------------------------------- /modules/activities/public/activities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.activities', ['atwork.system']); -------------------------------------------------------------------------------- /modules/activities/public/activities.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.activities') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | 6 | $locationProvider.html5Mode(true); 7 | }]); -------------------------------------------------------------------------------- /modules/activities/public/activities.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.activities') 4 | .factory('appActivities', ['$resource', 5 | function($resource) { 6 | return $resource('activities/feed/:userId', { 7 | userId: '@_id' 8 | }); 9 | } 10 | ]) 11 | ; 12 | -------------------------------------------------------------------------------- /modules/activities/public/views/activities.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | Load New Items ({{newActivitiesCount}}) 6 | 7 | 8 | 9 |

10 | {{::item.created | date:'short'}} 11 | 12 | 13 | Liked this 14 | Removed my like from this 15 | Commented on this 16 | Written a new 17 | Post. 18 | 19 |

20 | 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /modules/activities/server/controllers/activities.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Activity = mongoose.model('Activity'); 3 | 4 | module.exports = function(System) { 5 | var User = mongoose.model('User'); 6 | var obj = {}; 7 | var json = System.plugins.JSON; 8 | var event = System.plugins.event; 9 | 10 | /** 11 | * Event listeners for post activities 12 | */ 13 | ['like', 'unlike', 'comment', 'newpost'].map(function(action) { 14 | event.on(action, function(data) { 15 | var post = data.post; 16 | var actor = data.actor; 17 | console.log(post.content, 'has been', action, 'by', actor.name); 18 | obj.create(action, actor, post); 19 | }); 20 | }); 21 | 22 | /** 23 | * Create a new activity 24 | * @param {Object} req Request 25 | * @param {Object} res Response 26 | * @return {Void} 27 | */ 28 | obj.create = function(action, actor, post) { 29 | var activity = new Activity({ 30 | actor: actor, 31 | post: post, 32 | action: action 33 | }); 34 | activity.save(function(err) { 35 | if (err) { 36 | return err; 37 | } 38 | return activity; 39 | }); 40 | }; 41 | 42 | /** 43 | * Get activities for a user 44 | * @param {Object} req The request object 45 | * @param {Object} res The response object 46 | * @return {Void} 47 | */ 48 | obj.feed = function(req, res) { 49 | 50 | /** 51 | * The search criteria 52 | * @type {Object} 53 | */ 54 | var criteria = {}; 55 | 56 | /** 57 | * Can accept username 58 | */ 59 | criteria.username = req.params.userId; 60 | 61 | User 62 | .findOne(criteria) 63 | .lean() 64 | .exec(function(err, user) { 65 | if (err) { 66 | return json.unhappy(err, res); 67 | } 68 | var activityCriteria = { actor: user._id }; 69 | Activity.find(activityCriteria, null, {sort: {created: -1}}) 70 | .populate('actor') 71 | .populate('post') 72 | .lean() 73 | .exec(function(err, activities) { 74 | /** 75 | * Filter activities to exclude the ones 76 | * whose posts are non existent 77 | */ 78 | activities = activities.filter(function(activity) { 79 | return activity.post; 80 | }); 81 | if (err) { 82 | return json.unhappy(err, res); 83 | } else { 84 | return json.happy({ 85 | records: activities 86 | }, res); 87 | } 88 | }); 89 | }); 90 | }; 91 | 92 | return obj; 93 | }; -------------------------------------------------------------------------------- /modules/activities/server/main.js: -------------------------------------------------------------------------------- 1 | var routes = require('./routes/activities'); 2 | 3 | module.exports = function(System) { 4 | /** 5 | * Name of this module 6 | * @type {String} 7 | */ 8 | var moduleName = 'activities'; 9 | 10 | /** 11 | * Build the routes 12 | * @type {Array} 13 | */ 14 | var builtRoutes = routes(System); 15 | 16 | /** 17 | * Add routes to System 18 | */ 19 | System.route(builtRoutes, moduleName); 20 | }; -------------------------------------------------------------------------------- /modules/activities/server/models/activities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'); 10 | 11 | /** 12 | * Getter 13 | */ 14 | var escapeProperty = function(value) { 15 | return _.escape(value); 16 | }; 17 | 18 | /** 19 | * Activity Schema 20 | */ 21 | 22 | var ActivitySchema = new Schema({ 23 | created: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | actor: { 28 | type: Schema.ObjectId, 29 | required: true, 30 | ref: 'User' 31 | }, 32 | post: { 33 | type: Schema.ObjectId, 34 | required: false, 35 | ref: 'Post' 36 | }, 37 | action: { 38 | type: String, 39 | required: true, 40 | get: escapeProperty 41 | } 42 | }); 43 | 44 | /** 45 | * Methods 46 | */ 47 | ActivitySchema.methods = { 48 | /** 49 | * Hide security sensitive fields 50 | * 51 | * @returns {*|Array|Binary|Object} 52 | */ 53 | toJSON: function() { 54 | var obj = this.toObject(); 55 | if (obj.actor) { 56 | delete obj.actor.token; 57 | delete obj.actor.hashed_password; 58 | delete obj.actor.salt; 59 | delete obj.actor.following; 60 | } 61 | return obj; 62 | } 63 | }; 64 | 65 | mongoose.model('Activity', ActivitySchema); 66 | -------------------------------------------------------------------------------- /modules/activities/server/routes/activities.js: -------------------------------------------------------------------------------- 1 | var myController = require('../controllers/activities'); 2 | /** 3 | * Init the controller 4 | */ 5 | module.exports = function(System) { 6 | var activities = myController(System); 7 | 8 | var routes = []; 9 | 10 | routes.push({ 11 | method: 'get', 12 | path: '/feed/:userId', 13 | handler: activities.feed, 14 | authorized: true 15 | }); 16 | 17 | return routes; 18 | }; -------------------------------------------------------------------------------- /modules/chats/public/chats.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.chats') 4 | .controller('ChatsCtrl', [ 5 | '$scope', 6 | '$rootScope', 7 | '$routeParams', 8 | '$timeout', 9 | 'appAuth', 10 | 'appToast', 11 | 'appStorage', 12 | 'appLocation', 13 | 'appWebSocket', 14 | 'appChats', 15 | 'appDialog', 16 | 'appDesktop', 17 | function($scope, $rootScope, $routeParams, $timeout, appAuth, appToast, appStorage, appLocation, appWebSocket, appChats, appDialog, appDesktop) { 18 | $scope.chats = []; 19 | $scope.actions = {}; 20 | var openChats = {}; 21 | 22 | var updateBadges = function() { 23 | /** 24 | * Update badge 25 | */ 26 | var messagesCount = 0; 27 | _.each($scope.chats, function(chat) { 28 | messagesCount += chat.unread; 29 | }) 30 | appDesktop.notify({messagesCount: messagesCount}); 31 | }; 32 | 33 | /** 34 | * Open a new conversation 35 | * @return {Void} 36 | */ 37 | $scope.message = function(ev, profile, chatItem) { 38 | var criteria; 39 | 40 | if (chatItem) { 41 | chatItem.unread = 0; 42 | updateBadges(); 43 | criteria = { 44 | chatId: chatItem._id 45 | }; 46 | } else { 47 | criteria = { 48 | participants: [ 49 | profile._id, 50 | appAuth.getUser()._id 51 | ] 52 | } 53 | } 54 | 55 | var chat = new appChats.single(criteria); 56 | 57 | chat.$save(function(response) { 58 | var chatId = response.res.record._id; 59 | 60 | /** 61 | * Add chat to openChats 62 | */ 63 | openChats[response.res.record._id] = response.res.record; 64 | 65 | /** 66 | * Show dialog 67 | */ 68 | appDialog.show({ 69 | controller: [ 70 | '$scope', 71 | 'appDialog', 72 | function($scope, appDialog) { 73 | updateBadges(); 74 | /** 75 | * Assign likers to the users variable 76 | * @type {Array} 77 | */ 78 | $scope.messages = response.res.record.messages; 79 | 80 | $scope.chatId = chatId; 81 | $scope.firstTime = true; 82 | 83 | $scope.$on('chatMessage', function(e, data) { 84 | $scope.$apply(function() { 85 | $scope.messages.unshift(data.chatMessage); 86 | }); 87 | appWebSocket.conn.emit('markAccessed', {chatId: data.chatId, userId: appAuth.getUser()._id}); 88 | }); 89 | 90 | /** 91 | * Hide the dialog 92 | * @return {Void} 93 | */ 94 | $scope.hide = function() { 95 | appDialog.hide(); 96 | }; 97 | 98 | $scope.sendMessage = function(isValid) { 99 | if (isValid) { 100 | var message = $scope.message; 101 | $scope.message = ''; 102 | appChats.single.message({ 103 | message: message, 104 | creator: appAuth.getUser()._id, 105 | _id: $scope.chatId 106 | }, function(response) { 107 | $scope.messages.unshift(response.res.record.messages[0]); 108 | }); 109 | } 110 | }; 111 | } 112 | ], 113 | templateUrl: '/modules/chats/views/chat-dialog.html', 114 | targetEvent: ev, 115 | }) 116 | .finally(function() { 117 | delete openChats[chatId]; 118 | }); 119 | 120 | }); 121 | }; 122 | 123 | $scope.updateChats = function (options) { 124 | options = options || {}; 125 | var chatsData = appChats.single.get({}, function() { 126 | /** 127 | * Check if the feed needs to reload 128 | */ 129 | if (options.reload) { 130 | $scope.chats = []; 131 | } 132 | 133 | /** 134 | * Check whether to append to feed (at bottom) or insert (at top) 135 | */ 136 | if (chatsData.res.records.length) { 137 | if (!options.append) { 138 | $scope.chats = chatsData.res.records.concat($scope.chats); 139 | } else { 140 | $scope.chats = $scope.chats.concat(chatsData.res.records); 141 | } 142 | } 143 | 144 | updateBadges(); 145 | 146 | /** 147 | * Check if there are more pages 148 | * @type {Boolean} 149 | */ 150 | $scope.noMoreChats = !chatsData.res.morePages; 151 | 152 | /** 153 | * Set the updated timestamp 154 | */ 155 | $scope.lastUpdated = Date.now(); 156 | }); 157 | }; 158 | 159 | $scope.$on('chatMessage', function(e, data) { 160 | if (!openChats[data.chatId]) { 161 | $scope.updateChats({reload: true}); 162 | } 163 | }); 164 | 165 | } 166 | ]) 167 | ; 168 | -------------------------------------------------------------------------------- /modules/chats/public/chats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.chats', ['atwork.system']); -------------------------------------------------------------------------------- /modules/chats/public/chats.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.chats') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | $routeProvider 6 | ; 7 | $locationProvider.html5Mode(true); 8 | }]); -------------------------------------------------------------------------------- /modules/chats/public/chats.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.chats') 4 | .factory('appChats', ['$resource', 5 | function($resource) { 6 | return { 7 | single: $resource('chats/:chatId/:action', { 8 | chatId: '@_id' 9 | }, { 10 | message: { 11 | method: 'POST', 12 | params: {action: 'message'} 13 | }, 14 | end: { 15 | method: 'POST', 16 | params: {action: 'end'} 17 | } 18 | }) 19 | } 20 | } 21 | ]) 22 | ; 23 | -------------------------------------------------------------------------------- /modules/chats/public/views/chat-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat 4 | 5 |
6 | 7 | 8 | 9 |
10 |
This is required!
11 |
That's too long!
12 |
That's too short!
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | {{item.creator.name}} 21 |
22 |

{{item.creator.name}} {{item.created | date:'short'}}

23 |

{{item.message}}

24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 | 32 | 33 | Close 34 | 35 |
36 |
-------------------------------------------------------------------------------- /modules/chats/public/views/chatsList.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Chats 4 |

5 | 6 | 18 |

Loading...

19 |

No chats yet

20 |
-------------------------------------------------------------------------------- /modules/chats/server/controllers/chats.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Chat = mongoose.model('Chat'); 3 | 4 | module.exports = function(System) { 5 | var obj = {}; 6 | var json = System.plugins.JSON; 7 | var event = System.plugins.event; 8 | var sck = System.webSocket; 9 | 10 | /** 11 | * Chat related sockets 12 | */ 13 | sck.on('connection', function(socket){ 14 | socket.on('markAccessed', function(data) { 15 | Chat 16 | .findOne({_id: data.chatId}) 17 | .exec(function(err, chat) { 18 | chat.doAccess({_id: data.userId}); 19 | chat.calculateUnread(); 20 | chat.save(); 21 | }); 22 | }); 23 | }); 24 | 25 | 26 | /** 27 | * Event based notifications 28 | */ 29 | ['chatMessage'].map(function(action) { 30 | event.on(action, function(data) { 31 | var chat = data.chat; 32 | var actor = data.actor; 33 | var chatMessage = data.message; 34 | chat.notifyUsers({ 35 | chatId: chat._id, 36 | actorId: actor._id, 37 | type: action, 38 | chatMessage: chatMessage, 39 | config: { 40 | systemLevel: true 41 | } 42 | }, System); 43 | }); 44 | }); 45 | 46 | /** 47 | * Create a stream 48 | * @param {Object} req The request object 49 | * @param {Object} res The response object 50 | * @return {Void} 51 | */ 52 | obj.create = function (req, res) { 53 | 54 | /** 55 | * Always sort, so whoever creates, it has participants in the same order 56 | */ 57 | if (req.body.participants) { 58 | req.body.participants.sort(function(a, b) { 59 | return a < b; 60 | }); 61 | } 62 | 63 | var criteria = {}; 64 | if (req.body.chatId) { 65 | criteria = { 66 | _id: req.body.chatId 67 | }; 68 | } else { 69 | criteria = { 70 | participants: req.body.participants 71 | }; 72 | } 73 | Chat.findOne(criteria) 74 | .populate('creator') 75 | .populate('participants') 76 | .populate('messages') 77 | .populate('messages.creator') 78 | .exec(function(err, chat) { 79 | if (err) { 80 | return json.unhappy(err, res); 81 | } else if (chat) { 82 | chat.doAccess(req.user); 83 | chat.calculateUnread(); 84 | chat.save(function(err, chat) { 85 | return json.happy({ 86 | record: chat 87 | }, res); 88 | }); 89 | } else { 90 | var chat = new Chat(req.body); 91 | chat.creator = req.user._id; 92 | 93 | /** 94 | * Mark as first accessed for each participant 95 | */ 96 | req.body.participants.map(function(userId) { 97 | chat.doAccess({_id: userId}); 98 | }); 99 | chat.save(function(err) { 100 | if (err) { 101 | return json.unhappy(err, res); 102 | } 103 | return json.happy({ 104 | record: chat 105 | }, res); 106 | }); 107 | } 108 | }); 109 | }; 110 | 111 | /** 112 | * Create a stream 113 | * @param {Object} req The request object 114 | * @param {Object} res The response object 115 | * @return {Void} 116 | */ 117 | obj.message = function (req, res) { 118 | Chat.findOne({ 119 | _id: req.params.chatId 120 | }) 121 | .populate('creator') 122 | .populate('participants') 123 | .populate('messages') 124 | .populate('messages.creator') 125 | .exec(function(err, chat) { 126 | if (err) { 127 | return json.unhappy(err, res); 128 | } else if (chat) { 129 | chat.messages.unshift({ 130 | message: req.body.message, 131 | creator: req.user._id 132 | }); 133 | chat.doAccess(req.user); 134 | chat.calculateUnread(); 135 | chat.save(function(err, chat) { 136 | chat 137 | .populate('messages messages.creator', function(err, chat) { 138 | event.trigger('chatMessage', {chat: chat, message: chat.messages[0], actor: req.user}); 139 | return json.happy({ 140 | record: chat 141 | }, res); 142 | }) 143 | }) 144 | } else { 145 | return json.unhappy({message: 'Chat not found'}, res); 146 | } 147 | }); 148 | }; 149 | 150 | /** 151 | * Modify a stream 152 | * @param {Object} req The request object 153 | * @param {Object} res The response object 154 | * @return {Void} 155 | */ 156 | obj.addParticipant = function (req, res) { 157 | Chat.findOne({ 158 | _id: req.params.chatId 159 | }) 160 | .populate('creator') 161 | .exec(function(err, chat) { 162 | if (err) { 163 | return json.unhappy(err, res); 164 | } 165 | chat.participants.push = req.body.userId; 166 | chat.save(function(err) { 167 | if (err) { 168 | return json.unhappy(err, res); 169 | } 170 | return json.happy(chat, res); 171 | }); 172 | }); 173 | }; 174 | 175 | /** 176 | * List all streams 177 | * @param {Object} req The request object 178 | * @param {Object} res The response object 179 | * @return {Void} 180 | */ 181 | obj.list = function (req, res) { 182 | var user = req.user; 183 | 184 | var criteria = { 185 | participants: req.user 186 | }; 187 | 188 | Chat.find(criteria, null, {sort: {modified: 1}}) 189 | .populate('creator') 190 | .populate('participants') 191 | .skip(parseInt(req.query.page) * System.config.settings.perPage) 192 | .limit(System.config.settings.perPage+1) 193 | .exec(function(err, chats) { 194 | chats.map(function(chat) { 195 | chat.calculateUnreadFor(req.user); 196 | }); 197 | if (err) { 198 | json.unhappy(err, res); 199 | } else { 200 | var morePages = System.config.settings.perPage < chats.length; 201 | if (morePages) { 202 | chats.pop(); 203 | } 204 | json.happy({ 205 | records: chats, 206 | morePages: morePages 207 | }, res); 208 | } 209 | }); 210 | }; 211 | 212 | /** 213 | * Return info about single stream 214 | * @param {Object} req The req object 215 | * @param {Object} res The res object 216 | * @return {Void} 217 | */ 218 | obj.single = function(req, res) { 219 | Chat.findOne({ 220 | _id: req.params.chatId 221 | }) 222 | .populate('creator') 223 | .populate('participants') 224 | .populate('messages') 225 | .populate('messages.creator') 226 | .exec(function(err, chat) { 227 | if (err) { 228 | return json.unhappy(err, res); 229 | } else if (chat) { 230 | 231 | return json.happy({ 232 | record: chat 233 | }, res); 234 | } else { 235 | return json.unhappy({message: 'Chat not found'}, res); 236 | } 237 | }); 238 | }; 239 | 240 | return obj; 241 | }; -------------------------------------------------------------------------------- /modules/chats/server/main.js: -------------------------------------------------------------------------------- 1 | var routes = require('./routes/chats'); 2 | 3 | module.exports = function(System) { 4 | /** 5 | * Name of this module 6 | * @type {String} 7 | */ 8 | var moduleName = 'chats'; 9 | 10 | /** 11 | * Build the routes 12 | * @type {Array} 13 | */ 14 | var builtRoutes = routes(System); 15 | 16 | /** 17 | * Add routes to System 18 | */ 19 | System.route(builtRoutes, moduleName); 20 | }; -------------------------------------------------------------------------------- /modules/chats/server/models/chats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'); 10 | 11 | /** 12 | * Getter 13 | */ 14 | var escapeProperty = function(value) { 15 | return _.escape(value); 16 | }; 17 | 18 | /** 19 | * Post Schema 20 | */ 21 | 22 | var ChatsSchema = new Schema({ 23 | created: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | modified: { 28 | type: Date, 29 | default: Date.now 30 | }, 31 | lastAccessed: [{ 32 | accessed: { 33 | type: Date, 34 | default: Date.now 35 | }, 36 | user: { 37 | type: Schema.ObjectId, 38 | required: true, 39 | ref: 'User' 40 | }, 41 | unread: Number 42 | }], 43 | creator: { 44 | type: Schema.ObjectId, 45 | required: true, 46 | ref: 'User' 47 | }, 48 | unread: Number, 49 | messages: [ 50 | { 51 | created: { 52 | type: Date, 53 | default: Date.now 54 | }, 55 | creator: { 56 | type: Schema.ObjectId, 57 | required: true, 58 | ref: 'User' 59 | }, 60 | message: String 61 | } 62 | ], 63 | participants: [{ 64 | type: Schema.ObjectId, 65 | required: false, 66 | ref: 'User' 67 | }] 68 | }); 69 | 70 | /** 71 | * Methods 72 | */ 73 | ChatsSchema.methods = { 74 | /** 75 | * Hide security sensitive fields 76 | * 77 | * @returns {*|Array|Binary|Object} 78 | */ 79 | toJSON: function() { 80 | var obj = this.toObject(); 81 | if (obj.creator) { 82 | delete obj.creator.token; 83 | delete obj.creator.hashed_password; 84 | delete obj.creator.salt; 85 | delete obj.creator.following; 86 | } 87 | return obj; 88 | }, 89 | calculateUnread: function() { 90 | var obj = this; 91 | obj.lastAccessed.map(function(access) { 92 | access.unread = obj.messages.filter(function(msg) { 93 | return msg.created > access.accessed; 94 | }).length; 95 | }); 96 | }, 97 | calculateUnreadFor: function(user) { 98 | var obj = this; 99 | obj.lastAccessed.map(function(access) { 100 | if (access.user.toString() === user._id.toString()) { 101 | obj.unread = access.unread; 102 | } 103 | }); 104 | }, 105 | doAccess: function(user) { 106 | var chat = this; 107 | //change last accessed 108 | var lastAccessedUpdated = false; 109 | chat.lastAccessed.map(function(access) { 110 | if (access.user.toString() === user._id.toString()) { 111 | access.accessed = Date.now(); 112 | lastAccessedUpdated = true; 113 | } 114 | }); 115 | if (!lastAccessedUpdated) { 116 | chat.lastAccessed.push({ 117 | user: user._id, 118 | accessed: Date.now() 119 | }); 120 | } 121 | }, 122 | notifyUsers: function(data, System) { 123 | var chatMessage = data.chatMessage; 124 | 125 | var notification = { 126 | chatId: data.chatId, 127 | chatMessage: data.chatMessage, 128 | actorId: data.actorId, 129 | notificationType: data.type, 130 | config: data.config 131 | }; 132 | this.populate('creator participants', function(err, chat) { 133 | chat.participants.map(function(user) { 134 | /** 135 | * Ignore creator of message 136 | */ 137 | if (user._id.toString() === chatMessage.creator._id.toString()) { 138 | return; 139 | } 140 | /** 141 | * Ignore the person taking this action 142 | */ 143 | if (user._id.toString() === data.actorId.toString()) { 144 | return; 145 | } 146 | /** 147 | * Notify 148 | */ 149 | user.notify(notification, System); 150 | }); 151 | }); 152 | } 153 | }; 154 | 155 | mongoose.model('Chat', ChatsSchema); 156 | -------------------------------------------------------------------------------- /modules/chats/server/routes/chats.js: -------------------------------------------------------------------------------- 1 | var myController = require('../controllers/chats'); 2 | /** 3 | * Init the controller 4 | */ 5 | module.exports = function(System) { 6 | var chats = myController(System); 7 | 8 | var routes = []; 9 | 10 | routes.push({ 11 | method: 'post', 12 | path: '/', 13 | handler: chats.create, 14 | authorized: true 15 | }); 16 | 17 | routes.push({ 18 | method: 'get', 19 | path: '/', 20 | handler: chats.list, 21 | authorized: true 22 | }); 23 | 24 | routes.push({ 25 | method: 'get', 26 | path: '/:chatId', 27 | handler: chats.single, 28 | authorized: true 29 | }); 30 | 31 | routes.push({ 32 | method: 'post', 33 | path: '/:chatId/message', 34 | handler: chats.message, 35 | authorized: true 36 | }); 37 | 38 | return routes; 39 | }; -------------------------------------------------------------------------------- /modules/notifications/public/notifications.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('atwork.notifications') 3 | .controller('notificationsCtrl', [ 4 | '$scope', 5 | '$rootScope', 6 | 'appLocation', 7 | 'appUsers', 8 | 'appNotification', 9 | 'appWebSocket', 10 | 'appNotificationText', 11 | 'appDesktop', 12 | function($scope, $rootScope, appLocation, appUsers, appNotification, appWebSocket, appNotificationText, appDesktop) { 13 | /** 14 | * Initialize the defaults 15 | */ 16 | $scope.notificationShown = false; 17 | $scope.notificationCount = 0; 18 | $scope.items = []; 19 | 20 | /** 21 | * The event will be broadcasted when new notifications are received 22 | */ 23 | $rootScope.$on('notification', function(e, data) { 24 | $scope.updateNotifications(); 25 | }); 26 | 27 | /** 28 | * Hide or show notifications box 29 | * @param {Object} $event 30 | * @return {Void} 31 | */ 32 | $scope.showUserNotifications = function($event) { 33 | $scope.notificationShown = !$scope.notificationShown; 34 | }; 35 | 36 | /** 37 | * Mark notification as read 38 | * @param {Object} item The item object 39 | * @return {Void} 40 | */ 41 | $scope.markRead = function (item) { 42 | var record = appUsers.notifications.get({notificationId: item._id}, function () { 43 | if (record.res.notifications) { 44 | record.res.notifications.map(function (item) { 45 | item.display = appNotificationText(item); 46 | }); 47 | } 48 | $scope.items = record.res.notifications; 49 | $scope.notificationCount = record.res.notifications.length; 50 | }); 51 | $scope.showUserNotifications(); 52 | }; 53 | 54 | /** 55 | * Get notifications 56 | * @return {Void} 57 | */ 58 | $scope.updateNotifications = function () { 59 | var record = appUsers.notifications.get({}, function () { 60 | if (record.res.notifications) { 61 | record.res.notifications.map(function (item) { 62 | item.display = appNotificationText(item); 63 | if (item.post) { 64 | item.href = '/post/' + item.post._id 65 | } else if (item.user) { 66 | item.href = '/profile/' + item.actor.username 67 | } 68 | }); 69 | } 70 | $scope.items = record.res.notifications; 71 | $scope.notificationCount = record.res.notifications ? record.res.notifications.length : 0; 72 | appDesktop.notify({notificationsCount: $scope.notificationCount}); 73 | // if (window.fluid) { 74 | // window.fluid.dockBadge = $scope.notificationCount ? $scope.notificationCount : undefined; 75 | // if (window.fluid.dockBadge) { 76 | // window.fluid.playSound('Sosumi'); 77 | // window.fluid.playSound('Purr'); 78 | // } 79 | // } 80 | }); 81 | }; 82 | 83 | /** 84 | * Get initial notifications on load 85 | */ 86 | $scope.updateNotifications(); 87 | 88 | } 89 | ]); -------------------------------------------------------------------------------- /modules/notifications/public/notifications.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('atwork.notifications', ['atwork.system']) 3 | .run([ 4 | '$rootScope', 5 | 'appLocation', 6 | 'appNotification', 7 | 'appWebSocket', 8 | 'appNotificationText', 9 | function($rootScope, appLocation, appNotification, appWebSocket, appNotificationText) { 10 | appWebSocket.conn.on('notification', function (data) { 11 | /** 12 | * Broadcast the notification to the application 13 | */ 14 | $rootScope.$broadcast('notification', data); 15 | $rootScope.$broadcast(data.notificationType, data); 16 | 17 | /** 18 | * No data will be received if it is just a notification update signal 19 | */ 20 | if (!data) return; 21 | 22 | /** 23 | * Prepare to show the notification 24 | */ 25 | data.message = appNotificationText(data).text; 26 | 27 | data.then = function () { 28 | if (data.postId) { 29 | appLocation.url('/post/' + data.postId); 30 | } else if (data.userId) { 31 | appLocation.url('/profile/' + data.actor.username); 32 | } 33 | }; 34 | 35 | appNotification.show(data); 36 | }); 37 | 38 | /** 39 | * A system level notification is only 40 | * for broadcasting to the application 41 | */ 42 | appWebSocket.conn.on('system', function (data) { 43 | /** 44 | * Broadcast the notification to the application 45 | */ 46 | $rootScope.$broadcast(data.notificationType, data); 47 | }); 48 | } 49 | ]); -------------------------------------------------------------------------------- /modules/notifications/public/notifications.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.notifications') 4 | .factory('appNotification', [ 5 | '$resource', 6 | '$mdToast', 7 | function($resource, $mdToast) { 8 | return { 9 | show: function(data) { 10 | if (!data.message) { 11 | return; 12 | } 13 | var toast = $mdToast.simple() 14 | .content(data.message) 15 | .action('VIEW') 16 | .highlightAction(false) 17 | .position('bottom right'); 18 | 19 | $mdToast.show(toast).then(function() { 20 | if (data.then) { 21 | data.then(); 22 | } 23 | }); 24 | if (window.fluid) { 25 | window.fluid.showGrowlNotification({ 26 | title: "Atwork", 27 | description: data.message, 28 | priority: 1, 29 | sticky: false, 30 | identifier: "foo", 31 | onclick: function() { 32 | // window.fluid.activate(); 33 | }, 34 | icon: imgEl // or URL string 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | ]) 41 | .factory('appNotificationText', [ 42 | function() { 43 | return function(obj) { 44 | if (!obj) return {text: ''}; 45 | var msg = ''; 46 | var actor = obj.actor; 47 | 48 | switch (obj.notificationType) { 49 | case 'like': 50 | msg = actor.name + ' has liked a post'; 51 | break; 52 | 53 | case 'comment': 54 | msg = actor.name + ' has commented on a post'; 55 | break; 56 | 57 | case 'follow': 58 | msg = actor.name + ' is now following you'; 59 | break; 60 | 61 | case 'mention': 62 | msg = actor.name + ' mentioned you in a post'; 63 | break; 64 | } 65 | return {text: msg}; 66 | } 67 | } 68 | ]) 69 | ; 70 | -------------------------------------------------------------------------------- /modules/notifications/public/views/activities.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Load New Items ({{newActivitiesCount}}) 6 | 7 | 8 | 9 |

10 | {{::item.created | date:'short'}} 11 | 12 | I have 13 | liked this 14 | removed my like from this 15 | commented on this 16 | written a new 17 | Post. 18 | 19 |

20 | 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /modules/posts/public/posts.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.posts') 4 | .controller('PostsCtrl', [ 5 | '$scope', 6 | '$route', 7 | '$rootScope', 8 | '$routeParams', 9 | '$timeout', 10 | 'appPosts', 11 | 'appAuth', 12 | 'appToast', 13 | 'appStorage', 14 | 'appLocation', 15 | 'appWebSocket', 16 | 'appUsersSearch', 17 | 'appPostsFeed', 18 | 'resolvedFeeds', 19 | function($scope, $route, $rootScope, $routeParams, $timeout, appPosts, appAuth, appToast, appStorage, appLocation, appWebSocket, appUsersSearch, appPostsFeed, resolvedFeeds) { 20 | $scope.content = ''; 21 | $scope.lastUpdated = 0; 22 | $scope.postForm = ''; 23 | $scope.newFeedCount = 0; 24 | $scope.feed = []; 25 | $scope.feedsFilter = ''; 26 | $scope.limitComments = true; 27 | $scope.feedPage = 0; 28 | $scope.showBack = false; 29 | $scope.mentionsResults = []; 30 | 31 | var hashtag = $routeParams.hashtag; 32 | var userId = $scope.timelinePage = $routeParams.userId; 33 | var postId = $scope.detailPage = $routeParams.postId; 34 | var streamId = $scope.streamPage = $routeParams.streamId; 35 | 36 | if (hashtag) { 37 | $scope.feedsFilter = '#' + hashtag; 38 | } 39 | 40 | /** 41 | * Back for history 42 | * @return {Void} 43 | */ 44 | $scope.back = function() { 45 | history.go(-1); 46 | }; 47 | 48 | $scope.loadMore = function() { 49 | $scope.feedPage++; 50 | $scope.lastUpdated = 0; 51 | $scope.feed.push({spacer: true}); //spacer in the UI 52 | $scope.updateFeed({append: true}); 53 | }; 54 | 55 | /** 56 | * Function to update the feed on the client side 57 | * @param {Object} data The data received from endpoint 58 | * @return {Void} 59 | */ 60 | function doUpdate(data) { 61 | var options = data.config || {}; 62 | 63 | /** 64 | * If it's a filter request, emoty the feeds 65 | */ 66 | if ($scope.feedsFilter && !options.append) { 67 | $scope.feed = []; 68 | } 69 | /** 70 | * Check whether to append to feed (at bottom) or insert (at top) 71 | */ 72 | if (!options.append) { 73 | $scope.feed = data.res.records.concat($scope.feed); 74 | } else { 75 | $scope.feed = $scope.feed.concat(data.res.records); 76 | } 77 | 78 | /** 79 | * Check if there are more pages 80 | * @type {Boolean} 81 | */ 82 | $scope.noMorePosts = !data.res.morePages; 83 | /** 84 | * Set the updated timestamp 85 | */ 86 | $scope.lastUpdated = Date.now(); 87 | $scope.showBack = false; 88 | 89 | /** 90 | * Set page title 91 | */ 92 | if ($scope.timelinePage) { 93 | $scope.feedTitle = 'Timeline'; 94 | } else if ($scope.streamPage) { 95 | $scope.feedTitle = ''; 96 | } else if ($scope.detailPage) { 97 | $scope.feedTitle = 'Written by ' + $scope.feed[0].creator.name; 98 | } else { 99 | $scope.feedTitle = 'Lobby'; 100 | } 101 | } 102 | 103 | $scope.updateFeed = function(options, passedData) { 104 | var options = options || {}; 105 | appPostsFeed.getFeeds(angular.extend(options, $routeParams, { 106 | passedData: passedData, 107 | feedsFilter: $scope.feedsFilter, 108 | limitComments: $scope.limitComments, 109 | feedPage: $scope.feedPage, 110 | lastUpdated: $scope.lastUpdated 111 | }), function(response) { 112 | angular.extend($scope, response.config); 113 | doUpdate(response); 114 | }); 115 | }; 116 | 117 | /** 118 | * Initial feeds 119 | */ 120 | angular.extend($scope, resolvedFeeds.config); 121 | doUpdate(resolvedFeeds); 122 | 123 | 124 | /** 125 | * Check if feed needs to be filtered 126 | * If not, call $scope.updateFeed() anyway as first run 127 | */ 128 | var feedsFilterTimeout; 129 | $scope.$watch('feedsFilter', function(newValue, oldValue) { 130 | if (newValue !== oldValue) { 131 | $scope.feed = []; 132 | } 133 | 134 | $timeout.cancel(feedsFilterTimeout); 135 | feedsFilterTimeout = $timeout(function() { 136 | if (!newValue) { 137 | if ($scope.feedsFilterEnabled) { 138 | $scope.lastUpdated = 0; 139 | $scope.noPosting = false; 140 | $scope.updateFeed(); 141 | } 142 | } else { 143 | $scope.noPosting = true; 144 | $scope.updateFeed(); 145 | } 146 | $scope.feedsFilterEnabled = $scope.feedsFilter !== ''; 147 | }, 500); 148 | }); 149 | 150 | var updateNewCount = function(event, data) { 151 | /** 152 | * Main stream notification 153 | */ 154 | if (!data.streamId && !$route.current.params.streamId) { 155 | $scope.newFeedCount++; 156 | $scope.$digest(); 157 | } else { 158 | var currentStream = $route.current.params.streamId; 159 | if (currentStream && currentStream === data.streamId) { 160 | $scope.newFeedCount++; 161 | $scope.$digest(); 162 | } else { 163 | $rootScope.$broadcast('stream-message', data); 164 | } 165 | } 166 | }; 167 | 168 | /** 169 | * Update a single item in the existing list if it exists 170 | * @param {[type]} postId [description] 171 | * @return {[type]} [description] 172 | */ 173 | var updateItem = function(e, data) { 174 | _.each($scope.feed, function(candidate, i) { 175 | if (candidate._id == data.postId) { 176 | (function(item) { 177 | var params = { 178 | postId: data.postId 179 | }; 180 | if ($scope.detailPage && item._id === $routeParams.postId) { 181 | params.allowMarking = true; 182 | } 183 | if (item._id == data.postId) { 184 | var post = appPosts.single.get(params, function() { 185 | angular.extend(item, post.res.record); 186 | }); 187 | } 188 | })(candidate); 189 | } 190 | 191 | }); 192 | }; 193 | 194 | /** 195 | * Enable socket listeners 196 | */ 197 | $rootScope.$on('like', updateItem); 198 | $rootScope.$on('unlike', updateItem); 199 | $rootScope.$on('comment', updateItem); 200 | $rootScope.$on('feed', updateNewCount); 201 | 202 | /** 203 | * Reset the form 204 | * @return {Void} 205 | */ 206 | $scope.reset = function(frm) { 207 | $scope.content = ''; 208 | $timeout(function() { 209 | $scope.postForm.$setPristine(); 210 | $scope.postForm.$setUntouched(); 211 | }); 212 | }; 213 | 214 | /** 215 | * Search for mentions 216 | * @param {String} content The content in which to look for the string 217 | * @return {Void} 218 | */ 219 | var replaceCandidate = null; 220 | $scope.checkMentions = function(content, element) { 221 | if (!content) return; 222 | /** 223 | * Mention format will be @xyz, searching only end of text 224 | */ 225 | var re = /@([A-Za-z0-9_]+)$/g; 226 | 227 | /** 228 | * The length should never be more than 1 229 | * @type {Array} 230 | */ 231 | var mentions = content.match(re); 232 | 233 | if (mentions && mentions.length === 1 && mentions[0].length >= 4) { 234 | appUsersSearch(mentions[0].replace('@',''), false).then(function(response) { 235 | $scope.mentionsResults = response.res.items; 236 | }); 237 | } else { 238 | $scope.mentionsResults = []; 239 | } 240 | 241 | /** 242 | * Remember the element 243 | */ 244 | replaceCandidate = element; 245 | 246 | /** 247 | * Placement 248 | */ 249 | var elem = angular.element(replaceCandidate); 250 | angular.element('.mentions-results').css({top: elem.offset().top }); 251 | }; 252 | 253 | /** 254 | * Replace selected mentions 255 | * @param {String} username The selected item's username 256 | * @return {Void} 257 | */ 258 | $scope.replaceName = function(username) { 259 | var re = /@([A-Za-z0-9_]+)$/g; 260 | // $scope.content = $scope.content.replace(re, '@' + username); 261 | var elem = angular.element(replaceCandidate); 262 | elem.val(elem.val().replace(re, '@' + username) + ' '); 263 | $timeout(function() { 264 | elem.change(); 265 | elem.focus(); 266 | $scope.mentionsResults = []; 267 | }); 268 | }; 269 | 270 | /** 271 | * Create a new post 272 | * @param {Boolean} isValid Will be true if form validation passes 273 | * @return {Void} 274 | */ 275 | $scope.create = function(isValid, item) { 276 | if (isValid) { 277 | var post = new appPosts.single({ 278 | content: this.content, 279 | stream: streamId 280 | }); 281 | 282 | post.$save(function(response) { 283 | if (response.success) { 284 | 285 | /** 286 | * We are the creator ourselves, we know that 287 | * @type {Object} 288 | */ 289 | response.res = angular.extend(response.res, { 290 | creator: appAuth.getUser() 291 | }); 292 | 293 | /** 294 | * Update feed 295 | * @type {Object} 296 | */ 297 | $scope.updateFeed(); 298 | 299 | $scope.reset(); 300 | } else { 301 | $scope.failure = true; 302 | appToast(response.res.message); 303 | } 304 | }); 305 | } else { 306 | 307 | } 308 | }; 309 | 310 | 311 | } 312 | ]) 313 | ; 314 | -------------------------------------------------------------------------------- /modules/posts/public/posts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.posts', ['atwork.system']); -------------------------------------------------------------------------------- /modules/posts/public/posts.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.posts') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | $routeProvider 6 | .when('/feed', { 7 | templateUrl: '/modules/posts/views/feed.html', 8 | controller: 'PostsCtrl', 9 | resolve: { 10 | resolvedFeeds: resolvedFeeds({limitComments: true}) 11 | } 12 | }) 13 | .when('/', { 14 | templateUrl: '/modules/posts/views/feed.html', 15 | controller: 'PostsCtrl', 16 | resolve: { 17 | resolvedFeeds: resolvedFeeds({limitComments: true}) 18 | } 19 | }) 20 | .when('/post/:postId', { 21 | templateUrl: '/modules/posts/views/feed.html', 22 | controller: 'PostsCtrl', 23 | resolve: { 24 | resolvedFeeds: resolvedFeeds({limitComments: false}) 25 | } 26 | }) 27 | .when('/feed/:hashtag', { 28 | templateUrl: '/modules/posts/views/feed.html', 29 | controller: 'PostsCtrl', 30 | resolve: { 31 | resolvedFeeds: resolvedFeeds({limitComments: true}) 32 | } 33 | }) 34 | ; 35 | $locationProvider.html5Mode(true); 36 | }]); 37 | 38 | /** 39 | * Get configuration for resolved feeds to reuse in routes 40 | * @param {Object} params Contains parameters for the options 41 | * @return {Array} 42 | */ 43 | function resolvedFeeds(params) { 44 | return [ 45 | '$route', 46 | 'appPostsFeed', 47 | function($route, appPostsFeed) { 48 | var deferred = Q.defer(); 49 | var options = angular.extend({ 50 | limitComments: params.limitComments 51 | }, $route.current.params); 52 | 53 | appPostsFeed.getFeeds(options, function(response) { 54 | deferred.resolve(response); 55 | }); 56 | 57 | return deferred.promise; 58 | } 59 | ]; 60 | } -------------------------------------------------------------------------------- /modules/posts/public/posts.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.posts') 4 | .factory('appPosts', ['$resource', 5 | function($resource) { 6 | return { 7 | single: $resource('posts/:postId/:action', { 8 | postId: '@_id' 9 | }, { 10 | like: { 11 | method: 'POST', 12 | params: {action: 'like'} 13 | }, 14 | unlike: { 15 | method: 'POST', 16 | params: {action: 'unlike'} 17 | }, 18 | comment: { 19 | method: 'POST', 20 | params: {action: 'comment'} 21 | }, 22 | likes: { 23 | method: 'GET', 24 | params: {action: 'likes'} 25 | } 26 | }), 27 | feed: $resource('posts/'), 28 | stream: $resource('posts/stream/:streamId'), 29 | timeline: $resource('posts/timeline/:userId') 30 | } 31 | } 32 | ]) 33 | .filter('appPostFormat', [ 34 | '$sce', 35 | function($sce) { 36 | return function(text) { 37 | var hashtags = new RegExp('#([A-Za-z0-9]+)', 'g'); 38 | text = text.replace(hashtags, function(hashtag) { 39 | return '' + hashtag + '' 40 | }); 41 | 42 | var mentions = new RegExp('@([A-Za-z0-9_]+)', 'g'); 43 | text = text.replace(mentions, function(mention) { 44 | return '' + mention + '' 45 | }); 46 | 47 | /** 48 | * Emoticons 49 | */ 50 | var emots = [ 51 | { 52 | key: ':)', 53 | value: 'fa-smile-o' 54 | }, { 55 | key: ':|', 56 | value: 'fa-meh-o' 57 | }, { 58 | key: ':(', 59 | value: 'fa-frown-o' 60 | }, { 61 | key: '(y)', 62 | value: 'fa-thumbs-o-up' 63 | }, { 64 | key: '(n)', 65 | value: 'fa-thumbs-o-down' 66 | }, { 67 | key: ':+1', 68 | value: 'fa-thumbs-up' 69 | }, { 70 | key: '(h)', 71 | value: 'fa-heart' 72 | }, { 73 | key: '(i)', 74 | value: 'fa-lightbulb-o' 75 | }, 76 | ]; 77 | 78 | var emotTemplate = ''; 79 | for (var i in emots) { 80 | var key = emots[i].key; 81 | var value = emots[i].value; 82 | text = text.replace(key, emotTemplate.replace('{{emoticon}}', value)); 83 | }; 84 | 85 | return $sce.trustAsHtml(text); 86 | }; 87 | } 88 | ]) 89 | .factory('appPostsFeed', [ 90 | 'appPosts', 91 | function(appPosts) { 92 | return { 93 | getFeeds: function(options, cb) { 94 | options = options || {}; 95 | var userId = options.userId; 96 | var hashtag = options.hashtag; 97 | var postId = options.postId; 98 | var streamId = options.streamId; 99 | var passedData = options.passedData; 100 | 101 | /** 102 | * Configuration for the service 103 | * that will also be returned 104 | * @type {Object} 105 | */ 106 | var config = options; 107 | 108 | if (userId) { 109 | /** 110 | * TIMELINE: If there is a userId, let's load feeds of the specific user 111 | */ 112 | /** 113 | * Disable posting 114 | * @type {Boolean} 115 | */ 116 | config.noPosting = true; 117 | /** 118 | * Show limited comments 119 | * @type {Boolean} 120 | */ 121 | config.limitComments = true; 122 | 123 | /** 124 | * Prepare the request 125 | */ 126 | var timelineData = appPosts.timeline.get({ 127 | userId: userId, 128 | timestamp: config.lastUpdated, 129 | filter: config.feedsFilter, 130 | limitComments: config.limitComments, 131 | page: config.feedPage 132 | }, function() { 133 | doUpdate(timelineData); 134 | }); 135 | 136 | } else if (streamId) { 137 | /** 138 | * STREAM: If there is a streamId, let's load feeds of the specific stream 139 | */ 140 | 141 | /** 142 | * Show limited comments 143 | * @type {Boolean} 144 | */ 145 | config.limitComments = true; 146 | 147 | /** 148 | * Prepare the request 149 | */ 150 | var streamsData = appPosts.stream.get({ 151 | streamId: streamId, 152 | timestamp: config.lastUpdated, 153 | filter: config.feedsFilter, 154 | limitComments: config.limitComments, 155 | page: config.feedPage 156 | }, function() { 157 | doUpdate(streamsData); 158 | }); 159 | } else if (postId) { 160 | /** 161 | * SINGLE: If there is a postId, let's load a single feed 162 | */ 163 | /** 164 | * Disable filtering if its a single feed 165 | * @type {Boolean} 166 | */ 167 | config.noFiltering = true; 168 | /** 169 | * Disable posting 170 | * @type {Boolean} 171 | */ 172 | config.noPosting = true; 173 | /** 174 | * No load-more button 175 | * @type {Boolean} 176 | */ 177 | config.noMorePosts = true; 178 | /** 179 | * Get ready to show all comments 180 | */ 181 | delete config.limitComments; 182 | 183 | /** 184 | * Prepare the request 185 | */ 186 | var timelineData = appPosts.single.get({ 187 | postId: postId, 188 | limitComments: config.limitComments, 189 | allowMarking: true 190 | }, function() { 191 | /** 192 | * The retrieved record is the only one to show 193 | * @type {Array} 194 | */ 195 | timelineData.res.records = [timelineData.res.record]; 196 | doUpdate(timelineData); 197 | 198 | /** 199 | * Set the last updated timestamp 200 | */ 201 | config.lastUpdated = Date.now(); 202 | config.showBack = true; 203 | }); 204 | } else { 205 | /** 206 | * FEED: If there is no postId and no userId, let's load the user's latest feed 207 | */ 208 | /** 209 | * Limit comments 210 | * @type {Boolean} 211 | */ 212 | config.limitComments = true; 213 | 214 | /** 215 | * Prepare the request 216 | */ 217 | var feedData = appPosts.feed.get({ 218 | timestamp: config.lastUpdated, 219 | filter: config.feedsFilter, 220 | limitComments: config.limitComments, 221 | page: config.feedPage 222 | }, function() { 223 | doUpdate(feedData); 224 | }); 225 | } 226 | 227 | /** 228 | * If data was sent to the function directly 229 | * update it for faster client side updates 230 | */ 231 | if (passedData) { 232 | doUpdate(passedData); 233 | } 234 | 235 | /** 236 | * Default feedcount to 0 237 | * @type {Number} 238 | */ 239 | config.newFeedCount = 0; 240 | 241 | /** 242 | * Function to update the feed on the client side 243 | * @param {Object} data The data received from endpoint 244 | * @return {Void} 245 | */ 246 | function doUpdate(data) { 247 | config.lastUpdated = Date.now(); 248 | data.config = config; 249 | cb(data); 250 | } 251 | 252 | } 253 | } 254 | } 255 | ]) 256 | .directive('awFeedItem', [ 257 | 'appPosts', 258 | 'appWebSocket', 259 | 'appAuth', 260 | 'appDialog', 261 | function(appPosts, appWebSocket, appAuth, appDialog) { 262 | return { 263 | templateUrl: '/modules/posts/views/post-single.html', 264 | controller: [ 265 | '$scope', 266 | function($scope) { 267 | 268 | /** 269 | * Like the post 270 | * @param {Object} item The item object 271 | * @return {Void} 272 | */ 273 | $scope.doLike = function(item) { 274 | item.liked = true; 275 | appPosts.single.like(item, function(response) { 276 | angular.extend(item, response.res.record); 277 | }); 278 | }; 279 | 280 | /** 281 | * Unlike the post 282 | * @param {Object} item The item object 283 | * @return {Void} 284 | */ 285 | $scope.undoLike = function(item) { 286 | item.liked = false; 287 | appPosts.single.unlike(item, function(response) { 288 | angular.extend(item, response.res.record); 289 | }); 290 | }; 291 | 292 | /** 293 | * Comment on a post 294 | * @param {Boolean} isValid Will be true if form validation passes 295 | * @return {Void} 296 | */ 297 | $scope.comment = function(isValid, item) { 298 | if (isValid) { 299 | var commentContent = this.content; 300 | 301 | /** 302 | * Enable client side comments update for faster response time 303 | */ 304 | item.commentEnabled = false; 305 | item.comments.unshift({ 306 | creator: appAuth.getUser(), 307 | content: commentContent 308 | }); 309 | 310 | item.comment = commentContent; 311 | 312 | appPosts.single.comment(item, function(response) { 313 | angular.extend(item, response.res.record); 314 | item.commentEnabled = false; 315 | }); 316 | 317 | } 318 | }; 319 | 320 | /** 321 | * Show the list of likers for a specific post 322 | * @param {Object} item The post item 323 | * @return {Void} 324 | */ 325 | $scope.showLikers = function(ev, item) { 326 | /** 327 | * Get the likers 328 | */ 329 | appPosts.single.likes({ 330 | postId: item._id 331 | }, function(response) { 332 | /** 333 | * Show dialog 334 | */ 335 | appDialog.show({ 336 | controller: [ 337 | '$scope', 338 | 'appDialog', 339 | function($scope, appDialog) { 340 | /** 341 | * Assign likers to the users variable 342 | * @type {Array} 343 | */ 344 | $scope.users = response.res.records; 345 | /** 346 | * Hide the dialog 347 | * @return {Void} 348 | */ 349 | $scope.hide = function() { 350 | appDialog.hide(); 351 | }; 352 | } 353 | ], 354 | templateUrl: '/modules/users/views/users-dialog.html', 355 | targetEvent: ev, 356 | }); 357 | }); 358 | }; 359 | 360 | } 361 | ] 362 | } 363 | } 364 | ]) 365 | ; 366 | -------------------------------------------------------------------------------- /modules/posts/public/views/feed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |
5 | 6 | Back 7 | 8 |
9 |

...

10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
Write your post first.
24 |
That's too long!
25 |
That's too short. Write more?
26 |
27 |
28 | 29 | 30 | Share 31 | 32 | 33 | Cancel 34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | {{item.name}} 45 |
46 |

{{item.name}}

47 |

@{{item.username}}

48 |

{{item.designation}}

49 |

{{item.email}}

50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | Filter 60 | 61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 | Load New Items ({{newFeedCount}}) 69 | 70 | 71 |

Loading...

72 | 73 |
74 | Loaded more below 75 |
76 | 77 |
78 | 79 |
80 |
81 | Load More 82 |
83 |

-- end --

84 | 85 |
86 |
87 |
-------------------------------------------------------------------------------- /modules/posts/public/views/post-single.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{item.creator.name}} 4 |
5 | {{::item.creator.name}} 6 |

7 |
8 | 13 | - 14 | 15 | Comment 16 | 17 | - 18 | 19 | View 20 | 21 | - 22 | 23 | $ {{item.stream.title}} 24 | 25 | 26 | {{::item.created | date:'medium'}} 27 | 28 |
29 | {{::item.created | date:'medium'}} 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 |
39 |
Write your post first.
40 |
That's too long!
41 |
That's too short. Write more?
42 |
43 |
44 | 45 | 46 | Share 47 | 48 | 49 | Cancel 50 | 51 |
52 |
53 | 54 |
55 | 56 | 57 | {{comment.creator.name}} 58 |
59 | {{::comment.creator.name}} 60 |

61 | {{::comment.created | date:'medium'}} 62 |
63 |
64 | 65 | More Comments ({{::item.hasMoreComments}}) 66 | 67 |
68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 |
-------------------------------------------------------------------------------- /modules/posts/server/main.js: -------------------------------------------------------------------------------- 1 | var routes = require('./routes/posts'); 2 | 3 | module.exports = function(System) { 4 | /** 5 | * Name of this module 6 | * @type {String} 7 | */ 8 | var moduleName = 'posts'; 9 | 10 | /** 11 | * Build the routes 12 | * @type {Array} 13 | */ 14 | var builtRoutes = routes(System); 15 | 16 | /** 17 | * Add routes to System 18 | */ 19 | System.route(builtRoutes, moduleName); 20 | }; -------------------------------------------------------------------------------- /modules/posts/server/models/posts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'); 10 | 11 | /** 12 | * Getter 13 | */ 14 | var escapeProperty = function(value) { 15 | return _.escape(value); 16 | }; 17 | 18 | /** 19 | * Post Schema 20 | */ 21 | 22 | var PostSchema = new Schema({ 23 | created: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | creator: { 28 | type: Schema.ObjectId, 29 | required: true, 30 | ref: 'User' 31 | }, 32 | content: { 33 | type: String, 34 | required: true, 35 | get: escapeProperty 36 | }, 37 | comments: [{ 38 | created: { 39 | type: Date, 40 | default: Date.now 41 | }, 42 | content: { 43 | type: String, 44 | required: true, 45 | get: escapeProperty 46 | }, 47 | creator: { 48 | type: Schema.ObjectId, 49 | required: true, 50 | ref: 'User' 51 | } 52 | }], 53 | stream: { 54 | type: Schema.ObjectId, 55 | required: false, 56 | ref: 'Stream' 57 | }, 58 | subscribers: [{ 59 | type: Schema.ObjectId, 60 | required: false, 61 | ref: 'User' 62 | }], 63 | likes: [{ 64 | type: Schema.ObjectId, 65 | required: false, 66 | ref: 'User' 67 | }], 68 | liked: { 69 | type: Boolean, 70 | default: false 71 | }, 72 | hasMoreComments: { 73 | type: Number, 74 | default: 0 75 | } 76 | }); 77 | 78 | /** 79 | * Methods 80 | */ 81 | PostSchema.methods = { 82 | /** 83 | * Hide security sensitive fields 84 | * 85 | * @returns {*|Array|Binary|Object} 86 | */ 87 | toJSON: function() { 88 | var obj = this.toObject(); 89 | if (obj.creator) { 90 | delete obj.creator.token; 91 | delete obj.creator.hashed_password; 92 | delete obj.creator.salt; 93 | delete obj.creator.following; 94 | } 95 | if (obj.likes) { 96 | obj.likeCount = obj.likes.length; 97 | } else { 98 | obj.likeCount = 0; 99 | } 100 | return obj; 101 | }, 102 | afterSave: function(user, limitComments) { 103 | var obj = this; 104 | obj.liked = obj.likes.indexOf(user._id) != -1; 105 | if (limitComments && obj.comments && obj.comments.length > 3) { 106 | obj.hasMoreComments = obj.comments.length - 3; 107 | obj.comments = obj.comments.slice(0, 3); 108 | } 109 | return obj; 110 | }, 111 | getMentionedUsers: function(cb) { 112 | /** 113 | * Mention format will be @xyz 114 | */ 115 | var re = /@([A-Za-z0-9_]+)/g; 116 | 117 | /** 118 | * Try to find the usernames 119 | * @type {Array} 120 | */ 121 | var usernames = this.content.match(re); 122 | 123 | if (!usernames || !usernames.length) { 124 | return []; 125 | } 126 | 127 | /** 128 | * Remove the '@' symbol 129 | */ 130 | usernames.map(function(username, i) { 131 | usernames[i] = username.substring(1); 132 | }); 133 | 134 | /** 135 | * Find in the db 136 | */ 137 | var User = mongoose.model('User'); 138 | 139 | User.find({username: {$in: usernames} }) 140 | .exec(function(err, users) { 141 | if (cb) { 142 | cb(err, users); 143 | } 144 | }); 145 | }, 146 | subscribe: function(userId) { 147 | if (this.subscribers.indexOf(userId) === -1 && this._id !== userId) { //cannot subscribe to own post 148 | this.subscribers.push(userId); 149 | } 150 | }, 151 | notifyUsers: function(data, System) { 152 | 153 | var notification = { 154 | postId: this._id, 155 | actorId: data.actorId, 156 | notificationType: data.type, 157 | config: data.config 158 | }; 159 | this.populate('creator subscribers', function(err, post) { 160 | post.subscribers.map(function(user) { 161 | /** 162 | * Ignore creator, because we have a special call for that later 163 | */ 164 | if (user._id.toString() === post.creator._id.toString()) { 165 | return; 166 | } 167 | /** 168 | * Ignore the person taking this action 169 | */ 170 | if (user._id.toString() === data.actorId.toString()) { 171 | return; 172 | } 173 | /** 174 | * Notify 175 | */ 176 | user.notify(notification, System); 177 | }); 178 | 179 | /** 180 | * Notify creator, if its not the creator taking this action 181 | */ 182 | if (post.creator._id.toString() !== data.actorId.toString()) { 183 | post.creator.notify(notification, System); 184 | } 185 | }); 186 | } 187 | }; 188 | 189 | mongoose.model('Post', PostSchema); 190 | -------------------------------------------------------------------------------- /modules/posts/server/routes/posts.js: -------------------------------------------------------------------------------- 1 | var myController = require('../controllers/posts'); 2 | /** 3 | * Init the controller 4 | */ 5 | module.exports = function(System) { 6 | var posts = myController(System); 7 | 8 | var routes = []; 9 | 10 | routes.push({ 11 | method: 'post', 12 | path: '/', 13 | handler: posts.create, 14 | authorized: true 15 | }); 16 | 17 | routes.push({ 18 | method: 'get', 19 | path: '/', 20 | handler: posts.feed, 21 | authorized: true 22 | }); 23 | 24 | routes.push({ 25 | method: 'get', 26 | path: '/timeline/:userId', 27 | handler: posts.timeline, 28 | authorized: true 29 | }); 30 | 31 | routes.push({ 32 | method: 'get', 33 | path: '/stream/:streamId', 34 | handler: posts.streamPosts, 35 | authorized: true 36 | }); 37 | 38 | routes.push({ 39 | method: 'get', 40 | path: '/:postId', 41 | handler: posts.single, 42 | authorized: true 43 | }); 44 | 45 | routes.push({ 46 | method: 'post', 47 | path: '/:postId/like', 48 | handler: posts.like, 49 | authorized: true 50 | }); 51 | 52 | routes.push({ 53 | method: 'post', 54 | path: '/:postId/comment', 55 | handler: posts.comment, 56 | authorized: true 57 | }); 58 | 59 | routes.push({ 60 | method: 'get', 61 | path: '/:postId/likes', 62 | handler: posts.likes, 63 | authorized: true 64 | }); 65 | 66 | routes.push({ 67 | method: 'post', 68 | path: '/:postId/unlike', 69 | handler: posts.unlike, 70 | authorized: true 71 | }); 72 | 73 | return routes; 74 | }; -------------------------------------------------------------------------------- /modules/streams/public/streams.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.streams') 4 | .controller('StreamsPurposeCtrl', [ 5 | '$scope', 6 | '$rootScope', 7 | '$routeParams', 8 | '$timeout', 9 | 'appAuth', 10 | 'appToast', 11 | 'appStorage', 12 | 'appLocation', 13 | 'appWebSocket', 14 | 'appStreams', 15 | function($scope, $rootScope, $routeParams, $timeout, appAuth, appToast, appStorage, appLocation, appWebSocket, appStreams) { 16 | var streamId = $routeParams.streamId; 17 | 18 | $scope.getStream = function (streamId) { 19 | var stream = appStreams.single.get({streamId: streamId}, function() { 20 | $scope.stream = stream.res.record; 21 | $scope.stream.purpose = $scope.stream.purpose || 'Set the stream\'s purpose here...' 22 | }); 23 | }; 24 | 25 | $scope.updateStreamPurpose = function (isValid) { 26 | var stream = appStreams.single.get({streamId: streamId}, function() { 27 | stream = angular.extend(stream, $scope.stream); 28 | stream.$save(function(response) { 29 | appToast('Stream purpose updated.'); 30 | }); 31 | }); 32 | }; 33 | 34 | if (streamId) { 35 | $scope.getStream(streamId); 36 | } 37 | } 38 | ]) 39 | .controller('StreamsCtrl', [ 40 | '$scope', 41 | '$rootScope', 42 | '$routeParams', 43 | '$timeout', 44 | 'appAuth', 45 | 'appToast', 46 | 'appStorage', 47 | 'appLocation', 48 | 'appWebSocket', 49 | 'appStreams', 50 | function($scope, $rootScope, $routeParams, $timeout, appAuth, appToast, appStorage, appLocation, appWebSocket, appStreams) { 51 | $scope.streams = []; 52 | $scope.actions = {}; 53 | // $scope.toSubscribe = ''; 54 | 55 | $scope.clearThis = function(item) { 56 | console.log($scope); 57 | console.log(item); 58 | $timeout(function() { 59 | // $scope.toSubscribe = undefined; 60 | }, 2000); 61 | }; 62 | 63 | $scope.processMoreStreams = function(selected) { 64 | $timeout(function() { 65 | if (selected === '1') { 66 | $scope.createNew(); 67 | // $scope.toSubscribe = undefined; 68 | } else { 69 | var selectedStreamData = appStreams.single.get({streamId: selected}, function() { 70 | selectedStreamData.$subscribe({streamId: selected}, function() { 71 | $scope.updateStreams({reload: true}); 72 | appToast('You have subscribed to the new stream.'); 73 | appLocation.url('/stream/' + selected); 74 | // $scope.toSubscribe = undefined; 75 | }); 76 | }); 77 | } 78 | }, 500); 79 | }; 80 | 81 | /** 82 | * Unsubscribe from a specific stream 83 | * @return {Void} 84 | */ 85 | $scope.unsubscribe = function(stream) { 86 | var streamId = stream._id; 87 | var selectedStreamData = appStreams.single.get({streamId: streamId}, function() { 88 | selectedStreamData.$unsubscribe({streamId: streamId}, function() { 89 | $scope.updateStreams({reload: true}); 90 | appToast('You have unsubscribed from that stream.'); 91 | }); 92 | }); 93 | }; 94 | 95 | $scope.updateStreams = function (options) { 96 | options = options || {}; 97 | 98 | var streamsData = appStreams.single.get({subscribed: true}, function() { 99 | /** 100 | * Check if the feed needs to reload 101 | */ 102 | if (options.reload) { 103 | $scope.streams = []; 104 | } 105 | 106 | /** 107 | * Check whether to append to feed (at bottom) or insert (at top) 108 | */ 109 | if (!options.append) { 110 | $scope.streams = streamsData.res.records.concat($scope.streams); 111 | } else { 112 | $scope.streams = $scope.streams.concat(streamsData.res.records); 113 | } 114 | /** 115 | * Check if there are more pages 116 | * @type {Boolean} 117 | */ 118 | $scope.noMoreStreams = !streamsData.res.morePages; 119 | /** 120 | * Set the updated timestamp 121 | */ 122 | $scope.lastUpdated = Date.now(); 123 | }); 124 | 125 | var moreStreamsData = appStreams.single.get({unsubscribed: true}, function() { 126 | $scope.moreStreams = moreStreamsData.res.records; 127 | }); 128 | }; 129 | 130 | $scope.createNew = function () { 131 | $scope.actions.createNew = true; 132 | }; 133 | 134 | /** 135 | * Create a new post 136 | * @param {Boolean} isValid Will be true if form validation passes 137 | * @return {Void} 138 | */ 139 | $scope.create = function (isValid) { 140 | if (isValid) { 141 | var stream = new appStreams.single({ 142 | title: this.newStreamName 143 | }); 144 | stream.$save(function(response) { 145 | if (response.success) { 146 | appWebSocket.conn.emit('stream', response.res._id); 147 | $scope.actions.createNew = false; 148 | $scope.updateStreams({reload: true}); 149 | appLocation.url('/stream/' + response.res._id); 150 | } else { 151 | $scope.failure = true; 152 | appToast(response.res.message); 153 | } 154 | }); 155 | } else { 156 | appToast('Bummer! Is the stream name good?'); 157 | } 158 | }; 159 | 160 | /** 161 | * If a stream has a new msg, show a badge 162 | */ 163 | $rootScope.$on('stream-message', function(e, data) { 164 | angular.forEach($scope.streams, function(stream) { 165 | if (data.streamId === stream._id) { 166 | stream.unread = stream.unread ? stream.unread++ : 1; 167 | } 168 | }); 169 | $scope.$digest(); 170 | }); 171 | 172 | /** 173 | * Clear the stream's badge 174 | * @param {Object} stream The stream object 175 | * @return {Void} 176 | */ 177 | $scope.clearBadge = function(stream) { 178 | stream.unread = 0; 179 | }; 180 | 181 | /** 182 | * Listen to socket 183 | */ 184 | appWebSocket.conn.on('stream', function() { 185 | appToast('Woot! There is a new stream available!'); 186 | $scope.updateStreams({reload: true}); 187 | }); 188 | 189 | /** 190 | * Get the initial list 191 | */ 192 | $scope.updateStreams(); 193 | } 194 | ]) 195 | ; 196 | -------------------------------------------------------------------------------- /modules/streams/public/streams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.streams', ['atwork.system']); -------------------------------------------------------------------------------- /modules/streams/public/streams.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.streams') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | $routeProvider 6 | .when('/stream/:streamId', { 7 | templateUrl: '/modules/posts/views/feed.html', 8 | controller: 'PostsCtrl', 9 | resolve: { 10 | resolvedFeeds: [ 11 | '$route', 12 | 'appPostsFeed', 13 | function($route, appPostsFeed) { 14 | var deferred = Q.defer(); 15 | var options = angular.extend({ 16 | limitComments: true 17 | }, $route.current.params); 18 | 19 | appPostsFeed.getFeeds(options, function(response) { 20 | deferred.resolve(response); 21 | }); 22 | 23 | return deferred.promise; 24 | } 25 | ] 26 | } 27 | }) 28 | ; 29 | $locationProvider.html5Mode(true); 30 | }]); -------------------------------------------------------------------------------- /modules/streams/public/streams.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.streams') 4 | .factory('appStreams', ['$resource', 5 | function($resource) { 6 | return { 7 | single: $resource('streams/:streamId/:action', { 8 | streamId: '@_id' 9 | }, { 10 | subscribe: { 11 | method: 'POST', 12 | params: {action: 'subscribe'} 13 | }, 14 | unsubscribe: { 15 | method: 'POST', 16 | params: {action: 'unsubscribe'} 17 | } 18 | }) 19 | } 20 | } 21 | ]) 22 | ; 23 | -------------------------------------------------------------------------------- /modules/streams/public/views/streamsList.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Streams 4 |

5 | 6 | 40 |

Loading...

41 |
-------------------------------------------------------------------------------- /modules/streams/server/controllers/streams.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Stream = mongoose.model('Stream'); 3 | 4 | module.exports = function(System) { 5 | var obj = {}; 6 | var json = System.plugins.JSON; 7 | var event = System.plugins.event; 8 | var sck = System.webSocket; 9 | 10 | /** 11 | * Stream related sockets 12 | */ 13 | sck.on('connection', function(socket){ 14 | socket.on('stream', function(streamId) { 15 | socket.broadcast.emit('stream', streamId); 16 | }); 17 | }); 18 | 19 | /** 20 | * Create a stream 21 | * @param {Object} req The request object 22 | * @param {Object} res The response object 23 | * @return {Void} 24 | */ 25 | obj.create = function (req, res) { 26 | var stream = new Stream(req.body); 27 | stream.creator = req.user._id; 28 | stream.subscribers.push(req.user._id); 29 | 30 | stream.save(function(err) { 31 | if (err) { 32 | return json.unhappy(err, res); 33 | } 34 | return json.happy(stream, res); 35 | }); 36 | }; 37 | 38 | /** 39 | * Modify a stream 40 | * @param {Object} req The request object 41 | * @param {Object} res The response object 42 | * @return {Void} 43 | */ 44 | obj.modify = function (req, res) { 45 | Stream.findOne({ 46 | _id: req.params.streamId 47 | }) 48 | .populate('creator') 49 | .exec(function(err, stream) { 50 | if (err) { 51 | return json.unhappy(err, res); 52 | } 53 | stream.purpose = req.body.purpose; 54 | stream.title = req.body.title || stream.title; 55 | stream.save(function(err) { 56 | if (err) { 57 | return json.unhappy(err, res); 58 | } 59 | return json.happy(stream, res); 60 | }); 61 | }); 62 | }; 63 | 64 | /** 65 | * List all streams 66 | * @param {Object} req The request object 67 | * @param {Object} res The response object 68 | * @return {Void} 69 | */ 70 | obj.list = function (req, res) { 71 | var user = req.user; 72 | 73 | var criteria = {}; 74 | 75 | /** 76 | * Do we want only subscribed streams? 77 | */ 78 | if (req.query.subscribed) { 79 | criteria.subscribers = req.user._id; 80 | } 81 | 82 | /** 83 | * Do we want only unsubscribed streams? 84 | */ 85 | if (req.query.unsubscribed) { 86 | criteria.subscribers = {$ne: req.user._id}; 87 | } 88 | 89 | Stream.find(criteria, null, {sort: {title: 1}}) 90 | .populate('creator') 91 | .skip(parseInt(req.query.page) * System.config.settings.perPage) 92 | .limit(System.config.settings.perPage+1) 93 | .exec(function(err, streams) { 94 | if (err) { 95 | json.unhappy(err, res); 96 | } else { 97 | var morePages = System.config.settings.perPage < streams.length; 98 | if (morePages) { 99 | streams.pop(); 100 | } 101 | json.happy({ 102 | records: streams, 103 | morePages: morePages 104 | }, res); 105 | } 106 | }); 107 | }; 108 | 109 | /** 110 | * Return info about single stream 111 | * @param {Object} req The req object 112 | * @param {Object} res The res object 113 | * @return {Void} 114 | */ 115 | obj.single = function(req, res) { 116 | Stream.findOne({ 117 | _id: req.params.streamId 118 | }) 119 | .populate('creator') 120 | .exec(function(err, stream) { 121 | if (err) { 122 | return json.unhappy(err, res); 123 | } else if (stream) { 124 | 125 | return json.happy({ 126 | record: stream 127 | }, res); 128 | } else { 129 | return json.unhappy({message: 'Stream not found'}, res); 130 | } 131 | }); 132 | }; 133 | 134 | /** 135 | * Subscribe to a stream 136 | * @param {Object} req The request object 137 | * @param {Object} res The response object 138 | * @return {Void} 139 | */ 140 | obj.subscribe = function (req, res) { 141 | Stream.findOne({_id: req.params.streamId}) 142 | .populate('creator') 143 | .exec(function(err, stream) { 144 | if (err) { 145 | return json.unhappy(err, res); 146 | } else if (stream) { 147 | if (stream.subscribers.indexOf(req.user._id) !== -1) { 148 | return json.unhappy('You have already subscribers to the group', res); 149 | } 150 | stream.subscribe(req.user._id); 151 | stream.save(function(err, item) { 152 | if (err) { 153 | return json.unhappy(err, res); 154 | } 155 | json.happy({ 156 | record: item 157 | }, res); 158 | }); 159 | 160 | } else { 161 | return json.unhappy({message: 'Stream not found'}, res); 162 | } 163 | }); 164 | }; 165 | 166 | /** 167 | * Unsubscribe to a stream 168 | * @param {Object} req The request object 169 | * @param {Object} res The response object 170 | * @return {Void} 171 | */ 172 | obj.unsubscribe = function (req, res) { 173 | Stream.findOne({_id: req.params.streamId}) 174 | .populate('creator') 175 | .exec(function(err, stream) { 176 | if (err) { 177 | return json.unhappy(err, res); 178 | } else if (stream) { 179 | if (stream.subscribers.indexOf(req.user._id) !== -1) { 180 | stream.subscribers.splice(stream.subscribers.indexOf(req.user._id), 1); 181 | } else { 182 | return json.unhappy('You have ubsubscribed', res); 183 | } 184 | 185 | stream.save(function(err, item) { 186 | if (err) { 187 | return json.unhappy(err, res); 188 | } 189 | json.happy({ 190 | record: item 191 | }, res); 192 | }); 193 | 194 | } else { 195 | return json.unhappy({message: 'Stream not found'}, res); 196 | } 197 | }); 198 | }; 199 | 200 | /** 201 | * Remove the stream 202 | * @param {Object} req The request object 203 | * @param {Object} res The response object 204 | * @return {Void} 205 | */ 206 | obj.remove = function (req, res) { 207 | 208 | }; 209 | 210 | return obj; 211 | }; -------------------------------------------------------------------------------- /modules/streams/server/main.js: -------------------------------------------------------------------------------- 1 | var routes = require('./routes/streams'); 2 | 3 | module.exports = function(System) { 4 | /** 5 | * Name of this module 6 | * @type {String} 7 | */ 8 | var moduleName = 'streams'; 9 | 10 | /** 11 | * Build the routes 12 | * @type {Array} 13 | */ 14 | var builtRoutes = routes(System); 15 | 16 | /** 17 | * Add routes to System 18 | */ 19 | System.route(builtRoutes, moduleName); 20 | }; -------------------------------------------------------------------------------- /modules/streams/server/models/streams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'); 10 | 11 | /** 12 | * Getter 13 | */ 14 | var escapeProperty = function(value) { 15 | return _.escape(value); 16 | }; 17 | 18 | /** 19 | * Post Schema 20 | */ 21 | 22 | var StreamsSchema = new Schema({ 23 | created: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | creator: { 28 | type: Schema.ObjectId, 29 | required: true, 30 | ref: 'User' 31 | }, 32 | title: { 33 | type: String, 34 | required: true, 35 | get: escapeProperty, 36 | match: [/^[a-zA-Z0-9_]*$/, 'Oh no! Only numbers and letters for stream names.'] 37 | }, 38 | purpose: { 39 | type: String, 40 | get: escapeProperty 41 | }, 42 | subscribers: [{ 43 | type: Schema.ObjectId, 44 | required: false, 45 | ref: 'User' 46 | }] 47 | }); 48 | 49 | /** 50 | * Methods 51 | */ 52 | StreamsSchema.methods = { 53 | /** 54 | * Hide security sensitive fields 55 | * 56 | * @returns {*|Array|Binary|Object} 57 | */ 58 | toJSON: function() { 59 | var obj = this.toObject(); 60 | if (obj.creator) { 61 | delete obj.creator.token; 62 | delete obj.creator.hashed_password; 63 | delete obj.creator.salt; 64 | delete obj.creator.following; 65 | } 66 | if (obj.likes) { 67 | obj.likeCount = obj.likes.length; 68 | } else { 69 | obj.likeCount = 0; 70 | } 71 | return obj; 72 | }, 73 | subscribe: function(userId) { 74 | if (this.subscribers.indexOf(userId) === -1 && this._id !== userId) { //cannot subscribe to own post 75 | this.subscribers.push(userId); 76 | } 77 | }, 78 | notifyUsers: function(data, System) { 79 | 80 | var notification = { 81 | postId: data.postId, 82 | actorId: data.actorId, 83 | notificationType: data.type 84 | }; 85 | this.populate('creator subscribers', function(err, stream) { 86 | stream.subscribers.map(function(user) { 87 | /** 88 | * Ignore creator, because we have a special call for that later 89 | */ 90 | if (user._id.toString() === stream.creator._id.toString()) { 91 | return; 92 | } 93 | /** 94 | * Ignore the person taking this action 95 | */ 96 | if (user._id.toString() === data.actorId.toString()) { 97 | return; 98 | } 99 | /** 100 | * Notify 101 | */ 102 | user.notify(notification, System); 103 | }); 104 | 105 | /** 106 | * Notify creator, if its not the creator taking this action 107 | */ 108 | if (stream.creator._id.toString() !== data.actorId.toString()) { 109 | stream.creator.notify(notification, System); 110 | } 111 | }); 112 | } 113 | }; 114 | 115 | mongoose.model('Stream', StreamsSchema); 116 | -------------------------------------------------------------------------------- /modules/streams/server/routes/streams.js: -------------------------------------------------------------------------------- 1 | var myController = require('../controllers/streams'); 2 | /** 3 | * Init the controller 4 | */ 5 | module.exports = function(System) { 6 | var streams = myController(System); 7 | 8 | var routes = []; 9 | 10 | routes.push({ 11 | method: 'post', 12 | path: '/', 13 | handler: streams.create, 14 | authorized: true 15 | }); 16 | 17 | routes.push({ 18 | method: 'get', 19 | path: '/', 20 | handler: streams.list, 21 | authorized: true 22 | }); 23 | 24 | routes.push({ 25 | method: 'get', 26 | path: '/:streamId', 27 | handler: streams.single, 28 | authorized: true 29 | }); 30 | 31 | routes.push({ 32 | method: 'post', 33 | path: '/:streamId', 34 | handler: streams.modify, 35 | authorized: true 36 | }); 37 | 38 | routes.push({ 39 | method: 'post', 40 | path: '/:streamId/subscribe', 41 | handler: streams.subscribe, 42 | authorized: true 43 | }); 44 | 45 | routes.push({ 46 | method: 'post', 47 | path: '/:streamId/unsubscribe', 48 | handler: streams.unsubscribe, 49 | authorized: true 50 | }); 51 | 52 | routes.push({ 53 | method: 'delete', 54 | path: '/:streamId', 55 | handler: streams.remove, 56 | authorized: true 57 | }); 58 | 59 | return routes; 60 | }; -------------------------------------------------------------------------------- /modules/users/public/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.users', ['atwork.system']) 4 | .factory('appAuth', [ 5 | '$http', 6 | 'appStorage', 7 | function($http, appStorage) { 8 | return { 9 | isLoggedIn: function() { 10 | return appStorage.get('userToken'); 11 | }, 12 | getToken: function() { 13 | return appStorage.get('userToken'); 14 | }, 15 | refreshUser: function(cb) { 16 | /** 17 | * FIXME: convert this to an ngResource call 18 | */ 19 | $http.get('/users/me').success(function(response) { 20 | var serializedUser = angular.toJson(response.res.record); 21 | appStorage.set('user', serializedUser); 22 | cb(response.res.record); 23 | }); 24 | }, 25 | getUser: function() { 26 | var serialized = appStorage.get('user'); 27 | if (serialized) { 28 | return angular.fromJson(serialized); 29 | } else { 30 | return { 31 | name: 'unknown' 32 | }; 33 | } 34 | } 35 | } 36 | } 37 | ]); 38 | -------------------------------------------------------------------------------- /modules/users/public/users.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.users') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | $routeProvider 6 | .when('/login', { 7 | templateUrl: '/modules/users/views/login.html?v', 8 | controller: 'LoginCtrl' 9 | }) 10 | .when('/logout', { 11 | templateUrl: '/modules/users/views/login.html?v', 12 | controller: 'LogoutCtrl' 13 | }) 14 | .when('/activate/:userId/:activationCode', { 15 | templateUrl: '/modules/users/views/activating.html', 16 | controller: 'ActivationCtrl' 17 | }) 18 | .when('/changePassword/:userId/:activationCode', { 19 | templateUrl: '/modules/users/views/change-password.html', 20 | controller: 'PasswordCtrl' 21 | }) 22 | .when('/profile/:userId/change-password', { 23 | templateUrl: '/modules/users/views/change-password.html', 24 | controller: 'ProfileCtrl', 25 | resolve: { 26 | profileData: [ 27 | '$route', 28 | 'appAuth', 29 | 'appUsers', 30 | function($route, appAuth, appUsers) { 31 | var routeParams = $route.current.params; 32 | var userId = routeParams.userId || appAuth.getUser()._id; 33 | return appUsers.single.get({userId: userId}).$promise; 34 | } 35 | ], 36 | resolvedFeeds: [ 37 | '$route', 38 | 'appPostsFeed', 39 | function($route, appPostsFeed) { 40 | var deferred = Q.defer(); 41 | var options = angular.extend({ 42 | feedPage: 0 43 | }, $route.current.params); 44 | 45 | appPostsFeed.getFeeds(options, function(response) { 46 | deferred.resolve(response); 47 | }); 48 | 49 | return deferred.promise; 50 | } 51 | ] 52 | } 53 | }) 54 | .when('/profile/:userId', { 55 | templateUrl: '/modules/users/views/profile.html?v', 56 | controller: 'ProfileCtrl', 57 | resolve: { 58 | profileData: [ 59 | '$route', 60 | 'appAuth', 61 | 'appUsers', 62 | function($route, appAuth, appUsers) { 63 | var routeParams = $route.current.params; 64 | var userId = routeParams.userId || appAuth.getUser()._id; 65 | return appUsers.single.get({userId: userId}).$promise; 66 | } 67 | ], 68 | resolvedFeeds: [ 69 | '$route', 70 | 'appPostsFeed', 71 | function($route, appPostsFeed) { 72 | var deferred = Q.defer(); 73 | var options = angular.extend({ 74 | feedPage: 0 75 | }, $route.current.params); 76 | 77 | appPostsFeed.getFeeds(options, function(response) { 78 | deferred.resolve(response); 79 | }); 80 | 81 | return deferred.promise; 82 | } 83 | ] 84 | } 85 | }) 86 | .when('/me', { 87 | templateUrl: '/modules/users/views/profile.html?v', 88 | controller: 'ProfileCtrl' 89 | }) 90 | ; 91 | $locationProvider.html5Mode(true); 92 | }]); -------------------------------------------------------------------------------- /modules/users/public/users.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.users') 4 | .factory('appUsers', ['$resource', 5 | function($resource) { 6 | return { 7 | single: $resource('users/:userId/:action', { 8 | userId: '@_id' 9 | }, { 10 | update: { 11 | method: 'PUT' 12 | }, 13 | follow: { 14 | method: 'POST', 15 | params: {action: 'follow'} 16 | }, 17 | unfollow: { 18 | method: 'POST', 19 | params: {action: 'unfollow'} 20 | }, 21 | activate: { 22 | method: 'POST', 23 | params: {action: 'activate'} 24 | }, 25 | invite: { 26 | method: 'POST', 27 | params: {action: 'invite'} 28 | }, 29 | resetPassword: { 30 | method: 'POST', 31 | params: {action: 'resetPassword'} 32 | } 33 | }), 34 | auth: $resource('users/authenticate'), 35 | notifications: $resource('users/notifications/:notificationId') 36 | } 37 | } 38 | ]) 39 | .factory('appUsersSearch', [ 40 | '$resource', 41 | function($resource) { 42 | var search = $resource('users/search/:keyword', {}, {query: {isArray: false}}); 43 | return function(keyword, onlyUsernames) { 44 | //implement search logic here 45 | var criteria = {keyword: keyword}; 46 | if (onlyUsernames) { 47 | criteria.onlyUsernames = true; 48 | } 49 | var promise = search.query(criteria).$promise; 50 | return promise; 51 | }; 52 | } 53 | ]) 54 | .factory('follow'); -------------------------------------------------------------------------------- /modules/users/public/views/activating.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Activating...

4 |
5 |
6 | -------------------------------------------------------------------------------- /modules/users/public/views/change-password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Please wait...

4 |
5 | 6 |

New Password

7 |
8 | 9 | 10 | 11 |
12 |
This is required!
13 |
That's too long!
14 |
That's too short!
15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
This is required!
23 |
That's too long!
24 |
That's too short!
25 |
26 |
27 | 28 | Change 29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /modules/users/public/views/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{systemSettings.tagline}}

4 |

Becoming a member is quick & easy.

5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 |
This is required!
13 |
That's too long!
14 |
That's too short!
15 |
16 |
17 | 18 | 19 | 20 | 21 |
22 |
This is required!
23 |
That's too long!
24 |
That's too short!
25 |
26 |
27 | 28 | 29 | Login 30 | 31 |
32 | Forgot Password? 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 |
41 |
This is required!
42 |
That's too long!
43 |
That's too short!
44 |
45 |
46 | 47 | 48 | 49 | 50 |
51 |
This is required!
52 |
That's too long!
53 |
That's too short!
54 |
55 |
56 | 57 | 58 | 59 | 60 |
61 |
This is required!
62 |
That's too long!
63 |
That's too short!
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 |
This is required!
72 |
That's too long!
73 |
That's too short!
74 |
75 |
76 | 77 | 78 | 79 | 80 |
81 |
This is required!
82 |
That's too long!
83 |
That's too short!
84 |
85 |
86 | 87 | 88 | 89 | 90 |
91 |
This is required!
92 |
That's too long!
93 |
That's too short!
94 |
95 |
96 | 97 | Register 98 | 99 |
100 |
101 | 102 | 103 |

104 | 105 | Awesome! 106 |

107 |

As last step of the registration process, please check your email to activate your account and login.

108 |
109 |

Didn't get an email yet?

110 | Resend Email 111 |
112 | 113 |
114 |
115 |
116 |
117 | -------------------------------------------------------------------------------- /modules/users/public/views/profile.html: -------------------------------------------------------------------------------- 1 |
2 | 142 | 7 | 8 | {{ item.name }} 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /modules/users/public/views/users-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Likers 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{item.name}} 11 |
12 |

{{item.name}}

13 |

@{{item.username}}

14 |

{{item.designation}}

15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | Close 27 | 28 |
29 |
-------------------------------------------------------------------------------- /modules/users/public/views/users-invite-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Invite 4 | 5 |
6 |
7 | 8 | 9 | 10 |
11 |
This is required!
12 |
That's too long!
13 |
That's too short!
14 |
15 |
16 | 17 | 18 | 19 | 20 |
21 |
This is required!
22 |
That's too long!
23 |
That's too short!
24 |
25 |
26 | 27 | 28 | 29 | 30 |
31 |
That's too long!
32 |
33 |
34 | 35 | 36 | Send Invite 37 | 38 |
39 | 40 |
41 |

42 | 43 | Awesome! 44 |

45 |

Your invitation is on its way!

46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 | 54 | Close 55 | 56 |
57 |
-------------------------------------------------------------------------------- /modules/users/public/views/users-pwd-dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Reset Password 4 | 5 |
6 |
7 | 8 | 9 | 10 |
11 |
This is required!
12 |
That's too long!
13 |
That's too short!
14 |
15 |
16 | 17 | 18 | Next 19 | 20 |
21 | 22 |
23 |

24 | 25 | Awesome! 26 |

27 |

Check your email for password reset instructions.

28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | Close 37 | 38 |
39 |
-------------------------------------------------------------------------------- /modules/users/server/main.js: -------------------------------------------------------------------------------- 1 | var routes = require('./routes/users'); 2 | 3 | module.exports = function(System) { 4 | /** 5 | * Name of this module 6 | * @type {String} 7 | */ 8 | var moduleName = 'users'; 9 | 10 | /** 11 | * Build the routes 12 | * @type {Array} 13 | */ 14 | var builtRoutes = routes(System); 15 | 16 | /** 17 | * Add routes to System 18 | */ 19 | System.route(builtRoutes, moduleName); 20 | }; -------------------------------------------------------------------------------- /modules/users/server/models/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'), 10 | fs = require('fs'); 11 | 12 | /** 13 | * Validations 14 | */ 15 | var validatePresenceOf = function(value) { 16 | // If you are authenticating by any of the oauth strategies, don't validate. 17 | return (this.provider && this.provider !== 'local') || (value && value.length); 18 | }; 19 | 20 | var validateUniqueEmail = function(value, callback) { 21 | var User = mongoose.model('User'); 22 | User.find({ 23 | $and: [{ 24 | email: new RegExp('^' + value + '$', 'i') 25 | }, { 26 | _id: { 27 | $ne: this._id 28 | } 29 | }] 30 | }, function(err, user) { 31 | callback(err || user.length === 0); 32 | }); 33 | }; 34 | 35 | /** 36 | * Getter 37 | */ 38 | var escapeProperty = function(value) { 39 | return _.escape(value); 40 | }; 41 | 42 | /** 43 | * Regexp to validate email format 44 | * @type {RegExp} 45 | */ 46 | var emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 47 | 48 | /** 49 | * User Schema 50 | */ 51 | 52 | var UserSchema = new Schema({ 53 | created: { 54 | type: Date, 55 | default: Date.now 56 | }, 57 | name: { 58 | type: String, 59 | required: true, 60 | get: escapeProperty 61 | }, 62 | username: { 63 | type: String, 64 | required: true, 65 | unique: true, 66 | get: escapeProperty, 67 | match: [/^\w+$/, 'Please enter only alphanumeric values for username'] 68 | }, 69 | email: { 70 | type: String, 71 | required: true, 72 | unique: true, 73 | // Regexp to validate emails with more strict rules as added in tests/users.js which also conforms mostly with RFC2822 guide lines 74 | match: [emailRE, 'Please enter a valid email'], 75 | validate: [validateUniqueEmail, 'E-mail address is already in-use'] 76 | }, 77 | designation: { 78 | type: String, 79 | required: false, 80 | get: escapeProperty 81 | }, 82 | face: { 83 | type: String 84 | }, 85 | roles: { 86 | type: Array, 87 | default: ['authenticated'] 88 | }, 89 | hashed_password: { 90 | type: String, 91 | validate: [validatePresenceOf, 'Password cannot be blank'] 92 | }, 93 | provider: { 94 | type: String, 95 | default: 'local' 96 | }, 97 | following: [{ 98 | type: Schema.ObjectId, 99 | required: true, 100 | ref: 'User' 101 | }], 102 | socketId: { 103 | type: String, 104 | default: '' 105 | }, 106 | onlineStatus: { 107 | type: Boolean, 108 | default: false 109 | }, 110 | loggedIn: { 111 | type: Boolean, 112 | default: false 113 | }, 114 | notifications: [{ 115 | post: { 116 | type: Schema.ObjectId, 117 | ref: 'Post' 118 | }, 119 | user: { 120 | type: Schema.ObjectId, 121 | ref: 'User' 122 | }, 123 | actor: { 124 | type: Schema.ObjectId, 125 | ref: 'User' 126 | }, 127 | created: { 128 | type: Date, 129 | default: Date.now 130 | }, 131 | notificationType: String, 132 | unread: { 133 | type: Boolean, 134 | default: true 135 | } 136 | }], 137 | salt: String, 138 | token: String, 139 | resetPasswordToken: String, 140 | resetPasswordExpires: Date, 141 | facebook: {}, 142 | twitter: {}, 143 | github: {}, 144 | google: {}, 145 | linkedin: {}, 146 | activationCode: { 147 | type: String 148 | }, 149 | active: { 150 | type: Boolean, 151 | default: false 152 | } 153 | }); 154 | 155 | /** 156 | * Virtuals 157 | */ 158 | UserSchema.virtual('password').set(function(password) { 159 | this._password = password; 160 | this.salt = this.makeSalt(); 161 | this.hashed_password = this.hashPassword(password); 162 | this.activationCode = Date.now().toString().substr(4, 4) + Date.now().toString().substr(6, 4) + Date.now().toString(); 163 | }).get(function() { 164 | return this._password; 165 | }); 166 | 167 | /** 168 | * Pre-save hook 169 | */ 170 | UserSchema.pre('save', function(next) { 171 | if (this.isNew && this.provider === 'local' && this.password && !this.password.length) 172 | return next(new Error('Invalid password')); 173 | next(); 174 | }); 175 | 176 | /** 177 | * Methods 178 | */ 179 | UserSchema.methods = { 180 | 181 | /** 182 | * HasRole - check if the user has required role 183 | * 184 | * @param {String} plainText 185 | * @return {Boolean} 186 | * @api public 187 | */ 188 | hasRole: function(role) { 189 | var roles = this.roles; 190 | return roles.indexOf('admin') !== -1 || roles.indexOf(role) !== -1; 191 | }, 192 | 193 | /** 194 | * IsAdmin - check if the user is an administrator 195 | * 196 | * @return {Boolean} 197 | * @api public 198 | */ 199 | isAdmin: function() { 200 | return this.roles.indexOf('admin') !== -1; 201 | }, 202 | 203 | /** 204 | * Authenticate - check if the passwords are the same 205 | * 206 | * @param {String} plainText 207 | * @return {Boolean} 208 | * @api public 209 | */ 210 | authenticate: function(plainText) { 211 | return this.hashPassword(plainText) === this.hashed_password; 212 | }, 213 | 214 | /** 215 | * Send notification to this user 216 | * @param {Object} data The data containing notification infp 217 | * @param {Object} System The core system object 218 | * @return {Void} 219 | */ 220 | notify: function(data, System) { 221 | data = data || {}; 222 | data.config = data.config || {}; 223 | 224 | /** 225 | * Save a ref to self 226 | * @type {Object} 227 | */ 228 | var thisUser = this; 229 | 230 | /** 231 | * If notifications is not an object (array), initialize it 232 | */ 233 | if (!thisUser.notifications || typeof thisUser.notifications !== 'object') { 234 | thisUser.notifications = []; 235 | } 236 | 237 | /** 238 | * Load the user model 239 | * @type {Object} 240 | */ 241 | var User = mongoose.model('User'); 242 | 243 | /** 244 | * Set a ref to notifications plugin 245 | * @type {Object} 246 | */ 247 | var notifications = System.plugins.notifications; 248 | 249 | /** 250 | * Do the actual notification 251 | * This will be called after populating required fields in the data 252 | * @param {Object} fullData Populated object containing actor and user data 253 | * @return {Void} 254 | */ 255 | var doNotify = function (fullData) { 256 | /** 257 | * If socketId is enabled, send a push 258 | */ 259 | if (thisUser.socketId) { 260 | //get total unread count 261 | var unread = thisUser.notifications.filter(function(item) { 262 | return item.unread; 263 | }).length; 264 | fullData.unread = unread; 265 | notifications.send(thisUser.socketId, fullData); 266 | 267 | console.log(thisUser.name, 'is notified in the browser.'); 268 | 269 | } 270 | 271 | /** 272 | * If socketId is not enabled, send an email 273 | */ 274 | if (!thisUser.socketId && !fullData.config.avoidEmail) { 275 | console.log(thisUser.name, 'is notified via email.'); 276 | // 'Hi ' + user.name + ', you\'ve got a new notification on AtWork!

Check it out here: ' + 'View' // html body 277 | 278 | var msg = ''; 279 | 280 | switch (fullData.notificationType) { 281 | case 'like': 282 | msg = fullData.actor.name + ' has liked a post'; 283 | break; 284 | 285 | case 'comment': 286 | msg = fullData.actor.name + ' has commented on a post'; 287 | break; 288 | 289 | case 'follow': 290 | msg = fullData.actor.name + ' is now following you'; 291 | break; 292 | 293 | case 'mention': 294 | msg = fullData.actor.name + ' mentioned you in a post'; 295 | 296 | case 'chatMessage': 297 | msg = fullData.actor.name + ' sent you this message: ' + (fullData.chatMessage ? fullData.chatMessage.message : ''); 298 | break; 299 | } 300 | 301 | System.plugins.emailing.generate({ 302 | name: thisUser.name, 303 | message: msg, 304 | action: fullData.postId ? 'View Post' : 'View Profile', 305 | href: fullData.postId ? System.config.baseURL + '/post/' + fullData.postId : System.config.baseURL + '/profile/' + fullData.actor.username 306 | }, function(html) { 307 | fullData.html = html; 308 | notifications.sendByEmail(thisUser, fullData); 309 | }); 310 | } 311 | }; 312 | 313 | /** 314 | * Populate the actor 315 | */ 316 | User.findOne({_id: data.actorId}).exec(function (err, actor) { 317 | data.actor = actor; 318 | doNotify(data); 319 | }); 320 | 321 | /** 322 | * Add the notification data to the user 323 | */ 324 | if (!data.config.systemLevel) { 325 | thisUser.notifications.push({ 326 | post: data.postId, 327 | user: data.userId, 328 | actor: data.actorId, 329 | notificationType: data.notificationType 330 | }); 331 | } 332 | 333 | /** 334 | * Sort all notifications in order 335 | */ 336 | thisUser.notifications.sort(function(a, b) { 337 | var dt1 = new Date(a.created); 338 | var dt2 = new Date(b.created); 339 | if (dt1 > dt2) { 340 | return -1; 341 | } else { 342 | return 1; 343 | } 344 | }); 345 | 346 | /** 347 | * Save the current user 348 | */ 349 | return thisUser.save(function(err, user) { 350 | return user; 351 | }); 352 | }, 353 | 354 | /** 355 | * Send a notification to all followers 356 | * @param {Object} data The notification data 357 | * @param {Object} System The core system object 358 | * @return {Void} 359 | */ 360 | notifyFollowers: function(data, System) { 361 | var User = mongoose.model('User'); 362 | User.find({following: this._id}, function(err, followers) { 363 | followers.map(function(follower) { 364 | follower.notify(data, System); 365 | }); 366 | }); 367 | }, 368 | 369 | /** 370 | * Make salt 371 | * 372 | * @return {String} 373 | * @api public 374 | */ 375 | makeSalt: function() { 376 | return crypto.randomBytes(16).toString('base64'); 377 | }, 378 | 379 | /** 380 | * Hash password 381 | * 382 | * @param {String} password 383 | * @return {String} 384 | * @api public 385 | */ 386 | hashPassword: function(password) { 387 | if (!password || !this.salt) return ''; 388 | var salt = new Buffer(this.salt, 'base64'); 389 | return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); 390 | }, 391 | 392 | /** 393 | * Hide security sensitive fields 394 | * 395 | * @returns {*|Array|Binary|Object} 396 | */ 397 | toJSON: function() { 398 | var obj = this.toObject(); 399 | obj.onlineStatus = obj.socketId ? true : false; 400 | delete obj.socketId; 401 | delete obj.hashed_password; 402 | delete obj.notifications; 403 | delete obj.salt; 404 | delete obj.token; 405 | delete obj.following; 406 | return obj; 407 | } 408 | }; 409 | 410 | mongoose.model('User', UserSchema); 411 | -------------------------------------------------------------------------------- /modules/users/server/routes/users.js: -------------------------------------------------------------------------------- 1 | var myController = require('../controllers/users'); 2 | /** 3 | * Init the controller 4 | */ 5 | module.exports = function(System) { 6 | var users = myController(System); 7 | 8 | var routes = []; 9 | 10 | routes.push({ 11 | method: 'post', 12 | path: '/', 13 | handler: users.create 14 | }); 15 | 16 | routes.push({ 17 | method: 'post', 18 | path: '/authenticate', 19 | handler: users.authenticate 20 | }); 21 | 22 | routes.push({ 23 | method: 'get', 24 | path: '/me', 25 | handler: users.me, 26 | authorized: true 27 | }); 28 | 29 | routes.push({ 30 | method: 'get', 31 | path: '/', 32 | handler: users.list, 33 | authorized: true 34 | }); 35 | 36 | routes.push({ 37 | method: 'post', 38 | path: '/:userId/avatar', 39 | handler: users.avatar, 40 | authorized: true 41 | }); 42 | 43 | routes.push({ 44 | method: 'post', 45 | path: '/:userId/activate', 46 | handler: users.activate, 47 | authorized: false //allow anonymous 48 | }); 49 | 50 | routes.push({ 51 | method: 'post', 52 | path: '/resetPassword', 53 | handler: users.resetPassword, 54 | authorized: false //allow anonymous 55 | }); 56 | 57 | routes.push({ 58 | method: 'post', 59 | path: '/:userId/invite', 60 | handler: users.invite, 61 | authorized: true 62 | }); 63 | 64 | routes.push({ 65 | method: 'post', 66 | path: '/:userId/follow', 67 | handler: users.follow, 68 | authorized: true 69 | }); 70 | 71 | routes.push({ 72 | method: 'post', 73 | path: '/:userId/unfollow', 74 | handler: users.unfollow, 75 | authorized: true 76 | }); 77 | 78 | routes.push({ 79 | method: 'get', 80 | path: '/notifications', 81 | handler: users.notifications, 82 | authorized: true 83 | }); 84 | 85 | routes.push({ 86 | method: 'get', 87 | path: '/notifications/:notificationId', 88 | handler: users.markRead, 89 | authorized: true 90 | }); 91 | 92 | routes.push({ 93 | method: 'get', 94 | path: '/:userId', 95 | handler: users.single, 96 | authorized: true 97 | }); 98 | 99 | routes.push({ 100 | method: 'put', 101 | path: '/', 102 | handler: users.modify, 103 | authorized: true 104 | }); 105 | 106 | routes.push({ 107 | method: 'get', 108 | path: '/search/:keyword', 109 | handler: users.search, 110 | authorized: true 111 | }); 112 | 113 | return routes; 114 | }; -------------------------------------------------------------------------------- /modules/users/server/tests/users.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Load Prerequisites 5 | */ 6 | var expect = require('chai').expect, 7 | mongoose = require('mongoose'), 8 | User = mongoose.model('User') 9 | ; 10 | 11 | /** 12 | * This will hold all temporarily added records, which will be deleted at the end of the tests 13 | * @type {Object} 14 | */ 15 | var temps = {}; 16 | 17 | describe('', function() { 18 | describe('Model User:', function() { 19 | beforeEach(function(done) { 20 | temps = {}; 21 | /** 22 | * Create a new test user 23 | * @type {User} 24 | */ 25 | temps.user = new User({ 26 | name: 'John Doe', 27 | email: 'test@asdf.com', 28 | username: 'test123', 29 | password: 'password', 30 | provider: 'local', 31 | roles: ['authenticated'] 32 | }); 33 | 34 | done(); 35 | }); 36 | 37 | /** 38 | * Save comment 39 | */ 40 | describe('Method Save', function() { 41 | 42 | it('should be able to save a new user', function(done) { 43 | temps.user.save(function(err, user) { 44 | expect(user.name).to.be.equal('John Doe'); 45 | done(); 46 | }); 47 | }); 48 | 49 | }); 50 | 51 | 52 | /** 53 | * Clean up 54 | */ 55 | afterEach(function(done) { 56 | var remove = function(item, ok) { 57 | if (item && typeof item.remove === "function") { 58 | item.remove(ok); 59 | } else { 60 | ok(); 61 | } 62 | }; 63 | remove(temps.user, function() { 64 | done(); 65 | }); 66 | }); 67 | 68 | }); 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AtWork", 3 | "version": "0.0.1", 4 | "description": "An open-source social network for your workplace", 5 | "repository": "https://github.com/matterial/atwork", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node index", 9 | "dev": "node index", 10 | "karma": "node node_modules/karma/bin/karma start karma.conf.js", 11 | "mocha": "./node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- ./modules/**/server/tests/**/*.js -R spec -r tools/test/mocha-req.js", 12 | "test": "npm run mocha && npm run karma", 13 | "postinstall": "./node_modules/bower/bin/bower install" 14 | }, 15 | "author": "Riten Vagadiya", 16 | "license": "MIT", 17 | "dependencies": { 18 | "async": "^0.9.2", 19 | "aws-sdk": "^2.1.28", 20 | "body-parser": "^1.11.0", 21 | "bower": "^1.3.12", 22 | "ejs": "^2.3.1", 23 | "express": "^4.11.2", 24 | "jsonwebtoken": "^3.2.2", 25 | "lodash": "^3.9.3", 26 | "mongoose": "3.8", 27 | "morgan": "^1.5.1", 28 | "multer": "^0.1.7", 29 | "nodemailer": "^1.3.2", 30 | "socket.io": "^1.3.5" 31 | }, 32 | "devDependencies": { 33 | "chai": "^1.10.0", 34 | "expect": "^1.6.0", 35 | "gulp": "^3.8.10", 36 | "gulp-concat": "^2.5.2", 37 | "gulp-minify-css": "^1.1.3", 38 | "gulp-sass": "^1.3.2", 39 | "gulp-uglify": "^1.2.0", 40 | "istanbul": "^0.3.7", 41 | "karma": "^0.12.31", 42 | "karma-coverage": "^0.2.7", 43 | "karma-mocha": "^0.1.10", 44 | "karma-phantomjs-launcher": "^0.1.4", 45 | "mocha": "^2.1.0", 46 | "phantomjs": "^1.9.15" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('AtWork', [ 2 | 'atwork.system', 3 | 'atwork.users', 4 | 'atwork.posts', 5 | 'atwork.streams', 6 | 'atwork.chats', 7 | 'atwork.activities', 8 | 'atwork.notifications', 9 | 'atwork.settings', 10 | 'ngMaterial']); 11 | 12 | app.controller('AppCtrl', [ 13 | '$scope', 14 | '$route', 15 | '$rootScope', 16 | '$mdSidenav', 17 | '$mdBottomSheet', 18 | '$location', 19 | '$timeout', 20 | 'appLocation', 21 | 'appAuth', 22 | 'appWebSocket', 23 | 'appSettings', 24 | 'appSettingsValid', 25 | 'appToast', 26 | function($scope, $route, $rootScope, $mdSidenav, $mdBottomSheet, $location, $timeout, appLocation, appAuth, appWebSocket, appSettings, appSettingsValid, appToast) { 27 | $scope.barTitle = ''; 28 | $scope.search = ''; 29 | 30 | $scope.toggleSidenav = function(menuId) { 31 | $mdSidenav(menuId).toggle(); 32 | }; 33 | 34 | $scope.updateLoginStatus = function() { 35 | $scope.isLoggedIn = appAuth.isLoggedIn(); 36 | $scope.user = appAuth.getUser(); 37 | }; 38 | 39 | $scope.goHome = function() { 40 | appLocation.url('/'); 41 | }; 42 | 43 | $scope.showUserActions = function($event) { 44 | $mdBottomSheet.show({ 45 | templateUrl: '/modules/users/views/user-list.html', 46 | controller: 'UserSheet', 47 | targetEvent: $event 48 | }).then(function(clickedItem) { 49 | $scope.alert = clickedItem.name + ' clicked!'; 50 | }); 51 | }; 52 | 53 | var initiateSettings = function(cb) { 54 | appSettings.fetch(function(settings) { 55 | $rootScope.systemSettings = settings; 56 | if (cb) { 57 | cb(); 58 | } 59 | }); 60 | }; 61 | 62 | /** 63 | * Scroll the view to top on route change 64 | */ 65 | $scope.$on('$routeChangeSuccess', function() { 66 | angular.element('*[md-scroll-y]').animate({scrollTop: 0}, 300); 67 | $mdSidenav('left').close(); 68 | }); 69 | 70 | $scope.$on('loggedIn', function() { 71 | $scope.updateLoginStatus(); 72 | $scope.barTitle = ''; 73 | $scope.$broadcast('updateNotifications'); 74 | appWebSocket.conn.emit('online', {token: appAuth.getToken()}); 75 | appAuth.refreshUser(function(user) { 76 | $scope.user = user; 77 | }); 78 | /** 79 | * Fetch settings and get the app ready 80 | */ 81 | initiateSettings(function() { 82 | $scope.$on('$routeChangeStart', function (event, toState) { 83 | var valid = appSettingsValid(); 84 | if (!valid) { 85 | appToast('Please complete the setup first.'); 86 | } 87 | }); 88 | $scope.appReady = true; 89 | $scope.barTitle = $rootScope.systemSettings.tagline; 90 | $timeout(appSettingsValid); 91 | }); 92 | 93 | }); 94 | 95 | $scope.$on('loggedOut', function() { 96 | $scope.updateLoginStatus(); 97 | appWebSocket.conn.emit('logout', {token: appAuth.getToken()}); 98 | }); 99 | 100 | appWebSocket.conn.on('connect', function() { 101 | if (appAuth.isLoggedIn()) { 102 | appWebSocket.conn.emit('online', {token: appAuth.getToken()}); 103 | } 104 | }); 105 | 106 | $scope.updateLoginStatus(); 107 | $timeout(function() { 108 | if (!appAuth.isLoggedIn()) { 109 | if (window.location.href.indexOf('/activate/') == -1 && window.location.href.indexOf('/changePassword/') == -1) { 110 | appLocation.url('/login'); 111 | } 112 | initiateSettings(); 113 | $scope.appReady = true; 114 | } else { 115 | $scope.barTitle = ''; 116 | $scope.$broadcast('loggedIn'); 117 | } 118 | 119 | }); 120 | } 121 | ]); -------------------------------------------------------------------------------- /public/images/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritenv/atwork/3259d509ad56e994d9181e2a4f227ac879601e8f/public/images/preloader.gif -------------------------------------------------------------------------------- /public/images/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritenv/atwork/3259d509ad56e994d9181e2a4f227ac879601e8f/public/images/spacer.gif -------------------------------------------------------------------------------- /public/images/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ritenv/atwork/3259d509ad56e994d9181e2a4f227ac879601e8f/public/images/user.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |

Flying

18 |

Hold on, tightly...

19 |
20 |
21 | 22 | 23 | 26 |

{{systemSettings ? systemSettings.workplace : 'AtWork'}}

27 |
28 | 37 | 38 |
39 |
40 | 41 | 42 |
43 | {{ badges.messageBadge }} 44 | 47 | 48 | 49 | 50 | 51 | {{systemSettings ? systemSettings.workplace : 'AtWork'}} 52 |   {{barTitle}} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | 62 | {{notificationCount}} 63 | 64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | {{item.actor.name}} 72 |
73 |

{{::item.display.text}}

74 | {{::item.created | date:'medium'}} 75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

No unread notifications.

87 |
88 |
89 |
90 |
91 | 92 |
93 |
94 |
95 | 96 | 99 | 100 |
101 |
102 | 103 | 104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /system/config/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | console.log('Config in development'); 3 | module.exports = { 4 | REQUESTS_DELAY: 0, 5 | REQUESTS_DELAY_SYSTEM: 0, 6 | baseURL: 'http://localhost:8111', 7 | db: process.env.MONGOHQ_URL || 'mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR || 'localhost') + '/atwork', 8 | server: { 9 | host: 'localhost', 10 | port: 8111 11 | }, 12 | secret: 'atworksecret', 13 | settings: { 14 | perPage: 10, 15 | email: { 16 | service: 'Gmail' 17 | } 18 | }, 19 | aws: { 20 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 21 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 22 | } 23 | }; -------------------------------------------------------------------------------- /system/config/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | REQUESTS_DELAY: 0, 5 | REQUESTS_DELAY_SYSTEM: 0, 6 | baseURL: (process.env.BASEURL || 'http://atwork.riten.io'), 7 | db: 'mongodb://localhost:27017/' + (process.env.DB || 'atwork'), 8 | server: { 9 | host: 'localhost', 10 | port: process.env.PORT || 8111 11 | }, 12 | secret: 'atworksecret', 13 | settings: { 14 | perPage: 10, 15 | email: { 16 | service: 'Gmail' 17 | } 18 | }, 19 | aws: { 20 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 21 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY 22 | } 23 | }; -------------------------------------------------------------------------------- /system/helpers/auth.js: -------------------------------------------------------------------------------- 1 | var ensureAuthorized = function(req, res, next) { 2 | var mongoose = require('mongoose'); 3 | var User = mongoose.model('User'); 4 | 5 | var bearerToken; 6 | var bearerHeader = req.headers["authorization"]; 7 | if (typeof bearerHeader !== 'undefined') { 8 | var bearer = bearerHeader.split(" "); 9 | bearerToken = bearer[1]; 10 | req.token = bearerToken; 11 | //populate({path: 'following', select: 'name email'}). 12 | 13 | User.findOne({token: req.token}) 14 | .populate('following') 15 | .exec(function(err, user) { 16 | if (err || !user) { 17 | return res.sendStatus(403); 18 | } 19 | req.user = user; 20 | next(); 21 | }); 22 | } else { 23 | res.sendStatus(403); 24 | } 25 | }; 26 | 27 | var justGetUser = function(req, res, next) { 28 | var mongoose = require('mongoose'); 29 | var User = mongoose.model('User'); 30 | var bearerToken; 31 | var bearerHeader = req.headers["authorization"]; 32 | if (typeof bearerHeader !== 'undefined') { 33 | var bearer = bearerHeader.split(" "); 34 | bearerToken = bearer[1]; 35 | req.token = bearerToken; 36 | //populate({path: 'following', select: 'name email'}). 37 | 38 | User.findOne({token: req.token}).exec(function(err, user) { 39 | if (user) { 40 | req.user = user; 41 | } 42 | next(); 43 | }); 44 | } 45 | }; 46 | 47 | module.exports = function(System) { 48 | var plugin = { 49 | /** 50 | * The helper register method 51 | * @return {Void} 52 | */ 53 | register: function () { 54 | return { 55 | ensureAuthorized: ensureAuthorized, 56 | justGetUser: justGetUser 57 | }; 58 | } 59 | }; 60 | 61 | /** 62 | * Attributes to identify the plugin 63 | * @type {Object} 64 | */ 65 | plugin.register.attributes = { 66 | name: 'Auth Helper', 67 | key: 'auth', 68 | version: '1.1.0' 69 | }; 70 | return plugin; 71 | }; -------------------------------------------------------------------------------- /system/helpers/emailing.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var _ = require('lodash'); 3 | 4 | /** 5 | * Unhappy JSON response 6 | * @param {Error} err The error object 7 | * @return {Object} The json response to be output 8 | */ 9 | var generate = function(options, cb) { 10 | var emailTemplate = fs.readFile(options.template ? options.template : (__dirname + '/../public/templates/notification.html'), function(err, fileContent) { 11 | var ejs = require('ejs'); 12 | var html = ejs.render(fileContent.toString(), options); 13 | cb(html); 14 | }); 15 | }; 16 | 17 | module.exports = function(System) { 18 | var plugin = { 19 | /** 20 | * The helper register method 21 | * @return {Void} 22 | */ 23 | register: function () { 24 | return { 25 | generate: function(options, cb) { 26 | options = _.extend(options, System.settings); 27 | return generate(options, cb); 28 | } 29 | }; 30 | } 31 | }; 32 | 33 | /** 34 | * Attributes to identify the plugin 35 | * @type {Object} 36 | */ 37 | plugin.register.attributes = { 38 | name: 'Email Helper', 39 | key: 'emailing', 40 | version: '1.0.0' 41 | }; 42 | return plugin; 43 | }; -------------------------------------------------------------------------------- /system/helpers/events.js: -------------------------------------------------------------------------------- 1 | var registeredEvents = {}; 2 | var registerType = function(e) { 3 | if (typeof registeredEvents[e] !== 'object' || registeredEvents[e].constructor.name !== 'Array') { 4 | registeredEvents[e] = []; 5 | } 6 | }; 7 | var isRegisteredEvent = function(e) { 8 | return (typeof registeredEvents[e] === 'object' && registeredEvents[e].constructor.name === 'Array'); 9 | }; 10 | var registerEvent = function(e, cb) { 11 | registeredEvents[e].push(cb); 12 | }; 13 | var triggerEvent = function(e, args) { 14 | registeredEvents[e].map(function(cb) { 15 | cb(args); 16 | }); 17 | }; 18 | module.exports = function(System) { 19 | var plugin = { 20 | /** 21 | * The helper register method 22 | * @return {Void} 23 | */ 24 | register: function () { 25 | return { 26 | on: function(e, cb, args) { 27 | registerType(e); 28 | registerEvent(e, cb, args); 29 | }, 30 | trigger: function(e, args) { 31 | if (isRegisteredEvent(e)) { 32 | triggerEvent(e, args); 33 | } 34 | } 35 | }; 36 | } 37 | }; 38 | 39 | /** 40 | * Attributes to identify the plugin 41 | * @type {Object} 42 | */ 43 | plugin.register.attributes = { 44 | name: 'Events Helper', 45 | key: 'event', 46 | version: '1.0.0' 47 | }; 48 | return plugin; 49 | }; -------------------------------------------------------------------------------- /system/helpers/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Happy JSON response 3 | * @param {Object} obj The info object 4 | * @return {Object} The json response to be output 5 | */ 6 | var happy = function(obj, res) { 7 | res.send({ 8 | success: 1, 9 | res: obj 10 | }); 11 | }; 12 | 13 | var list = function(obj, res) { 14 | res.send(obj); 15 | }; 16 | 17 | /** 18 | * Unhappy JSON response 19 | * @param {Error} err The error object 20 | * @return {Object} The json response to be output 21 | */ 22 | var unhappy = function(err, res) { 23 | var obj = { 24 | success: 0, 25 | res: err 26 | }; 27 | if (obj.res.errors) { 28 | obj.res.messages = []; 29 | for (var i in obj.res.errors) { 30 | obj.res.messages.push(obj.res.errors[i].message); 31 | } 32 | obj.res.message = obj.res.messages[0]; 33 | } 34 | res.send(obj); 35 | }; 36 | 37 | 38 | 39 | module.exports = function(System) { 40 | var plugin = { 41 | /** 42 | * The helper register method 43 | * @return {Void} 44 | */ 45 | register: function () { 46 | return { 47 | happy: happy, 48 | list: list, 49 | unhappy: unhappy 50 | }; 51 | } 52 | }; 53 | 54 | /** 55 | * Attributes to identify the plugin 56 | * @type {Object} 57 | */ 58 | plugin.register.attributes = { 59 | name: 'JSON Helper', 60 | key: 'JSON', 61 | version: '1.0.0' 62 | }; 63 | return plugin; 64 | }; -------------------------------------------------------------------------------- /system/helpers/notifications.js: -------------------------------------------------------------------------------- 1 | module.exports = function(System) { 2 | var sck = System.webSocket; 3 | var plugin = { 4 | /** 5 | * The helper register method 6 | * @return {Void} 7 | */ 8 | register: function () { 9 | return { 10 | send: function(socketId, data) { 11 | console.log('NOTIFICATION FOR:', socketId); 12 | var type = data.config.systemLevel ? 'system' : 'notification'; 13 | sck.to(socketId).emit(type, data); 14 | }, 15 | sendByEmail: function(user, data) { 16 | var mailOptions = { 17 | from: data.actor.name + ' (' + data.notificationType + ')' + ' via ' + System.settings.workplace + ' <'+ System.settings.email +'>', // sender address 18 | to: data.to ? data.to : user.email, // list of receivers 19 | subject: data.subject ? data.subject : 'Notification', // Subject line 20 | html: data.html 21 | }; 22 | 23 | System.mailer.sendMail(mailOptions, function(error, info){ 24 | if (error) { 25 | console.log(error); 26 | } else { 27 | console.log('Message sent: ' + info.response); 28 | } 29 | }); 30 | }, 31 | }; 32 | } 33 | }; 34 | 35 | /** 36 | * Attributes to identify the plugin 37 | * @type {Object} 38 | */ 39 | plugin.register.attributes = { 40 | name: 'Notifications Helper', 41 | key: 'notifications', 42 | version: '1.0.0' 43 | }; 44 | return plugin; 45 | }; -------------------------------------------------------------------------------- /system/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load dependencies 3 | */ 4 | var express = require('express'); 5 | var app = express(); 6 | var http = require('http').Server(app); 7 | var io = require('socket.io')(http); 8 | var fs = require('fs'); 9 | var mongoose = require('mongoose'); 10 | var Config = require('./config/' + (process.env.NODE_ENV || 'development')); 11 | var bodyParser = require('body-parser'); 12 | var multer = require('multer'); 13 | var morgan = require('morgan'); 14 | var path = require('path'); 15 | var nodemailer = require('nodemailer'); 16 | 17 | /** 18 | * Load the settings model 19 | */ 20 | require('./models/settings'); 21 | var SystemSettings = mongoose.model('settings'); 22 | 23 | /** 24 | * Middleware 25 | */ 26 | app.use(bodyParser.json()); // for parsing application/json 27 | app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 28 | app.use(multer({ dest: './public/uploads/'})); // for parsing multipart/form-data 29 | app.use(morgan("dev")); 30 | app.use(function(req, res, next) { 31 | res.setHeader('Access-Control-Allow-Origin', '*'); 32 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); 33 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization'); 34 | next(); 35 | }); 36 | var options = { 37 | dotfiles: 'ignore', 38 | etag: false, 39 | // extensions: ['htm', 'html'], 40 | index: false, 41 | maxAge: '1d', 42 | redirect: false, 43 | setHeaders: function (res, path, stat) { 44 | res.set('x-timestamp', Date.now()) 45 | } 46 | }; 47 | app.use(express.static('public', options)); 48 | app.use('/dist', express.static('dist', options)); 49 | app.use('/system', express.static('system/public', options)); 50 | app.use('/system/public/views', express.static('system/public/views', options)); 51 | 52 | /** 53 | * Path where modules are located 54 | */ 55 | var modulePath = __dirname + '/../modules'; 56 | var moduleURL = 'modules'; 57 | 58 | /** 59 | * Create new server 60 | * @return {Void} 61 | */ 62 | function startServer() { 63 | app.use(function(req, res, next) { 64 | var output = fs.readFileSync(__dirname + '/../public/index.html'); 65 | res.type('html').send(output); 66 | next(); 67 | }); 68 | var server = http.listen(Config.server.port, function() { 69 | var host = server.address().address 70 | var port = server.address().port 71 | 72 | console.log('AtWork running at http://%s:%s', host, port); 73 | }); 74 | } 75 | 76 | /** 77 | * Load built-in system routes 78 | * @param {Object} System The system object 79 | * @return {Void} 80 | */ 81 | function systemRoutes(System) { 82 | var routes = []; 83 | routes = routes.concat(require('./routes/search')(System)); 84 | routes = routes.concat(require('./routes/settings')(System)); 85 | 86 | routes.forEach(function(route) { 87 | var moduleRouter = express.Router(); 88 | if (!route.authorized) { 89 | moduleRouter[route.method](route.path, System.auth.justGetUser, function(req, res) { 90 | setTimeout(function() { 91 | route.handler(req, res); 92 | }, System.config.REQUESTS_DELAY_SYSTEM); 93 | }); 94 | } else { 95 | moduleRouter[route.method](route.path, System.auth.ensureAuthorized, function(req, res) { 96 | setTimeout(function() { 97 | route.handler(req, res); 98 | }, System.config.REQUESTS_DELAY_SYSTEM); 99 | }); 100 | } 101 | app.use('/', moduleRouter); 102 | }); 103 | } 104 | 105 | /** 106 | * Load system settings 107 | * @param {Object} System The system object 108 | * @return {Void} 109 | */ 110 | function loadSettings(System, cb) { 111 | SystemSettings.find({}).exec(function(err, settings) { 112 | if (err) throw err; 113 | settings.map(function(setting) { 114 | System.settings[setting.name] = setting.value; 115 | }); 116 | System.mailer = nodemailer.createTransport({ 117 | service: Config.settings.email.service, 118 | auth: { 119 | user: System.settings.email, 120 | pass: System.settings.emailPassword 121 | } 122 | }); 123 | cb(); 124 | }); 125 | } 126 | 127 | /** 128 | * Connect to the database 129 | * @return {Object} Returns the connection object 130 | */ 131 | var dbConnect = function() { 132 | var db = mongoose.connect(Config.db); 133 | return db; 134 | }; 135 | 136 | /** 137 | * Connect to the database 138 | * @return {Object} Returns the connection object 139 | */ 140 | var loadPlugins = function(startingPath, System) { 141 | var helpersPath = startingPath + '/helpers'; 142 | if (!fs.existsSync(helpersPath)) { 143 | return false; 144 | } 145 | var files = fs.readdirSync(helpersPath); //not allowing subfolders for now inside 'helpers' folder 146 | files.forEach(function(file) { 147 | if (path.extname(file) !== '.js') { 148 | return true; 149 | } 150 | var plugin = require(helpersPath + '/' + file)(System); 151 | System.plugins[plugin.register.attributes.key] = plugin.register(); 152 | console.log('Loaded plugin: ' + file); 153 | }); 154 | 155 | /** 156 | * Expose the auth plugin 157 | */ 158 | System.auth = System.plugins.auth; 159 | 160 | return true; 161 | }; 162 | 163 | /** 164 | * Load all files inside the models folder (mongoose models) 165 | * @param {String} startingPath The starting path of the module 166 | * @return {Boolean} 167 | */ 168 | var loadDBModels = function(startingPath) { 169 | var modelsPath = startingPath + '/models'; 170 | if (!fs.existsSync(modelsPath)) { 171 | return false; 172 | } 173 | var files = fs.readdirSync(modelsPath); //not allowing subfolders for now inside 'models' folder 174 | files.forEach(function(file) { 175 | require(modelsPath + '/' + file); 176 | console.log('Loaded model: ' + file); 177 | }); 178 | return true; 179 | }; 180 | 181 | /** 182 | * Function to load all modules in the modules directory 183 | * @param {Object} System The main system object 184 | * @param {Function} callback The callback after loading all dependencies 185 | * @return {Void} 186 | */ 187 | var loadModules = function(System, callback) { 188 | var list = fs.readdirSync(modulePath); 189 | var requires = []; 190 | 191 | list.forEach(function(folder) { 192 | var serverPath = modulePath + '/' + folder + '/server'; 193 | var publicPath = moduleURL + '/' + folder; 194 | 195 | /** 196 | * Expose public paths 197 | */ 198 | app.use('/' + publicPath, express.static(publicPath + '/public', options)); 199 | 200 | /** 201 | * Load needed db models 202 | */ 203 | loadDBModels(serverPath); 204 | 205 | /** 206 | * Require all main files of each module 207 | */ 208 | var moduleFile = serverPath + '/main.js'; 209 | if (fs.existsSync(moduleFile)) { 210 | requires.push(require(moduleFile)); 211 | } 212 | }); 213 | 214 | /** 215 | * Initiate each module by passing system to it 216 | */ 217 | requires.map(function(module) { 218 | module(System); 219 | }); 220 | 221 | callback(); 222 | }; 223 | 224 | /** 225 | * Export the object 226 | * @type {Object} 227 | */ 228 | module.exports = { 229 | 230 | /** 231 | * Dynamically loaded plugins are accessible under plugins 232 | * @type {Object} 233 | */ 234 | plugins: {}, 235 | 236 | /** 237 | * Expose the web socket connection 238 | * @type {Object} 239 | */ 240 | webSocket: io, 241 | 242 | /** 243 | * Settings of the system 244 | * Populated from the DB 245 | * @type {Object} 246 | */ 247 | settings: {}, 248 | 249 | /** 250 | * Function to initialize the system and load all dependencies 251 | * @return {Void} 252 | */ 253 | boot: function() { 254 | /** 255 | * Reference self 256 | * @type {Object} 257 | */ 258 | var $this = this; 259 | 260 | /** 261 | * Pass the config object as is for now (TODO: reduce sensitive data) 262 | * @type {[type]} 263 | */ 264 | this.config = Config; 265 | 266 | /** 267 | * Connect to database 268 | */ 269 | dbConnect(); 270 | 271 | /** 272 | * Load the helpers 273 | */ 274 | loadPlugins(__dirname, this); 275 | 276 | /** 277 | * Finally, load dependencies and start the server 278 | */ 279 | loadModules($this, function() { 280 | loadSettings($this, function() { 281 | systemRoutes($this); 282 | startServer(); 283 | }); 284 | }); 285 | }, 286 | 287 | /** 288 | * Reduced config object 289 | * @type {Object} 290 | */ 291 | config: {}, 292 | 293 | /** 294 | * The mailer object to send out emails 295 | * @type {Object} 296 | */ 297 | mailer: {}, 298 | 299 | /** 300 | * Wrapping the server's route function 301 | * @param {Array} routes The array of routes 302 | * @return {Void} 303 | */ 304 | route: function(routes, moduleName) { 305 | var $this = this; 306 | 307 | /** 308 | * Module name is mandatory 309 | * @type {String} 310 | */ 311 | moduleName = moduleName || 'unidentified'; 312 | 313 | /** 314 | * Prefix each route to its module's path 315 | */ 316 | routes.forEach(function(route) { 317 | var moduleRouter = express.Router(); 318 | if (!route.authorized) { 319 | moduleRouter[route.method](route.path, $this.auth.justGetUser, function(req, res) { 320 | setTimeout(function() { 321 | route.handler(req, res); 322 | }, $this.config.REQUESTS_DELAY); 323 | }); 324 | } else { 325 | moduleRouter[route.method](route.path, $this.auth.ensureAuthorized, function(req, res) { 326 | setTimeout(function() { 327 | route.handler(req, res); 328 | }, $this.config.REQUESTS_DELAY); 329 | }); 330 | } 331 | app.use('/' + moduleName, moduleRouter); 332 | }); 333 | } 334 | 335 | }; -------------------------------------------------------------------------------- /system/models/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | _ = require('lodash'); 10 | 11 | /** 12 | * Activity Schema 13 | */ 14 | 15 | var SettingsSchema = new Schema({ 16 | name: String, 17 | value: String 18 | }); 19 | 20 | 21 | mongoose.model('settings', SettingsSchema); 22 | -------------------------------------------------------------------------------- /system/public/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.system', [ 4 | 'ngRoute', 5 | 'ngMessages', 6 | 'ngResource', 7 | 'angularFileUpload', 8 | 'atwork.utils', 9 | 'angular-loading-bar', 10 | 'ngAnimate' 11 | ]); 12 | 13 | angular.module('atwork.system') 14 | .factory('tokenHttpInterceptor', [ 15 | 'appStorage', 16 | function (appStorage) { 17 | return { 18 | request: function (config) { 19 | config.headers.Authorization = 'Bearer ' + appStorage.get('userToken'); 20 | return config; 21 | } 22 | }; 23 | } 24 | ]) 25 | .factory('appSearch', [ 26 | '$resource', 27 | function($resource) { 28 | var search = $resource('search/:keyword', {}, {query: {isArray: false}}); 29 | return function(keyword) { 30 | //implement search logic here 31 | var promise = search.query({keyword: keyword}).$promise; 32 | return promise; 33 | }; 34 | } 35 | ]) 36 | .config([ 37 | '$httpProvider', 38 | '$mdThemingProvider', 39 | 'cfpLoadingBarProvider', 40 | function ($httpProvider, $mdThemingProvider, cfpLoadingBarProvider) { 41 | $httpProvider.interceptors.push('tokenHttpInterceptor'); 42 | // $mdThemingProvider.theme('default') 43 | // .primaryPalette('blue') 44 | // .accentPalette('blue-grey'); 45 | 46 | // $mdThemingProvider.definePalette('primaryPalette', { 47 | // '50': 'E4EFF7', 48 | // '100': 'D6E0E7', 49 | // '200': '77C0F4', 50 | // '300': '63B4ED', 51 | // '400': '40A8F2', 52 | // '500': '36A5F4', 53 | // '600': '249DF4', 54 | // '700': '1196F4', 55 | // '800': '0691F4', 56 | // '900': '0A98FD', 57 | // 'A100': '89BEC8', 58 | // 'A200': '89BEC8', 59 | // 'A400': '89BEC8', 60 | // 'A700': '89BEC8', 61 | // 'contrastDefaultColor': 'light', // whether, by default, text (contrast) 62 | // // on this palette should be dark or light 63 | // 'contrastDarkColors': ['50', '100', //hues which contrast should be 'dark' by default 64 | // '200', '300', '400', 'A100'], 65 | // 'contrastLightColors': undefined // could also specify this if default was 'dark' 66 | // }); 67 | 68 | // $mdThemingProvider.theme('default') 69 | // .primaryPalette('primaryPalette') 70 | // .accentPalette('primaryPalette') 71 | 72 | $mdThemingProvider.definePalette('amazingPaletteName', { 73 | '50': 'ffebee', 74 | '100': 'ffcdd2', 75 | '200': 'ef9a9a', 76 | '300': 'e57373', 77 | '400': 'ef5350', 78 | '500': 'f44336', 79 | '600': 'e53935', 80 | '700': 'd32f2f', 81 | '800': 'c62828', 82 | '900': 'b71c1c', 83 | 'A100': 'ff8a80', 84 | 'A200': 'ff5252', 85 | 'A400': 'ff1744', 86 | 'A700': 'd50000', 87 | 'contrastDefaultColor': 'light', // whether, by default, text (contrast) 88 | // on this palette should be dark or light 89 | 'contrastDarkColors': ['50', '100', //hues which contrast should be 'dark' by default 90 | '200', '300', '400', 'A100'], 91 | 'contrastLightColors': undefined // could also specify this if default was 'dark' 92 | }); 93 | $mdThemingProvider.theme('default') 94 | .primaryPalette('amazingPaletteName') 95 | .accentPalette('amazingPaletteName') 96 | 97 | cfpLoadingBarProvider.includeSpinner = true; 98 | cfpLoadingBarProvider.includeBar = false; 99 | } 100 | ]); 101 | -------------------------------------------------------------------------------- /system/public/settings/settings.controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.settings') 4 | .controller('SettingsCtrl', [ 5 | '$scope', 6 | '$rootScope', 7 | '$routeParams', 8 | '$timeout', 9 | 'appPosts', 10 | 'appAuth', 11 | 'appToast', 12 | 'appStorage', 13 | 'appLocation', 14 | 'appWebSocket', 15 | 'appUsersSearch', 16 | 'appSettings', 17 | function($scope, $rootScope, $routeParams, $timeout, appPosts, appAuth, appToast, appStorage, appLocation, appWebSocket, appUsersSearch, appSettings) { 18 | /** 19 | * Refresh settings from API 20 | */ 21 | appSettings.fetch(function(settings) { 22 | $scope.systemSettings = settings; 23 | }); 24 | 25 | if (appAuth.getUser().roles.indexOf('admin') === -1) { 26 | appToast('Only an Administrator can change system\'s settings.'); 27 | appLocation.url('/'); 28 | } 29 | 30 | $scope.save = function(isValid) { 31 | var req = new appSettings.single($scope.systemSettings); 32 | req.$save(function(res) { 33 | if (res.success) { 34 | appToast('Your settings are saved.'); 35 | appLocation.url('/'); 36 | } 37 | }); 38 | }; 39 | } 40 | ]) 41 | ; 42 | -------------------------------------------------------------------------------- /system/public/settings/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.settings', []); -------------------------------------------------------------------------------- /system/public/settings/settings.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.settings') 4 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 5 | $routeProvider 6 | .when('/settings', { 7 | templateUrl: '/system/settings/views/settings.html', 8 | controller: 'SettingsCtrl' 9 | }) 10 | ; 11 | $locationProvider.html5Mode(true); 12 | }]); -------------------------------------------------------------------------------- /system/public/settings/settings.services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.settings') 4 | .factory('appSettings', [ 5 | '$resource', 6 | '$rootScope', 7 | function($resource, $rootScope) { 8 | return { 9 | cache: {}, 10 | single: $resource('system-settings/'), 11 | fetch: function(cb) { 12 | var $this = this; 13 | var settings = $this.single.get({}, function() { 14 | for (var i in settings.res.items) { 15 | var setting = settings.res.items[i]; 16 | $this.cache[setting.name] = setting.value; 17 | } 18 | $rootScope.systemSettings = $this.cache; 19 | return cb ? cb($this.cache) : true; 20 | }); 21 | } 22 | } 23 | } 24 | ]) 25 | .factory('appSettingsValid', [ 26 | 'appSettings', 27 | 'appLocation', 28 | '$rootScope', 29 | function(appSettings, appLocation, $rootScope) { 30 | return function() { 31 | if (appLocation.url() !== '/settings' && appLocation.url() !== '/logout') { 32 | if (!$rootScope.systemSettings || !$rootScope.systemSettings.domains || !$rootScope.systemSettings.workplace) { 33 | appLocation.url('/settings'); 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | } 40 | ]) 41 | 42 | ; 43 | -------------------------------------------------------------------------------- /system/public/settings/views/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Setup

4 |

Provide the details related to your workplace to customize your experience.

5 |
6 | 7 | 8 | 9 | 10 |
11 |
The name of your company, team, office or branch (e.g. Back Seat Boys)
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
Anything that inspires your team (e.g. Onward and upward!)
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
The email domains that you want to allow for registrations (e.g. @best-company.com, @best-branch.com)
28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
The email the system connects to, for sending notifications and stuff
36 |
37 |
38 | 39 | 40 | 41 | 42 |
43 |
The email the system connects to, for sending notifications and stuff
44 |
45 |
46 | 47 | 48 | Save Settings 49 | 50 |
51 |
52 |
-------------------------------------------------------------------------------- /system/public/templates/notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Notification 7 | 187 | 188 | 189 | 190 | 191 | 192 | 204 | 205 | 206 | 228 | 229 | 230 | 231 | 232 | 249 | 250 |
193 | 194 | 195 | 201 | 202 | 203 |
207 | 208 | 209 | 225 | 226 |
210 |
211 |
212 |

Hey <%= name %>,

213 |

<%= message %>

214 | <% if (locals.message2) { %> 215 |

<%= message2 %>

216 | <% } %> 217 |

<%= action %>

218 |

Cheers!

219 | <% if (typeof workplace !== 'undefined') { %> 220 |

<%= workplace %>

221 | <% } %> 222 |
223 |
224 |
227 |
233 | 234 | 235 | 237 | 238 | 239 | 246 | 247 | 248 |
251 | 252 | -------------------------------------------------------------------------------- /system/public/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('atwork.utils', ['ngRoute', 'ngMaterial']) 4 | .factory('appStorage', function() { 5 | return { 6 | get: function(item) { 7 | return localStorage.getItem(item); 8 | }, 9 | set: function(item, val) { 10 | return localStorage.setItem(item, val); 11 | }, 12 | remove: function(item) { 13 | return localStorage.removeItem(item); 14 | } 15 | } 16 | }) 17 | .factory('appPromise', [ 18 | function() { 19 | return function(fn) { 20 | var deferred = Q.defer(); 21 | fn(deferred); 22 | return deferred.promise; 23 | } 24 | } 25 | ]) 26 | .factory('appLocation', [ 27 | '$location', 28 | function($location) { 29 | return $location; 30 | } 31 | ]) 32 | .factory('appWebSocket', [ 33 | function($location) { 34 | var obj = { 35 | conn: {}, 36 | connect: function() { 37 | var $this = this; 38 | var socket = window.io(); 39 | socket.on('connect', function() { 40 | console.log('Connected'); 41 | }); 42 | socket.on('disconnect', function() { 43 | $this.connect(); 44 | }); 45 | this.conn = socket; 46 | }, 47 | reconnect: function() { 48 | this.conn.close(); 49 | this.connect(); 50 | }, 51 | close: function() { 52 | this.conn.close(); 53 | } 54 | }; 55 | obj.connect(); 56 | return obj; 57 | } 58 | ]) 59 | .factory('appToast', [ 60 | '$mdToast', 61 | function($mdToast) { 62 | return function(message) { 63 | var toast = $mdToast.simple() 64 | .content(message) 65 | .action('OK') 66 | .highlightAction(false) 67 | .position('top right'); 68 | $mdToast.show(toast); 69 | } 70 | } 71 | ]) 72 | .factory('appDialog', [ 73 | '$mdDialog', 74 | function($mdDialog) { 75 | return $mdDialog; 76 | } 77 | ]) 78 | .factory('appDesktop', [ 79 | '$rootScope', 80 | function($rootScope) { 81 | var notifBadge = 0; 82 | var messageBadge = 0; 83 | return { 84 | notify: function(options) { 85 | notifBadge = (options.notificationsCount !== undefined) ? options.notificationsCount : notifBadge; 86 | messageBadge = (options.messagesCount !== undefined) ? options.messagesCount : messageBadge; 87 | $rootScope.badges = {messageBadge: messageBadge}; 88 | if (window.fluid) { 89 | window.fluid.dockBadge = notifBadge + messageBadge; 90 | if (parseInt(window.fluid.dockBadge) <= 0) { 91 | window.fluid.dockBadge = undefined; 92 | } else { 93 | window.fluid.playSound('Sosumi'); 94 | window.fluid.playSound('Purr'); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | ]) 101 | .directive('setFocus', [ 102 | '$timeout', '$parse', 103 | function($timeout, $parse) { 104 | return { 105 | //scope: true, // optionally create a child scope 106 | link: function(scope, element, attrs) { 107 | /** 108 | * Set focus only if not on mobile 109 | */ 110 | if ($(window).width() <= 600) { 111 | return true; 112 | } 113 | var model = $parse(attrs.setFocus); 114 | scope.$watch(model, function(value) { 115 | if(value === true) { 116 | $timeout(function() { 117 | element[0].focus(); 118 | }, 800); 119 | } 120 | }); 121 | } 122 | }; 123 | } 124 | ]) 125 | 126 | ; 127 | -------------------------------------------------------------------------------- /system/public/views/search.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | {{item.name}} / @{{item.username}} 10 | 11 |
12 | -------------------------------------------------------------------------------- /system/routes/search.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var User = mongoose.model('User'); 3 | 4 | /** 5 | * Init the controller 6 | */ 7 | module.exports = function(System) { 8 | 9 | var routes = []; 10 | var json = System.plugins.JSON; 11 | 12 | routes.push({ 13 | method: 'get', 14 | path: '/search/:keyword', 15 | authorized: true, 16 | handler: function(req, res) { 17 | var keyword = req.param('keyword'); 18 | User 19 | .find({name: new RegExp(keyword, 'ig')}, null, {sort: {name: 1}}) 20 | .lean() 21 | .exec(function(err, items) { 22 | if (err) { 23 | return json.unhappy(err, res); 24 | } 25 | return json.happy({ items: items }, res); 26 | }); 27 | } 28 | }); 29 | return routes; 30 | }; -------------------------------------------------------------------------------- /system/routes/settings.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var async = require('async'); 3 | var SystemSettings = mongoose.model('settings'); 4 | 5 | /** 6 | * Init the controller 7 | */ 8 | module.exports = function(System) { 9 | 10 | var routes = []; 11 | var json = System.plugins.JSON; 12 | 13 | routes.push({ 14 | method: 'get', 15 | path: '/system-settings', 16 | authorized: false, 17 | handler: function(req, res) { 18 | var criteria = {}; 19 | if (!req.user) { 20 | criteria = { 21 | name: { $in: ['workplace', 'tagline'] } 22 | }; 23 | } 24 | SystemSettings 25 | .find(criteria, null, {sort: {name: 1}}) 26 | .lean() 27 | .exec(function(err, items) { 28 | if (err) { 29 | return json.unhappy(err, res); 30 | } 31 | return json.happy({ items: items }, res); 32 | }); 33 | } 34 | }); 35 | 36 | routes.push({ 37 | method: 'post', 38 | path: '/system-settings', 39 | authorized: true, 40 | handler: function(req, res) { 41 | /** 42 | * Received settings 43 | * @type {Object} 44 | */ 45 | var settingItems = req.body; 46 | 47 | /** 48 | * Check if user is an admin 49 | */ 50 | if (!req.user.isAdmin()) { 51 | console.log('FORBIDDEN ADMIN'); 52 | /** 53 | * Return as is 54 | */ 55 | return json.happy({ items: settingItems }, res); 56 | } 57 | 58 | /** 59 | * Function to save an individual setting 60 | * @param {Object} 61 | * @param {Function} 62 | * @return {Void} 63 | */ 64 | var saveSetting = function(setting, cb) { 65 | SystemSettings 66 | .findOne({name: setting.key}) 67 | .exec(function(err, item) { 68 | if (err) { 69 | return json.unhappy(err, res); 70 | } 71 | if (item) { 72 | item.value = setting.val; 73 | } else { 74 | item = new SystemSettings(); 75 | item.name = setting.key; 76 | item.value = setting.val; 77 | } 78 | item.save(function(err) { 79 | cb(err); 80 | }); 81 | }); 82 | }; 83 | 84 | /** 85 | * Create an array of items 86 | * @type {Array} 87 | */ 88 | var items = []; 89 | for (var key in settingItems) { 90 | var val = settingItems[key]; 91 | items.push({key: key, val: val}); 92 | }; 93 | 94 | /** 95 | * Save all gathered settings 96 | */ 97 | async.map(items, saveSetting, function(err, results) { 98 | console.log(results); 99 | return json.happy({ items: items, results: results }, res); 100 | }); 101 | 102 | } 103 | }); 104 | return routes; 105 | }; -------------------------------------------------------------------------------- /system/tests/system.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | var expect = chai.expect; 5 | 6 | // Articles Controller Spec 7 | describe('System', function() { 8 | describe('SystemServices', function() { 9 | beforeEach(function() { 10 | module('atwork.system'); 11 | }); 12 | 13 | // Initialize the controller and a mock scope 14 | var appSearch, 15 | appStorage, 16 | tokenHttpInterceptor, 17 | scope, 18 | $httpBackend, 19 | $stateParams, 20 | $location; 21 | 22 | // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). 23 | // This allows us to inject a service but then attach it to a variable 24 | // with the same name as the service. 25 | beforeEach(inject(function($rootScope, _$location_, _$httpBackend_, _appSearch_, _appStorage_, _tokenHttpInterceptor_) { 26 | 27 | scope = $rootScope.$new(); 28 | 29 | appSearch = _appSearch_; 30 | appStorage = _appStorage_; 31 | tokenHttpInterceptor = _tokenHttpInterceptor_; 32 | 33 | $httpBackend = _$httpBackend_; 34 | 35 | $location = _$location_; 36 | 37 | })); 38 | 39 | it('Service appSearch should be available', function() { 40 | expect(appSearch).to.be.a('function'); 41 | }); 42 | it('Service tokenHttpInterceptor should be available', function() { 43 | expect(tokenHttpInterceptor).to.be.a('object'); 44 | 45 | var config = {headers: {}}; 46 | appStorage.set('userToken', 'sample token'); 47 | config = tokenHttpInterceptor.request(config); 48 | expect(config.headers.Authorization).to.be.a('string'); 49 | expect(config.headers.Authorization).to.be.equal('Bearer sample token'); 50 | }); 51 | 52 | }); 53 | }); 54 | }()); 55 | -------------------------------------------------------------------------------- /tools/test/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": { 3 | "css": { 4 | "public/css/main.css": [] 5 | }, 6 | "js": { 7 | "public/bower_components/build/js/dist.min.js": [ 8 | "public/bower_components/angular/angular.js", 9 | "public/bower_components/angular-aria/angular-aria.js", 10 | "public/bower_components/angular-animate/angular-animate.js", 11 | "public/bower_components/angular-material/angular-material.js", 12 | "public/bower_components/angular-route/angular-route.js", 13 | "public/bower_components/angular-messages/angular-messages.js", 14 | "public/bower_components/angular-resource/angular-resource.js", 15 | "public/bower_components/q/q.js", 16 | "public/bower_components/ng-file-upload/angular-file-upload.js", 17 | "public/bower_components/angular-loading-bar/build/loading-bar.min.js", 18 | "public/bower_components/angular-mocks/angular-mocks.js" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tools/test/mocha-req.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | var appRoot = __dirname + '/../../'; 5 | global.System = require(appRoot + 'index'); 6 | 7 | /** 8 | * Mock event plugin 9 | * @type {Object} 10 | */ 11 | global.System.plugins.event = { 12 | on: function() { 13 | return true; 14 | }, 15 | trigger: function() { 16 | return true; 17 | } 18 | }; --------------------------------------------------------------------------------