├── .gitignore ├── Capfile ├── Gemfile ├── Gemfile.lock ├── Gruntfile.js ├── LICENSE ├── README.md ├── app.js ├── app ├── middleware │ ├── errors.js │ ├── passport.js │ └── users.js ├── models │ ├── index.js │ ├── orgs.js │ ├── project.js │ ├── requesters.js │ ├── requests.js │ ├── support.js │ └── user.js ├── routes │ ├── index.js │ ├── maintainers.js │ ├── organizations.js │ ├── projects.js │ ├── service.js │ ├── supporters.js │ └── users.js ├── utils │ ├── ensure-auth.js │ ├── git-request.js │ ├── logger.js │ ├── mailer.js │ ├── requests-converter.js │ ├── requests-notifications.js │ ├── socket.io.js │ ├── title-builder.js │ └── updater │ │ ├── index.js │ │ ├── organizations.js │ │ └── projects.js └── views │ ├── 404.ejs │ ├── 500.ejs │ ├── account.ejs │ ├── emails │ ├── comment-for-author.ejs │ ├── donate-request-maintainer.ejs │ ├── donate-request-notify-maintainer.ejs │ ├── donate-request-satisfied.ejs │ └── donate-request-support.ejs │ ├── index.ejs │ ├── maintainer-editor.ejs │ ├── modals │ ├── email-to-author.ejs │ ├── index.ejs │ ├── need-contribution-ways.ejs │ └── note-from-author.ejs │ ├── organization.ejs │ ├── parts │ ├── contributions.ejs │ ├── foot.ejs │ ├── head.ejs │ ├── header.ejs │ ├── project-row.ejs │ └── welcome.ejs │ ├── project.ejs │ ├── registration.ejs │ ├── repo-editor.ejs │ ├── service │ └── donation-requests.ejs │ └── settings.ejs ├── bower.json ├── config ├── deploy.rb ├── deploy │ ├── production.rb │ └── staging.rb ├── example.config.js └── index.js ├── less ├── base │ └── body.less ├── main.less ├── pages │ ├── account.less │ ├── donation-requests.less │ ├── index.less │ ├── maintainers.less │ ├── project.less │ └── repo-selector.less ├── parts │ ├── font.less │ ├── forms.less │ ├── help-overlay.less │ ├── profile-header.less │ └── repos-list.less └── vendors │ ├── bootstrap │ ├── alerts.less │ ├── badges.less │ ├── bootstrap.less │ ├── breadcrumbs.less │ ├── button-groups.less │ ├── buttons.less │ ├── carousel.less │ ├── close.less │ ├── code.less │ ├── component-animations.less │ ├── dropdowns.less │ ├── forms.less │ ├── glyphicons.less │ ├── grid.less │ ├── input-groups.less │ ├── jumbotron.less │ ├── labels.less │ ├── list-group.less │ ├── media.less │ ├── mixins.less │ ├── modals.less │ ├── navbar.less │ ├── navs.less │ ├── normalize.less │ ├── pager.less │ ├── pagination.less │ ├── panels.less │ ├── popovers.less │ ├── print.less │ ├── progress-bars.less │ ├── responsive-utilities.less │ ├── scaffolding.less │ ├── tables.less │ ├── theme.less │ ├── thumbnails.less │ ├── tooltip.less │ ├── type.less │ ├── utilities.less │ ├── variables.less │ └── wells.less │ └── font-awesome │ ├── bootstrap.less │ ├── core.less │ ├── extras.less │ ├── font-awesome-ie7.less │ ├── font-awesome.less │ ├── icons.less │ ├── mixins.less │ ├── path.less │ └── variables.less ├── package.json └── public ├── favicon.ico ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff ├── quicksand-bold-webfont.svg ├── quicksand-bold-webfont.ttf ├── quicksand-bold-webfont.woff ├── quicksand-light-webfont.eot ├── quicksand-light-webfont.svg ├── quicksand-light-webfont.ttf ├── quicksand-light-webfont.woff ├── quicksand-regular-webfont.eot ├── quicksand-regular-webfont.svg ├── quicksand-regular-webfont.ttf └── quicksand-regular-webfont.woff ├── images ├── bg-highlight.jpg ├── bg.png ├── codio-icon.png ├── digitalocean.png ├── flattr.png ├── github-ribbon.png ├── gittip.png ├── help │ └── supporters │ │ ├── donate.png │ │ ├── dropdown.jpg │ │ ├── preview.png │ │ ├── project.png │ │ └── supporting-icons.png ├── logo-darkbg.png ├── logo-lightbg.png └── paypal.gif └── js ├── bootstrap.js ├── common ├── templates │ └── donate-methods.html └── views │ └── donate-methods.js ├── modules ├── account │ └── main.js ├── donate-requests │ ├── collections │ │ ├── requests.js │ │ └── supporters.js │ ├── main.js │ ├── models │ │ └── request.js │ ├── templates │ │ ├── list.html │ │ ├── request.html │ │ ├── search.html │ │ ├── supporter.html │ │ └── supporters.html │ └── views │ │ ├── list.js │ │ ├── request.js │ │ ├── search.js │ │ ├── supporter.js │ │ └── supporters.js ├── maintainers │ ├── collections │ │ ├── project-groups.js │ │ └── projects.js │ ├── main.js │ ├── models │ │ └── project.js │ ├── templates │ │ ├── group.html │ │ ├── help.html │ │ ├── list.html │ │ ├── project-settings.html │ │ └── project.html │ └── views │ │ ├── group.js │ │ ├── list.js │ │ ├── project-settings.js │ │ └── project.js ├── project │ └── main.js ├── repo-selector │ ├── collections │ │ ├── project-groups.js │ │ ├── projects.js │ │ ├── repos.js │ │ └── search.js │ ├── main.js │ ├── models │ │ ├── project.js │ │ ├── repo.js │ │ └── support.js │ ├── router.js │ ├── templates │ │ ├── group-selector.html │ │ ├── help.html │ │ ├── layout.html │ │ ├── repo-list.html │ │ ├── repo-row.html │ │ ├── repo-search.html │ │ ├── selected.html │ │ └── share.html │ └── views │ │ ├── group-selector.js │ │ ├── layout.js │ │ ├── project-row.js │ │ ├── repo-list.js │ │ ├── repo-row.js │ │ ├── search.js │ │ └── selected.js └── user-settings │ └── main.js ├── plugins ├── activate-plugins.js ├── project.updater.js └── store.js └── require.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | bower_components 14 | node_modules 15 | public/js/vendors 16 | public/js/build 17 | public/css/main.css 18 | config/config.*.js 19 | 20 | npm-debug.log 21 | *.sublime* 22 | .idea/ -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | load 'deploy' 2 | load 'config/deploy' -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'capistrano' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | capistrano (2.15.5) 5 | highline 6 | net-scp (>= 1.0.0) 7 | net-sftp (>= 2.0.0) 8 | net-ssh (>= 2.0.14) 9 | net-ssh-gateway (>= 1.1.0) 10 | highline (1.6.19) 11 | net-scp (1.1.2) 12 | net-ssh (>= 2.6.5) 13 | net-sftp (2.1.2) 14 | net-ssh (>= 2.6.5) 15 | net-ssh (2.7.0) 16 | net-ssh-gateway (1.2.0) 17 | net-ssh (>= 2.6.5) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | capistrano 24 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | bower: { 5 | install: { 6 | options: { 7 | targetDir: './public/js/vendors/', 8 | layout: 'byComponent', 9 | install: true, 10 | verbose: true, 11 | cleanTargetDir: true, 12 | cleanBowerDir: true 13 | } 14 | } 15 | }, 16 | less: { 17 | default: { 18 | files: { 19 | 'public/css/main.css': 'less/main.less' 20 | }, 21 | options: { 22 | paths: [ 'less/basics', 'less/pages', 'less/parts' ] 23 | } 24 | } 25 | }, 26 | 27 | requirejs: { 28 | compile: { 29 | options: { 30 | appDir: './public/js', 31 | dir: './public/js/build', 32 | baseUrl: './', 33 | preserveLicenseComments: false, 34 | optimizeCss: 'none', 35 | useStrict: true, 36 | generateSourceMaps: true, 37 | optimize: 'uglify2', 38 | mainConfigFile: "./public/js/require.config.js", 39 | paths: { 40 | 'socket.io': 'empty:' 41 | }, 42 | modules: grunt.file.expand('./public/js/modules/*').map(function (entry) { 43 | return { 44 | name: entry.replace('./public/js/', '') + '/main' 45 | } 46 | }) 47 | } 48 | } 49 | }, 50 | 51 | autoprefixer: { 52 | options: { 53 | browsers: ['last 5 versions', '> 1%', 'ie 8', 'ie 7'] 54 | }, 55 | dist: { 56 | files: { 57 | 'public/css/main.css': 'public/css/main.css' 58 | } 59 | } 60 | }, 61 | 62 | cssmin: { 63 | default: { 64 | options: { 65 | report: 'min' 66 | }, 67 | files: { 68 | 'public/css/main.css': ['public/css/main.css'] 69 | } 70 | } 71 | }, 72 | 73 | watch: { 74 | files: 'less/**/*.less', 75 | tasks: ['build'] 76 | } 77 | }) 78 | 79 | grunt.loadNpmTasks('grunt-autoprefixer'); 80 | grunt.loadNpmTasks('grunt-bower-task'); 81 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 82 | grunt.loadNpmTasks('grunt-contrib-less'); 83 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 84 | grunt.loadNpmTasks('grunt-contrib-watch'); 85 | 86 | // Default task(s). 87 | grunt.registerTask('default', ['build']); 88 | grunt.registerTask('build', ['less', 'autoprefixer']); 89 | grunt.registerTask('build:prod', ['less', 'autoprefixer', 'cssmin', 'requirejs']); 90 | grunt.registerTask('update', ['bower', 'build:prod']); 91 | grunt.registerTask('heroku:production', ['update']); 92 | 93 | }; 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Codio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 7/29/13 4 | * Time: 9:14 PM 5 | */ 6 | var http = require('http'), 7 | path = require('path'), 8 | express = require('express'), 9 | cfg = require('./config'), 10 | logger = require('./app/utils/logger'), 11 | MongoStore = require('connect-mongo')(express), 12 | mongoose = require('mongoose'), 13 | app = express(); 14 | 15 | mongoose.connect(cfg.mongodbUri) 16 | mongoose.connection.on('error', function (err) { 17 | logger.error('Mongo connection error', err.message); 18 | }); 19 | mongoose.connection.once('open', function callback () { 20 | logger.info("Connected to MongoDB"); 21 | }); 22 | 23 | // Bootstrap models 24 | require('./app/models') 25 | 26 | var passport = require('./app/middleware/passport') 27 | var sessionStore = new MongoStore({ 28 | url: cfg.mongodbUri 29 | }) 30 | 31 | app.locals._ = require('lodash'); 32 | app.locals.titleBuilder = require('./app/utils/title-builder'); 33 | app.locals.jsPath = cfg.jsPath; 34 | app.locals.siteUrl = cfg.fullUrl(); 35 | 36 | // all environments 37 | app.set('port', cfg.port); 38 | app.set('views', __dirname + '/app/views'); 39 | app.set('view engine', 'ejs'); 40 | 41 | 42 | app.use(express.static(path.join(__dirname, 'public'))); 43 | app.use(express.logger('dev')); 44 | app.use(express.favicon(path.join(__dirname, 'public/favicon.ico'))); 45 | app.use(express.bodyParser()); 46 | app.use(express.methodOverride()); 47 | app.use(express.cookieParser(cfg.sessionSecret)); 48 | app.use(express.session({ 49 | secret: cfg.sessionSecret, 50 | store: sessionStore 51 | })); 52 | app.use(passport.initialize()); 53 | app.use(passport.session()); 54 | require('./app/middleware/users')(app) 55 | app.use(app.router); 56 | require('./app/routes')(app) 57 | if (cfg.isDev) { 58 | app.use(express.errorHandler()); 59 | } else { 60 | require('./app/middleware/errors')(app) 61 | } 62 | 63 | var server = http.createServer(app) 64 | 65 | require('./app/utils/socket.io')(server, sessionStore) 66 | 67 | require('./app/utils/mailer').fillTemplates(path.join(__dirname, 'app/views/emails/'), function (error) { 68 | if (error) throw error; 69 | 70 | server.listen(app.get('port'), function () { 71 | logger.info('Express server listening on port ' + app.get('port')); 72 | }); 73 | }) 74 | 75 | -------------------------------------------------------------------------------- /app/middleware/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/18/13 4 | * Time: 7:35 PM 5 | */ 6 | var logger = require('winston') 7 | 8 | module.exports = function (app) { 9 | //handling request errors 10 | app.use(function (req, res, next) { 11 | res.status(404); 12 | logger.warn('404 not found: %s', req.originalUrl) 13 | 14 | if (req.accepts('html')) { 15 | res.render('404'); 16 | return; 17 | } 18 | 19 | if (req.accepts('json')) { 20 | res.send({ error: 'Not found' }); 21 | return; 22 | } 23 | 24 | res.type('txt').send('Not found'); 25 | }); 26 | 27 | app.use(function (err, req, res, next) { 28 | res.status(err.status || 500); 29 | logger.warn('Route Error: %s', req.originalUrl, err) 30 | 31 | if (req.accepts('html')) { 32 | return res.render('500', { error: err }); 33 | } 34 | 35 | if (req.accepts('json')) { 36 | return res.send({ error: err }); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /app/middleware/passport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/4/13 4 | * Time: 6:15 PM 5 | */ 6 | var passport = require('passport'), 7 | cfg = require('../../config'), 8 | GitHubStrategy = require('passport-github').Strategy, 9 | mongoose = require('mongoose'), 10 | logger = require('winston'), 11 | User = mongoose.model('User') 12 | 13 | passport.serializeUser(function (user, done) { 14 | done(null, user); 15 | }); 16 | 17 | passport.deserializeUser(function (obj, done) { 18 | User.findOne({_id: obj._id}, function (err, user) { 19 | done(err, user); 20 | }); 21 | }); 22 | 23 | var callbackUrl = cfg.fullUrl() + '/auth/github/callback' 24 | logger.info('GitHub callback url is', callbackUrl) 25 | 26 | passport.use(new GitHubStrategy({ 27 | clientID: cfg.github.clientId, 28 | clientSecret: cfg.github.clientSecret, 29 | callbackURL: callbackUrl 30 | }, 31 | function (accessToken, refreshToken, profile, done) { 32 | User.findOneAndUpdate({ 'github.id': profile.id }, { $set: {authToken: accessToken}}, function (err, user) { 33 | if (user) return done(err, user) 34 | 35 | user = new User({ 36 | name: profile.displayName, 37 | email: profile._json.email, 38 | username: profile.username, 39 | displayName: profile.username, 40 | provider: 'github', 41 | github: profile._json, 42 | type: profile._json.type, 43 | authToken: accessToken 44 | }) 45 | 46 | user.register(function (err) { 47 | if (err) console.error(err) 48 | return done(err, user, true) 49 | }) 50 | }) 51 | } 52 | )); 53 | 54 | module.exports = passport 55 | -------------------------------------------------------------------------------- /app/middleware/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/18/13 4 | * Time: 7:34 PM 5 | */ 6 | module.exports = function (app) { 7 | //exposing current user 8 | app.use(function (req, res, next) { 9 | req.realIP = req.headers['x-real-ip'] || req.connection.remoteAddress 10 | res.locals.isLoggedIn = req.isAuthenticated() 11 | res.locals.loggedUser = req.user 12 | if (req.session.isNewUser) { 13 | res.locals.isNewUser = true 14 | req.session.isNewUser = null 15 | } 16 | next(); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/13/13 4 | * Time: 7:32 AM 5 | */ 6 | require('./user') 7 | require('./orgs') 8 | require('./project') 9 | require('./support') 10 | require('./requesters') 11 | require('./requests') -------------------------------------------------------------------------------- /app/models/orgs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 9/18/13 4 | * Time: 14:21 AM 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | gravatarRegexp = /gravatar.com\/avatar\/([0-9a-z]+)/i 9 | 10 | var OrganizationSchema = new Schema({ 11 | githubId: {type: Number, default: 0, index: {unique: true}}, 12 | name: String, 13 | gravatar: String, 14 | admins: [ 15 | {type: Schema.ObjectId, ref: 'User', index: true} 16 | ], 17 | publicAdmins: [ 18 | {type: Schema.ObjectId, ref: 'User', index: true} 19 | ] 20 | }) 21 | 22 | OrganizationSchema.statics.parseGitHubData = function (entry) { 23 | var gravatar = entry.avatar_url.match(gravatarRegexp) 24 | return { 25 | githubId: entry.id, 26 | name: entry.login, 27 | gravatar: gravatar ? gravatar[1] : '', 28 | admins: [], 29 | publicAdmins: [] 30 | } 31 | } 32 | 33 | mongoose.model('Organization', OrganizationSchema) -------------------------------------------------------------------------------- /app/models/requesters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 10/7/13 4 | * Time: 3:21 PM 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | emailRegExp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i 9 | 10 | var RequesterSchema = new Schema({ 11 | request: {type: Schema.ObjectId, ref: 'Request'}, 12 | ref: {type: Schema.ObjectId, ref: 'User'}, 13 | username: {type: String, trim: true }, 14 | email: {type: String, trim: true }, 15 | isAnon: {type: Boolean, default: false}, 16 | ip: {type: String, trim: true } 17 | }) 18 | 19 | RequesterSchema.path('email').validate(function (email) { 20 | if (email) return emailRegExp.test(email); 21 | return true 22 | }, 'Email field contain not an email') 23 | 24 | mongoose.model('Requester', RequesterSchema) 25 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/4/13 4 | * Time: 3:45 PM 5 | */ 6 | 7 | var mongoose = require('mongoose'), 8 | Schema = mongoose.Schema, 9 | emailRegExp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i 10 | 11 | var UserSchema = new Schema({ 12 | username: { type: String, required: true, index: { unique: true }, trim: true }, 13 | email: { type: String, trim: true }, 14 | avatar: String, 15 | admin: {type: Boolean, default: false}, 16 | 17 | displayName: {type: String, trim: true}, 18 | twitterName: {type: String, trim: true}, 19 | github: {}, 20 | provider: { type: String, default: '' }, 21 | authToken: { type: String, default: '' }, 22 | projectsUpdater: { 23 | updated: {type: Boolean}, 24 | updating: {type: Boolean}, 25 | status: {type: String, enum: ['error', 'success']}, 26 | updatedAt: { type: Date } 27 | }, 28 | noMaintainerNotifications: {type: Boolean, default: false}, 29 | statistics: { 30 | becameSupporterAt: Date, 31 | becameMaintainerAt: Date 32 | } 33 | }); 34 | 35 | UserSchema.path('email').validate(function (email) { 36 | if (email) return emailRegExp.test(email); 37 | return true 38 | }, 'Email field contain not an email') 39 | 40 | UserSchema.methods.register = function (callback) { 41 | this.save(function (error, user) { 42 | if (error) return callback(error) 43 | callback(error, user) 44 | require('../utils/updater/index.js')(user) 45 | }) 46 | } 47 | 48 | UserSchema.post('save', function (doc) { 49 | var Project = mongoose.model('Project') 50 | Project.updateOwner(doc) 51 | }) 52 | 53 | mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport') 2 | 3 | module.exports = function (app) { 4 | app.get('/auth/github', passport.authenticate('github')); 5 | 6 | app.get('/auth/github/callback', function (req, res, next) { 7 | passport.authenticate('github', function (err, user, isNew) { 8 | if (err) return next(err); 9 | if (!user) return res.redirect('/'); 10 | if (isNew) req.session.isNewUser = true; 11 | 12 | req.logIn(user, function (err) { 13 | if (err) return next(err); 14 | res.redirect(getRedirectUrl(req.session)); 15 | }); 16 | })(req, res, next); 17 | }); 18 | 19 | app.get('/logout', function (req, res) { 20 | req.logout(); 21 | res.redirect('/'); 22 | }); 23 | 24 | app.get('/', function (req, res) { 25 | res.render('index', { user: req.user }); 26 | }); 27 | 28 | require('./users')(app) 29 | require('./organizations')(app) 30 | require('./projects')(app) 31 | require('./supporters')(app) 32 | require('./maintainers')(app) 33 | require('./service')(app) 34 | }; 35 | 36 | function getRedirectUrl(session) { 37 | var redirectUrl = '/supporter/'; 38 | if (session.redirectUrl) { 39 | redirectUrl = session.redirectUrl; 40 | session.redirectUrl = null; 41 | } 42 | 43 | return redirectUrl 44 | } -------------------------------------------------------------------------------- /app/routes/organizations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 9/23/13 4 | * Time: 1:41 PM 5 | */ 6 | var mongoose = require('mongoose'), 7 | async = require('async'), 8 | Organization = mongoose.model('Organization'), 9 | Project = mongoose.model('Project'), 10 | Support = mongoose.model('Support'), 11 | User = mongoose.model('User') 12 | 13 | module.exports = function (app) { 14 | app.get('/orgs/:id', function (req, res) { 15 | Organization.findById(req.param('id')).populate('admins').exec(function (error, org) { 16 | if (error || !org) return res.send('400', 'There is no organization you looking for') 17 | 18 | 19 | async.parallel({ 20 | support: function (callback) { 21 | Support.find({ byOrganization: org._id }).populate('project').exec(callback) 22 | }, 23 | projects: function (callback) { 24 | Project.find({'owner.org': org._id}, callback) 25 | } 26 | }, function (error, results) { 27 | if (error) return res.send(500, 'Server error, please try later') 28 | 29 | res.render('organization', { 30 | org: org, 31 | projects: results.projects, 32 | support: results.support 33 | }); 34 | }) 35 | }) 36 | }); 37 | }; -------------------------------------------------------------------------------- /app/routes/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/17/13 4 | * Time: 4:44 PM 5 | */ 6 | var _ = require('lodash'), 7 | mongoose = require('mongoose'), 8 | async = require('async'), 9 | ensureAuthenticated = require('../utils/ensure-auth'), 10 | 11 | Project = mongoose.model('Project'), 12 | Organization = mongoose.model('Organization'), 13 | Support = mongoose.model('Support'), 14 | User = mongoose.model('User') 15 | 16 | module.exports = function (app) { 17 | 18 | app.get('/users/:username', function (req, res) { 19 | async.waterfall([ 20 | function (callback) { 21 | User.findOne({ 'username': req.param('username') }).exec(callback) 22 | }, 23 | function (user, callback) { 24 | if (!user) return callback(true); 25 | Organization.find({publicAdmins: user._id}) 26 | .exec(function(error, orgs) { 27 | if (error) return callback(error) 28 | 29 | user.orgs = orgs 30 | callback(null, user) 31 | }) 32 | } 33 | ], function (err, user) { 34 | if (err) return res.send(404) 35 | 36 | async.parallel({ 37 | personalSupport: function (callback) { 38 | Support.find({byUser: user._id}).populate('project').exec(callback) 39 | }, 40 | ownProjects: function (callback) { 41 | Project.find({'owner.user': user._id}, callback) 42 | }, 43 | adminProjects: function (callback) { 44 | Project.find({ 45 | admins: user._id, 46 | 'owner.org': {$in: _.pluck(user.orgs, '_id')} 47 | }, callback) 48 | } 49 | }, function (error, results) { 50 | if (error) return res.send(500, 'Server error, please try later') 51 | 52 | res.render('account', { 53 | user: user, 54 | projects: { 55 | personal: results.ownProjects, 56 | admin: results.adminProjects 57 | }, 58 | orgs: user.orgs, 59 | support: { 60 | personal: results.personalSupport 61 | } 62 | }); 63 | }) 64 | }) 65 | 66 | }) 67 | 68 | app.get('/settings', ensureAuthenticated, function (req, res) { 69 | res.render('settings', { user: req.user, error: ''}); 70 | }); 71 | 72 | app.post('/settings/:field', ensureAuthenticated, function (req, res) { 73 | var field = req.param('field') 74 | 75 | if (_.indexOf(['email', 'twitterName'], field) == -1) { 76 | return res.send(400, 'Wrong field'); 77 | } 78 | 79 | var data = {} 80 | data[field] = req.body.value 81 | 82 | User.findById(req.user._id, function (err, user) { 83 | if (err) return res.send(400, 'Failed to find user'); 84 | 85 | user[field] = req.body.value 86 | 87 | user.save(function (error) { 88 | if (error) return res.send(400, 'Failed to update field'); 89 | res.send(200, 'Field saved'); 90 | }) 91 | }); 92 | }); 93 | } 94 | ; -------------------------------------------------------------------------------- /app/utils/ensure-auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/18/13 4 | * Time: 8:12 PM 5 | */ 6 | module.exports = function (req, res, next) { 7 | if (req.isAuthenticated()) { 8 | return next(); 9 | } 10 | 11 | req.session.redirectUrl = req.url; 12 | res.redirect('/'); 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/git-request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 9/19/13 4 | * Time: 11:27 AM 5 | */ 6 | 7 | var _ = require('lodash'), 8 | mongoose = require('mongoose'), 9 | https = require('https'), 10 | qs = require('querystring'), 11 | Project = mongoose.model('Project') 12 | 13 | module.exports.request = function (path, params, callback) { 14 | var options = { 15 | hostname: 'api.github.com', 16 | port: 443, 17 | path: '/' + path + '?' + qs.stringify(params), 18 | method: 'GET', 19 | headers: { 20 | 'User-Agent': 'ilos', 21 | 'Agent': 'ilos', 22 | 'Accept': 'application/vnd.github.preview' 23 | } 24 | }; 25 | 26 | var request = https.request(options, function (response) { 27 | var bodyParts = [], bytes = 0 28 | 29 | if (Math.floor(response.statusCode / 100) === 5) { 30 | return callback('GitHub is currently not available. Please try later'); 31 | } 32 | 33 | response.on("data", function (c) { 34 | bodyParts.push(c) 35 | bytes += c.length 36 | }) 37 | 38 | response.on("end", function () { 39 | var body = new Buffer(bytes), copied = 0, status = response.statusCode 40 | 41 | bodyParts.forEach(function (b) { 42 | b.copy(body, copied, 0) 43 | copied += b.length 44 | }) 45 | 46 | try { 47 | body = JSON.parse(body.toString() || '{}'); 48 | } catch (err) { 49 | return callback('Failed to parse github response'); 50 | } 51 | 52 | var errorStatuses = [422, 400, 401, 404, 403] 53 | if (body.message && _.indexOf(errorStatuses, status) != -1) { 54 | return callback('GitHub error: ' + body.message); 55 | } 56 | 57 | callback(null, body, response.headers, status) 58 | }) 59 | }) 60 | 61 | request.end(); 62 | 63 | request.on('error', function (e) { 64 | callback('GitHub is currently not available. Please try later') 65 | }); 66 | } 67 | 68 | module.exports.requestRepos = function (path, params, callback) { 69 | module.exports.request(path, params, function (error, body, headers) { 70 | if (error) return callback(error) 71 | 72 | var linkHeader = headers['Link'] || headers['link'], 73 | links = {} 74 | 75 | if (linkHeader) { 76 | var parts = linkHeader.split(','); 77 | 78 | // Parse each part into a named link 79 | _.each(parts, function (p) { 80 | var section = p.split(';'); 81 | if (section.length != 2) return 82 | 83 | var url = section[0].replace(/<(.*)>/, '$1').trim().replace('https://api.github.com/', ''); 84 | var name = section[1].replace(/rel="(.*)"/, '$1').trim().toLowerCase(); 85 | links[name] = url; 86 | }); 87 | } 88 | 89 | var items = body.items || body 90 | if (!_.isArray(items)) items = [items] 91 | 92 | callback(null, _.map(items, function (repo) { 93 | repo = Project.parseGitHubData(repo) 94 | return repo 95 | }), links) 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /app/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 10/17/13 4 | * Time: 3:16 PM 5 | */ 6 | var winston = require('winston'), 7 | cfg = require('../../config') 8 | 9 | if (!cfg.isDev) { 10 | winston.remove(winston.transports.Console) 11 | winston.add(winston.transports.Console, { 12 | json: false, 13 | timestamp: true, 14 | handleExceptions: true 15 | }) 16 | winston.add(winston.transports.DailyRotateFile, { 17 | filename: cfg.logsDir + '/iloveopensource.log', 18 | datePattern: '.yyyy-MM-dd', 19 | handleExceptions: true 20 | }) 21 | } 22 | 23 | 24 | module.exports = winston; -------------------------------------------------------------------------------- /app/utils/mailer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 10/3/13 4 | * Time: 5:40 PM 5 | */ 6 | var cfg = require('../../config'), 7 | async = require('async'), 8 | _ = require('lodash'), 9 | ejs = require('ejs'), 10 | fs = require('fs'), 11 | logger = require('winston'), 12 | templates = {} 13 | 14 | //TODO: add mail queue 15 | module.exports.fillTemplates = function (dir, callback) { 16 | if (!_.isEmpty(templates)) return 17 | logger.info('Reading email templates from', dir) 18 | 19 | fs.readdir(dir, function (err, files) { 20 | if (err) return callback(err); 21 | 22 | async.each(files, function (file, cb) { 23 | fs.readFile(dir + file, 'utf-8', function (err, data) { 24 | templates[file.replace('.ejs', '')] = data 25 | cb.apply(this, arguments) 26 | }); 27 | }, callback); 28 | }); 29 | } 30 | 31 | module.exports.send = function (templateName, subject, to, data, callback) { 32 | var template = templates[templateName] 33 | if (!template) return callback('No such template') 34 | 35 | cfg.emails.transport.sendMail({ 36 | from: cfg.emails.from, 37 | subject: subject, 38 | to: to, 39 | html: ejs.render(template, _.merge(data, {siteUrl: cfg.fullUrl(), _: _})) 40 | }, function (error) { 41 | error && console.error(error) 42 | callback(error ? 'Failed to send email' : null) 43 | }) 44 | } -------------------------------------------------------------------------------- /app/utils/socket.io.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 9/19/13 4 | * Time: 5:09 PM 5 | */ 6 | var instance, 7 | passportSocketIo = require('passport.socketio'), 8 | express = require('express'), 9 | cfg = require('../../config') 10 | 11 | module.exports = function (server, sessionStore) { 12 | if (instance) return instance 13 | 14 | var io = require('socket.io').listen(server); 15 | 16 | io.configure('production', function () { 17 | io.enable('browser client minification'); 18 | io.enable('browser client etag'); 19 | io.enable('browser client gzip'); 20 | io.set('transports', [ 21 | 'websocket', 22 | 'flashsocket', 23 | 'htmlfile', 24 | 'xhr-polling', 25 | 'jsonp-polling' 26 | ]); 27 | io.set('polling duration', 10); 28 | }); 29 | 30 | io.configure('development', function () { 31 | io.set('transports', ['websocket']); 32 | }); 33 | 34 | io.set('authorization', passportSocketIo.authorize({ 35 | cookieParser: express.cookieParser, 36 | key: 'connect.sid', 37 | secret: cfg.sessionSecret, 38 | store: sessionStore 39 | })); 40 | 41 | io.of('/projects-update/status').on('connection', function (socket) { 42 | if (!socket.handshake.user) return socket.emit('error', 'unauthorized!') 43 | 44 | socket.join(socket.handshake.user._id) 45 | 46 | socket.on('start', function() { 47 | var state = socket.handshake.user.projectsUpdater 48 | !state.updating && require('./updater')(socket.handshake.user) 49 | }) 50 | }); 51 | 52 | instance = io 53 | } -------------------------------------------------------------------------------- /app/utils/title-builder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 8/14/13 4 | * Time: 3:51 AM 5 | */ 6 | module.exports = function () { 7 | var manager = {}, 8 | result = [], 9 | title = 'Open Source Supporters', 10 | sep = ' / ' 11 | 12 | manager.add = function (string) { 13 | result.push(string) 14 | } 15 | 16 | manager.render = function () { 17 | var string = result.reverse().join(sep) 18 | result = [title] 19 | return string 20 | } 21 | 22 | manager.add(title) 23 | return manager 24 | }() 25 | -------------------------------------------------------------------------------- /app/utils/updater/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: krasu 3 | * Date: 9/19/13 4 | * Time: 11:20 AM 5 | */ 6 | var mongoose = require('mongoose'), 7 | async = require('async'), 8 | logger = require('winston'), 9 | io = require('../socket.io.js'), 10 | OrgsUpdater = require('./organizations.js'), 11 | ProjectsUpdater = require('./projects.js'), 12 | Q = require('q'), 13 | _ = require('lodash'), 14 | ioNamespace = '/projects-update/status' 15 | 16 | function UserUpdater(user) { 17 | if (!(this instanceof UserUpdater)) 18 | return new UserUpdater(user); 19 | 20 | this.user = user 21 | this.deferred = Q.defer(); 22 | this.reqestParams = { 23 | access_token: this.user.authToken 24 | } 25 | 26 | this.user.projectsUpdater.updated = false 27 | this.user.projectsUpdater.updating = true 28 | this.user.save(_.bind(function () { 29 | this.init() 30 | }, this)) 31 | 32 | return this.deferred.promise 33 | } 34 | 35 | UserUpdater.prototype.progress = function (desc) { 36 | this.deferred.notify(desc); 37 | io().of(ioNamespace).in(this.user._id).emit('progress', desc) 38 | } 39 | 40 | UserUpdater.prototype.finish = function () { 41 | var self = this 42 | this.updateUser('success', function () { 43 | io().of(ioNamespace).in(self.user._id).emit('done') 44 | self.deferred.resolve(); 45 | }) 46 | } 47 | 48 | UserUpdater.prototype.error = function (error) { 49 | var self = this 50 | logger.error(error.msg, error.error) 51 | this.updateUser('error', function () { 52 | io().of(ioNamespace).in(self.user._id).emit('error', error.msg) 53 | self.deferred.reject(error); 54 | }) 55 | } 56 | 57 | UserUpdater.prototype.updateUser = function (status, callback) { 58 | this.user.projectsUpdater.updated = true 59 | this.user.projectsUpdater.updating = false 60 | this.user.projectsUpdater.updatedAt = new Date() 61 | this.user.projectsUpdater.status = status 62 | this.user.save(callback) 63 | } 64 | 65 | UserUpdater.prototype.init = function () { 66 | var self = this 67 | 68 | async.parallel({ 69 | organizations: function (callback) { 70 | (new OrgsUpdater(self.user)).then(function (result) { 71 | callback(null, result) 72 | }, callback, _.bind(self.progress, self)); 73 | }, 74 | projects: function (callback) { 75 | (new ProjectsUpdater(self.user)).then(function (result) { 76 | callback(null, result) 77 | }, callback, _.bind(self.progress, self)); 78 | } 79 | }, function (error, result) { 80 | if (error) return self.error(error) 81 | self.finish() 82 | }) 83 | } 84 | 85 | module.exports = UserUpdater -------------------------------------------------------------------------------- /app/views/404.ejs: -------------------------------------------------------------------------------- 1 | <% titleBuilder.add('Not found') %> 2 | <% include parts/head.ejs %> 3 | 4 |
We're Sorry, but the page you're looking for cannot be found
8 |Oops, something gone wrong
8 |2 | <% if (user) { %> 3 | User 4 | <%= user.username %> 5 | <% } else { %> 6 | Anonymous 7 | <% } %> 8 | 9 | sent a comment your project: 10 | <%= project.owner.username %> / <%= project.name %> 11 |
12 | 13 |14 | <%= message %> 15 |
-------------------------------------------------------------------------------- /app/views/emails/donate-request-maintainer.ejs: -------------------------------------------------------------------------------- 1 |2 | <% if (user) { %> 3 | The GitHub user <%= user.username %> 4 | <% } else { %> 5 | Anonymous 6 | <% } %> 7 | wants to make a donation to 8 | <%= project.owner.username %> / <%= project.name %> on 9 | <%= siteUrl %> 10 |
11 |12 | Please visit <%= siteUrl %> to set how you would like to receive donations. Head to the Maintainers page, locate the repo and edit your donation preferences. 13 |
14 |15 | If you don't want to receive donations then just leave a message in the 'Other' field. This way you're acknowledging the acknowledgement. 16 |
17 |18 | If you don't want to receive any donations requests notifications just click here to unsubscribe. 19 |
20 |21 | I LOVE OPEN SOURCE 22 |
23 | -------------------------------------------------------------------------------- /app/views/emails/donate-request-notify-maintainer.ejs: -------------------------------------------------------------------------------- 1 |2 | <% if (totalCount == 1) { %> 3 | <% if (usersCount) { %> 4 | The GitHub user <%= users[0].username %> 5 | <% } else { %> 6 | Anonymous user 7 | <% } %> 8 | <% } else { %> 9 | <%= totalCount %> of our users 10 | <% } %> 11 | wants to make a donation to your project 12 | <%= project.owner.username %> / <%= project.name %> 13 | on <%= siteUrl %> 14 |
15 |16 | Please visit 17 | <%= siteUrl %> to set how you would like to receive donations. Head to the Maintainers page, locate the repo and edit your donation preferences. 18 |
19 |20 | If you don't want to receive donations then just leave a message in the 'Other' field. This way you're acknowledging the acknowledgement. 21 |
22 | <% if (usersCount) { %> 23 |24 | Registered users: <% _.each(users, function(user, i) { %> 25 | <%= user.username %><%= ((i+1) == usersCount ? '' : ', ') %> 26 | <% }) %> 27 |
28 | <% } %> 29 | <% if (anonsCount) { %> 30 |31 | Anonymous users: <%= anonsCount %> 32 |
33 | <% } %> 34 |35 | I LOVE OPEN SOURCE 36 |
37 | -------------------------------------------------------------------------------- /app/views/emails/donate-request-satisfied.ejs: -------------------------------------------------------------------------------- 1 |2 | You recently wanted to make a donation to <%= project.owner.username %> / <%= project.name %> on <%= siteUrl %> 3 |
4 | 5 |6 | The maintainer of that repo has now set the donation preferences, so now would be a great time to go and make your donation. 7 |
8 | 9 |10 | Click here: <%= project.owner.username %> / <%= project.name %> 11 |
12 | -------------------------------------------------------------------------------- /app/views/emails/donate-request-support.ejs: -------------------------------------------------------------------------------- 1 |2 | <% if (user) { %> 3 | The GitHub user <%= user.username %> 4 | <% } else { %> 5 | Anonymous 6 | <% } %> 7 | wants to make a donation to 8 | <%= project.owner.username %> / <%= project.name %> 9 |
10 |11 | <% if (owner) { %> 12 | Project owned by 13 | <% if (project.owner.type.toLowerCase() == 'user') { %> 14 | our user: <%= project.owner.username %> 15 | <% } else { %> 16 | organization: <%= project.owner.username %> 17 | <% } %> 18 | which didn't specify email! 19 | <% } else { %> 20 | Owner not registered yet, or not claimed this project 21 | <% } %> 22 |
23 |24 | GitHub project link: <%= project.owner.username %> / <%= project.name %> 25 |
26 |27 | You could check all donation requests here <%= siteUrl %>/service/donation-requests 28 |
29 | -------------------------------------------------------------------------------- /app/views/index.ejs: -------------------------------------------------------------------------------- 1 | <% var noHead = true %> 2 | <% var forkMe = true %> 3 | <% var bodyClass = 'home' %> 4 | <% include parts/head.ejs %> 5 |Simple Recognition for Open Source Developers
9 | 10 |All you do is sign in with GitHub and then tell us how you would like people to support your efforts (we've got various options as standard). Then grab the button widget and drop it into your README file or your site.
15 |If you use Open Source software, then giving vocal support really makes a difference to that Open Source Project as well as the open source community as a whole. We make it really easy for you to say what projects you use in your applications.
21 | 22 |Watch a video of how I Love Open Source works.
26 | 27 |