├── .eslintignore ├── .gitignore ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── app.js ├── bin └── assignees ├── circle.yml ├── config ├── mongoose.js └── passport.js ├── controllers ├── admin.js ├── event.js ├── home.js ├── project.js └── user.js ├── docker-compose.yml ├── helpers ├── github.js ├── inspect.js └── logger.js ├── middlewares ├── asset.js ├── error.js └── logger.js ├── models ├── Repository.js └── User.js ├── package.json ├── public ├── css │ ├── assignees │ │ ├── _contact-us.scss │ │ ├── _home.scss │ │ ├── _main.scss │ │ ├── _notifications.scss │ │ ├── _projects.scss │ │ ├── _signin.scss │ │ └── _variables.scss │ ├── lib │ │ ├── bootstrap-select.min.css │ │ ├── bootstrap-social.scss │ │ ├── bootstrap │ │ │ ├── _alerts.scss │ │ │ ├── _badges.scss │ │ │ ├── _breadcrumbs.scss │ │ │ ├── _button-groups.scss │ │ │ ├── _buttons.scss │ │ │ ├── _carousel.scss │ │ │ ├── _close.scss │ │ │ ├── _code.scss │ │ │ ├── _component-animations.scss │ │ │ ├── _dropdowns.scss │ │ │ ├── _forms.scss │ │ │ ├── _glyphicons.scss │ │ │ ├── _grid.scss │ │ │ ├── _input-groups.scss │ │ │ ├── _jumbotron.scss │ │ │ ├── _labels.scss │ │ │ ├── _list-group.scss │ │ │ ├── _media.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modals.scss │ │ │ ├── _navbar.scss │ │ │ ├── _navs.scss │ │ │ ├── _normalize.scss │ │ │ ├── _pager.scss │ │ │ ├── _pagination.scss │ │ │ ├── _panels.scss │ │ │ ├── _popovers.scss │ │ │ ├── _print.scss │ │ │ ├── _progress-bars.scss │ │ │ ├── _responsive-embed.scss │ │ │ ├── _responsive-utilities.scss │ │ │ ├── _scaffolding.scss │ │ │ ├── _tables.scss │ │ │ ├── _theme.scss │ │ │ ├── _thumbnails.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _type.scss │ │ │ ├── _utilities.scss │ │ │ ├── _variables.scss │ │ │ ├── _wells.scss │ │ │ ├── bootstrap.scss │ │ │ └── mixins │ │ │ │ ├── _alerts.scss │ │ │ │ ├── _background-variant.scss │ │ │ │ ├── _border-radius.scss │ │ │ │ ├── _buttons.scss │ │ │ │ ├── _center-block.scss │ │ │ │ ├── _clearfix.scss │ │ │ │ ├── _forms.scss │ │ │ │ ├── _gradients.scss │ │ │ │ ├── _grid-framework.scss │ │ │ │ ├── _grid.scss │ │ │ │ ├── _hide-text.scss │ │ │ │ ├── _image.scss │ │ │ │ ├── _labels.scss │ │ │ │ ├── _list-group.scss │ │ │ │ ├── _nav-divider.scss │ │ │ │ ├── _nav-vertical-align.scss │ │ │ │ ├── _opacity.scss │ │ │ │ ├── _pagination.scss │ │ │ │ ├── _panels.scss │ │ │ │ ├── _progress-bar.scss │ │ │ │ ├── _reset-filter.scss │ │ │ │ ├── _reset-text.scss │ │ │ │ ├── _resize.scss │ │ │ │ ├── _responsive-visibility.scss │ │ │ │ ├── _size.scss │ │ │ │ ├── _tab-focus.scss │ │ │ │ ├── _table-row.scss │ │ │ │ ├── _text-emphasis.scss │ │ │ │ ├── _text-overflow.scss │ │ │ │ └── _vendor-prefixes.scss │ │ ├── font-awesome │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ │ └── octicons │ │ │ └── octicons.css │ ├── main.scss │ └── themes │ │ └── modern │ │ ├── _modern.scss │ │ └── _variables.scss ├── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── bootstrap │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── images │ ├── assignees-projects.png │ ├── enable-a-project.png │ └── pr-review-request.png ├── js │ ├── beacon.js │ └── lib │ │ ├── bootstrap-select.min.js │ │ ├── bootstrap.min.js │ │ └── jquery-2.2.4.min.js └── robots.txt ├── tasks ├── disableProject.js ├── disableProjects.js ├── findReviewers.js ├── listEmails.js ├── listOwners.js ├── listUserFeatures.js └── updateUserFeature.js ├── test ├── app.js ├── event.js ├── fixtures │ ├── pull-request-files-1.json │ ├── repo-collaborators-hateoas.json │ ├── repo-collaborators.json │ ├── repo-commits-annotation-exclusion.json │ ├── repo-commits-driver.json │ ├── repo-commits-exclusion.json │ ├── repo-commits-manager.json │ └── repo-commits-property.json ├── tasks │ └── findReviewers.js └── user.js ├── views ├── _faq.pug ├── account │ └── profile.pug ├── dashboard │ └── index.pug ├── error │ ├── 404.pug │ └── 500.pug ├── global-mixins.pug ├── home.pug ├── layout.pug ├── partials │ ├── flash.pug │ ├── footer.pug │ └── header.pug └── project │ ├── list.pug │ └── mixins.pug └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | public/js/lib/ 2 | test/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | #Build 17 | public/css/main.css 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | 26 | # Editors 27 | .idea 28 | *.iml 29 | 30 | # OS metadata 31 | .DS_Store 32 | Thumbs.db 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.1 2 | 3 | ENV APP_DIR=/usr/src/app 4 | 5 | RUN npm install -g nodemon yarn 6 | 7 | RUN mkdir -p $APP_DIR 8 | 9 | COPY package.json yarn.lock /usr/src/ 10 | WORKDIR /usr/src 11 | RUN yarn install 12 | 13 | ENV NODE_PATH=/usr/src/node_modules 14 | ENV PATH="$PATH:/usr/src/node_modules/.bin" 15 | WORKDIR $APP_DIR 16 | 17 | COPY . $APP_DIR 18 | 19 | CMD [ "nodemon", "--exec", "yarn", "run", "start" ] 20 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:8.1 2 | 3 | ENV APP_DIR=/usr/src/app 4 | 5 | RUN npm install -g nodemon yarn 6 | 7 | RUN mkdir -p $APP_DIR 8 | 9 | COPY package.json yarn.lock /usr/src/ 10 | WORKDIR /usr/src 11 | RUN yarn install 12 | 13 | ENV NODE_PATH=/usr/src/node_modules 14 | ENV PATH="$PATH:/usr/src/node_modules/.bin" 15 | WORKDIR $APP_DIR 16 | 17 | CMD [ "nodemon", "--exec", "yarn", "run", "start" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 TailorDev SAS 4 | Copyright (c) 2014-2016 Sahat Yalkabov (hackathon-starter) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Assignees 2 | ========= 3 | 4 | [![CircleCI](https://circleci.com/gh/TailorDev/assignees.svg?style=svg&circle-token=75bf93c8fc2ccb61e3cb3f07f1444a133bf87eab)](https://circleci.com/gh/TailorDev/assignees) 5 | 6 | Assignees does automatically code review requests on GitHub (currently). 7 | This project is based on the 8 | [hackathon-starter](https://github.com/sahat/hackathon-starter) :heart: 9 | and it is a side project developed in a couple days for our needs (and 10 | also for fun). 11 | 12 |

13 | 14 |

15 | 16 | ## Usage 17 | 18 | ### Web application 19 | 20 | ~The application is available at: https://app.assignees.io/.~ 21 |
:warning: As of April 2018, our public instance is not available anymore, 22 | sorry. 23 | 24 | See the next section if you want to host it yourself. 25 | 26 | ### Command line 27 | 28 | This project provides a command line tool to perform various operations. You can 29 | run it _via_ [Yarn](https://yarnpkg.com/): 30 | 31 | $ yarn run assignees 32 | 33 | In development environment, you should run the command above within a `app` 34 | Docker container: 35 | 36 | $ docker-compose run --rm app yarn run assignees 37 | yarn run v0.19.1 38 | $ node bin/assignees 39 | 40 | Usage: assignees [options] [command] 41 | 42 | 43 | Commands: 44 | 45 | feature:add [username] [feature] add feature to user 46 | feature:remove [username] [feature] remove feature to user 47 | feature:list [username...] list enabled features of user 48 | pr:process [options] [repositoryId] [number] [author] process pull request 49 | 50 | Options: 51 | 52 | -h, --help output usage information 53 | -V, --version output the version number 54 | 55 | ## Installation 56 | 57 | This project provides a [Docker Compose](https://docs.docker.com/compose/) 58 | configuration to quickly build and start it. Clone the project, then run: 59 | 60 | $ docker-compose up -d app 61 | 62 | The application will shortly be accessible at: http://assignees.localdev:3000/. 63 | 64 | **Important:** you must configure your local DNS to be able to use 65 | `assignees.localdev` in the URL. You can edit your `/etc/hosts` file, but it is 66 | recommended to install and configure 67 | [Dnsmasq](https://en.wikipedia.org/wiki/Dnsmasq). [This link is a good 68 | tutorial](https://passingcuriosity.com/2013/dnsmasq-dev-osx/). 69 | 70 | ### Limitations 71 | 72 | The `docker-compose.yml` contains *my* credentials (for an application that does 73 | not exist anymore at the time of writing by the way). You should [register a new 74 | OAuth application](https://github.com/settings/applications/new) on your own. 75 | 76 | It is also recommended to use [ngrok](https://ngrok.com/) if you plan to test 77 | webhooks from your development environment. 78 | 79 | ### Configuration 80 | 81 | This project is configured with environment variables. Here is the list: 82 | 83 | * `ADMIN_IDS`: a comma-separated (without space) list of GitHub user identifiers 84 | (not usernames) to grant admin rights to certain users; 85 | * `APP_DOMAIN`: the domain that points to this application (for redirecting 86 | traffic from Heroku `*.herokuapp.com` domain for instance); 87 | * `GITHUB_ID`: the GitHub ID you get when you register a GitHub (OAuth) 88 | application; 89 | * `GITHUB_SECRET`: the GitHub secret tied to the previous GitHub ID; 90 | * `GITHUB_APP_ID`: the identifier of the application you had to register on 91 | GitHub; 92 | * `GITHUB_WEBHOOK_SECRET`: a randomly generated value (like a password) used to 93 | sign and verify GitHub webhooks requests. 94 | * `GITHUB_WEBHOOK_URL`: the webhook URL that will be registered on each project 95 | enabled by Assignees (_e.g._ `https://app.assignees.io/events`); 96 | * `MONGODB_URI`: the MongoDB data source name (_e.g._ `mongodb://user:pass@server/db`). 97 | This variable is automatically set if you use the _mLab_ add-on on Heroku. 98 | Alternatively, you can use the `MONGOLAB_URI` environment variable; 99 | * `SESSION_SECRET`: a randomly generated value to prevent session tampering. 100 | 101 | Also, setting `NODE_ENV` to `production` looks like a good idea. 102 | 103 | **N.B.** because of the Yarn incompatibility on Heroku, I set 104 | `NODE_MODULES_CACHE` to `false` and `NPM_CONFIG_PRODUCTION` to `true`. 105 | 106 | #### New Relic 107 | 108 | To monitor the application at an early stage, it is possible to use New Relic 109 | APM. These extra environment variables could be useful (at least on Heroku): 110 | 111 | * `NEW_RELIC_APP_NAME`; 112 | * `NEW_RELIC_LICENSE_KEY`; 113 | * `NEW_RELIC_LOG` (automatically set by Heroku when enabling the add-on); 114 | * `NEW_RELIC_NO_CONFIG_FILE` should be set to `true`; 115 | * `NEW_RELIC_SKIP_NATIVE_METRICS` should be set to `true`. 116 | 117 | **Note:** [this commit](https://github.com/TailorDev/assignees/commit/f1d0e6657a2676ddf79c93c2da170363f926b71f) 118 | has disabled New Relic on this project. You can re-enable it by yourself by 119 | requiring the `newrelic` package in both the `package.json` file and the code. 120 | 121 | #### Other settings 122 | 123 | You may want to change the name of the project, the contact email address, or 124 | the Piwik tracking code by editing the [`views/global-mixins.pug` 125 | template](https://github.com/TailorDev/assignees/blob/master/views/global-mixins.pug). 126 | 127 | ## Running the Tests 128 | 129 | $ [docker-compose run --rm app] yarn run test 130 | 131 | ## License 132 | 133 | Assignees is released under the MIT License. See the bundled 134 | [LICENSE](LICENSE.md) file for details. 135 | -------------------------------------------------------------------------------- /bin/assignees: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander'); 3 | const chalk = require('chalk'); 4 | 5 | const logger = require('../helpers/logger').prependableLogger( 6 | console.log, 7 | chalk.green('SUCCESS'), 8 | chalk.red('NOPE') 9 | ); 10 | const mongoose = require('../config/mongoose')(logger); 11 | 12 | const findReviewersTask = require('../tasks/findReviewers'); 13 | const listUserFeaturesTask = require('../tasks/listUserFeatures'); 14 | const updateUserFeatureTask = require('../tasks/updateUserFeature'); 15 | const disableProjectTask = require('../tasks/disableProject'); 16 | const disableProjectsTask = require('../tasks/disableProjects'); 17 | const listOwnersTask = require('../tasks/listOwners'); 18 | const listEmailsTask = require('../tasks/listEmails'); 19 | 20 | const updateUserFeatureAction = operation => async (username, feature) => { 21 | const updateUserFeature = updateUserFeatureTask.configure({ logger }); 22 | try { 23 | await updateUserFeature(username, feature, operation); 24 | } catch(e) { 25 | logger.error(chalk.red(e.stack)); 26 | process.exitCode = 1; 27 | } finally { 28 | mongoose.connection.close(); 29 | } 30 | }; 31 | 32 | program 33 | .version(require('../package.json').version) 34 | ; 35 | 36 | program 37 | .command('feature:add [username] [feature]') 38 | .description('add feature to user') 39 | .action(updateUserFeatureAction(updateUserFeatureTask.ADD)) 40 | ; 41 | 42 | program 43 | .command('feature:remove [username] [feature]') 44 | .description('remove feature to user') 45 | .action(updateUserFeatureAction(updateUserFeatureTask.REMOVE)) 46 | ; 47 | 48 | program 49 | .command('feature:list [username...]') 50 | .description('list enabled features of user') 51 | .action(async (usernames) => { 52 | const listUserFeatures = listUserFeaturesTask.configure({ logger }); 53 | 54 | try { 55 | await Promise.all(usernames.map(username => listUserFeatures(username))); 56 | } catch(e) { 57 | logger.error(chalk.red(e.stack)); 58 | process.exitCode = 1; 59 | } finally { 60 | mongoose.connection.close(); 61 | } 62 | }) 63 | ; 64 | 65 | program 66 | .command('pr:process [repositoryId] [number] [author]') 67 | .description('process pull request') 68 | .option('--dryrun', 'do not create review request') 69 | .action(async (repositoryId, number, author, options) => { 70 | const dryrun = options.dryrun || false; 71 | 72 | if (dryrun) { 73 | logger.info('DRY RUN mode enabled'); 74 | } 75 | 76 | const findReviewers = findReviewersTask.configure({ 77 | maxPullRequestFilesToProcess: process.env.maxPullRequestFilesToProcess || 5, 78 | nbCommitsToRetrieve: process.env.nbCommitsToRetrieve || 30, 79 | createReviewRequest: dryrun === false, 80 | }); 81 | 82 | try { 83 | await findReviewers(repositoryId, number, author, logger); 84 | 85 | if (!dryrun) { 86 | logger.info('reviewer(s) assigned'); 87 | } 88 | } catch(e) { 89 | logger.error(chalk.red(e.stack)); 90 | process.exitCode = 1; 91 | } finally { 92 | mongoose.connection.close(); 93 | } 94 | }) 95 | ; 96 | 97 | program 98 | .command('project:disable [owner] [repo]') 99 | .description('disable a project completely') 100 | .option('--force', 'mark a project as disabled') 101 | .action(async (owner, repo, { force }) => { 102 | const disableProject = disableProjectTask.configure({ logger }); 103 | 104 | try { 105 | await disableProject({ owner, repo, force }); 106 | } catch(e) { 107 | logger.error(chalk.red(e.stack)); 108 | process.exitCode = 1; 109 | } finally { 110 | mongoose.connection.close(); 111 | } 112 | }) 113 | ; 114 | 115 | program 116 | .command('project:disable-all [owner]') 117 | .description('disable all projects of a user or organization completely') 118 | .action(async (owner, repo) => { 119 | const disableProjects = disableProjectsTask.configure({ logger }); 120 | 121 | try { 122 | await disableProjects(owner); 123 | } catch(e) { 124 | logger.error(chalk.red(e.stack)); 125 | process.exitCode = 1; 126 | } finally { 127 | mongoose.connection.close(); 128 | } 129 | }) 130 | ; 131 | 132 | program 133 | .command('project:list-owners') 134 | .description('list all the owners in database') 135 | .option('--as-list', 'output as list separated by spaces') 136 | .action(async (options) => { 137 | const listOwners = listOwnersTask.configure({ logger }); 138 | 139 | try { 140 | await listOwners(options.asList); 141 | } catch(e) { 142 | logger.error(chalk.red(e.stack)); 143 | process.exitCode = 1; 144 | } finally { 145 | mongoose.connection.close(); 146 | } 147 | }) 148 | ; 149 | 150 | program 151 | .command('project:list-emails') 152 | .description('list all the emails in database') 153 | .option('--as-list', 'output as list separated by spaces') 154 | .action(async (options) => { 155 | const listEmails = listEmailsTask.configure({ logger }); 156 | 157 | try { 158 | await listEmails(options.asList); 159 | } catch(e) { 160 | logger.error(chalk.red(e.stack)); 161 | process.exitCode = 1; 162 | } finally { 163 | mongoose.connection.close(); 164 | } 165 | }) 166 | ; 167 | 168 | program.parse(process.argv); 169 | 170 | if (!program.args.length) { 171 | program.help(); 172 | } 173 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 8.1 4 | environment: 5 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 6 | MONGODB_URI: mongodb://localhost:27017/assignees 7 | GITHUB_ID: 123 8 | GITHUB_SECRET: 123 9 | SESSION_SECRET: s3cr3t 10 | GITHUB_WEBHOOK_URL: http://assignees.localdev:3000/events 11 | GITHUB_WEBHOOK_SECRET: s3cr3t 12 | 13 | dependencies: 14 | override: 15 | - yarn 16 | cache_directories: 17 | - ~/.cache/yarn 18 | 19 | test: 20 | override: 21 | - yarn test 22 | -------------------------------------------------------------------------------- /config/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | module.exports = (logger) => { 4 | mongoose.Promise = global.Promise; 5 | mongoose.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI); 6 | mongoose.connection.on('error', () => { 7 | logger.error('✗ MongoDB connection error. Please make sure MongoDB is running.'); 8 | process.exit(); 9 | }); 10 | 11 | return mongoose; 12 | }; 13 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const GitHubStrategy = require('passport-github').Strategy; 3 | 4 | const User = require('../models/User'); 5 | 6 | passport.serializeUser((user, done) => { 7 | done(null, user.id); 8 | }); 9 | 10 | passport.deserializeUser((id, done) => { 11 | User.findById(id, (err, user) => { 12 | done(err, user); 13 | }); 14 | }); 15 | 16 | /** 17 | * Sign in with GitHub. 18 | */ 19 | passport.use(new GitHubStrategy({ 20 | clientID: process.env.GITHUB_ID, 21 | clientSecret: process.env.GITHUB_SECRET, 22 | callbackURL: '/auth/github/callback', 23 | passReqToCallback: true, 24 | }, (req, accessToken, refreshToken, profile, done) => { 25 | if (req.user) { 26 | if (req.user.hasGitHubScopes(req.session.scopes)) { 27 | return done(null, req.user); 28 | } 29 | 30 | // when not all *required* scopes have been granted, we need to 31 | // "reset" user info so that we are sure to use the right token. 32 | 33 | req.user.tokens = [{ 34 | kind: 'github', 35 | accessToken, 36 | scopes: req.session.scopes, 37 | }]; 38 | req.user.repositories = []; 39 | req.user.organizations = []; 40 | req.user.last_synchronized_at = null; 41 | 42 | return req.user.save((err) => { 43 | req.flash('info', { msg: 'We have updated your information to reflect new GitHub permissions.' }); 44 | done(err, req.user); 45 | }); 46 | } 47 | 48 | User.findOne({ github: profile.id }, (err, existingUser) => { 49 | if (err) { 50 | return done(err); 51 | } 52 | 53 | if (existingUser) { 54 | if (existingUser.hasGitHubScopes(req.session.scopes)) { 55 | return done(null, existingUser); 56 | } 57 | 58 | // when not all *required* scopes have been granted, we need to 59 | // "reset" user info so that we are sure to use the right token. 60 | 61 | existingUser.tokens = [{ 62 | kind: 'github', 63 | accessToken, 64 | scopes: req.session.scopes, 65 | }]; 66 | existingUser.repositories = []; 67 | existingUser.organizations = []; 68 | existingUser.last_synchronized_at = null; 69 | 70 | return existingUser.save((err) => { 71 | req.flash('info', { msg: 'We have updated your information to reflect new GitHub permissions.' }); 72 | done(err, existingUser); 73 | }); 74 | } 75 | 76 | const user = new User(); 77 | 78 | user.github = profile.id; 79 | user.github_login = profile._json.login; 80 | user.email = profile._json.email; 81 | user.tokens = [{ 82 | kind: 'github', 83 | accessToken, 84 | scopes: req.session.scopes, 85 | }]; 86 | user.profile.name = profile.displayName; 87 | user.profile.picture = profile._json.avatar_url; 88 | user.profile.location = profile._json.location; 89 | user.profile.website = profile._json.blog; 90 | 91 | user.save((err) => { 92 | done(err, user); 93 | }); 94 | }); 95 | })); 96 | 97 | /** 98 | * Login Required middleware. 99 | */ 100 | exports.isAuthenticated = function isAuthenticated(req, res, next) { 101 | if (req.isAuthenticated()) { 102 | return next(); 103 | } 104 | res.redirect('/'); 105 | }; 106 | 107 | /** 108 | * Authorization Required middleware. 109 | */ 110 | exports.isAdmin = function isAdmin(req, res, next) { 111 | if (req.isAuthenticated() && req.user.isAdmin()) { 112 | return next(); 113 | } 114 | 115 | res.redirect('/404'); 116 | }; 117 | -------------------------------------------------------------------------------- /controllers/admin.js: -------------------------------------------------------------------------------- 1 | const Repository = require('../models/Repository'); 2 | const User = require('../models/User'); 3 | 4 | /** 5 | * Dashboard 6 | */ 7 | exports.index = async (req, res) => { 8 | const nbRepos = await Repository.count().catch(null); 9 | const nbReposEnabled = await Repository.count({ enabled: true, }).catch(null); 10 | const nbUsers = await User.count().catch(null); 11 | const nbUsersWithPrivateAccess = await User.count({ 12 | 'tokens.scopes': { $in: ['repo'] }, 13 | }).catch(null); 14 | 15 | return res.render('dashboard/index', { 16 | title: 'Dashboard', 17 | nbRepos, 18 | nbUsers, 19 | nbUsersWithPrivateAccess, 20 | nbReposEnabled, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /controllers/event.js: -------------------------------------------------------------------------------- 1 | const findReviewersTask = require('../tasks/findReviewers'); 2 | 3 | const findReviewers = findReviewersTask.configure({ 4 | maxPullRequestFilesToProcess: process.env.maxPullRequestFilesToProcess || 5, 5 | nbCommitsToRetrieve: process.env.nbCommitsToRetrieve || 30, 6 | createReviewRequest: true, 7 | }); 8 | 9 | /** 10 | * Listen to GitHub events 11 | */ 12 | exports.listen = async (req, res) => { 13 | if (!req.header('x-github-event')) { 14 | return res.status(400).end(); 15 | } 16 | 17 | if (req.header('x-github-event') === 'ping') { 18 | return res.send('PONG'); 19 | } 20 | 21 | if (req.header('x-github-event') !== 'pull_request') { 22 | return res.send({ status: 'ignored', reason: 'I do not listen to such events' }); 23 | } 24 | 25 | if (req.body.action !== 'opened') { 26 | return res.send({ status: 'ignored', reason: 'action is not "opened"' }); 27 | } 28 | 29 | await findReviewers( 30 | req.body.repository.id, 31 | req.body.pull_request.number, 32 | req.body.pull_request.user.login, 33 | req.logger 34 | ); 35 | 36 | res.send({ status: 'ok' }); 37 | }; 38 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET / 3 | * Home page. 4 | */ 5 | exports.index = (req, res) => { 6 | res.render('home', { 7 | title: 'Automatically request code reviews on GitHub', 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /controllers/user.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | 3 | /** 4 | * GET /logout 5 | * Log out. 6 | */ 7 | exports.logout = (req, res) => { 8 | req.logout(); 9 | res.redirect('/'); 10 | }; 11 | 12 | /** 13 | * GET /account 14 | * Profile page. 15 | */ 16 | exports.getAccount = (req, res) => { 17 | res.render('account/profile', { 18 | title: 'Account Management', 19 | }); 20 | }; 21 | 22 | /** 23 | * POST /account/profile 24 | * Update profile information. 25 | */ 26 | exports.postUpdateProfile = (req, res, next) => { 27 | req.assert('email', 'Please enter a valid email address.').isEmail(); 28 | req.sanitize('email').normalizeEmail({ remove_dots: false }); 29 | 30 | const errors = req.validationErrors(); 31 | 32 | if (errors) { 33 | req.flash('errors', errors); 34 | return res.redirect('/account'); 35 | } 36 | 37 | User.findById(req.user.id, (err, user) => { 38 | if (err) { 39 | return next(err); 40 | } 41 | 42 | user.email = req.body.email || ''; 43 | user.profile.name = req.body.name || ''; 44 | user.profile.location = req.body.location || ''; 45 | user.profile.website = req.body.website || ''; 46 | 47 | user.save((err) => { 48 | if (err) { 49 | if (err.code === 11000) { 50 | req.flash('errors', { msg: 'The email address you have entered is already associated with an account.' }); 51 | return res.redirect('/account'); 52 | } 53 | 54 | return next(err); 55 | } 56 | 57 | req.flash('success', { msg: 'Profile information has been updated.' }); 58 | return res.redirect('/account'); 59 | }); 60 | }); 61 | }; 62 | 63 | /** 64 | * POST /account/delete 65 | * Delete user account. 66 | */ 67 | exports.postDeleteAccount = (req, res, next) => { 68 | User.remove({ _id: req.user.id }, (err) => { 69 | if (err) { return next(err); } 70 | req.logout(); 71 | req.flash('info', { msg: 'Your account has been deleted.' }); 72 | res.redirect('/'); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | ports: 9 | - 3000:3000 10 | volumes: 11 | - .:/usr/src/app 12 | environment: 13 | APP_DOMAIN: assignees.localdev 14 | MONGODB_URI: mongodb://db:27017/assignees 15 | GITHUB_ID: e31abf9f323d5fc9085e 16 | GITHUB_SECRET: 10bc7b2bf9fc5948812d7d565e4ef259d6550179 17 | SESSION_SECRET: s3cr3t 18 | GITHUB_WEBHOOK_URL: http://assignees.localdev:3000/events 19 | GITHUB_WEBHOOK_SECRET: Th4tIsS3cr3t 20 | ADMIN_IDS: 217628 21 | GITHUB_APP_ID: e31abf9f323d5fc9085e 22 | networks: 23 | public: 24 | aliases: ['assignees.localdev'] 25 | private: ~ 26 | depends_on: 27 | - db 28 | 29 | db: 30 | image: mongo:3.4 31 | networks: 32 | private: ~ 33 | 34 | networks: 35 | public: ~ 36 | private: ~ 37 | -------------------------------------------------------------------------------- /helpers/github.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const GitHub = require('github'); 3 | 4 | exports.auth = (user) => { 5 | const gh = new GitHub(); 6 | 7 | gh.authenticate({ 8 | type: 'oauth', 9 | token: user.getGitHubToken(), 10 | }); 11 | 12 | return gh; 13 | }; 14 | 15 | exports.getWebhookConfig = (owner, repo, active) => ({ 16 | owner, 17 | repo, 18 | name: 'web', 19 | config: { 20 | url: process.env.GITHUB_WEBHOOK_URL, 21 | content_type: 'json', 22 | secret: process.env.GITHUB_WEBHOOK_SECRET, 23 | }, 24 | events: ['pull_request'], 25 | active, 26 | }); 27 | 28 | exports.getExistingWebhookConfig = (id, owner, repo, active) => Object.assign( 29 | {}, 30 | module.exports.getWebhookConfig(owner, repo, active), 31 | { id } 32 | ); 33 | 34 | exports.verifySignature = (req, res, buffer) => { 35 | if (req.path !== '/events') { 36 | return; 37 | } 38 | 39 | [ 40 | 'x-hub-signature', 41 | 'x-github-event', 42 | 'x-github-delivery', 43 | ].forEach((header) => { 44 | if (!req.headers[header]) { 45 | throw new Error(`Header ${header} is missing.`); 46 | } 47 | }); 48 | 49 | const expected = req.headers['x-hub-signature']; 50 | const computed = `sha1=${crypto.createHmac('sha1', process.env.GITHUB_WEBHOOK_SECRET).update(buffer).digest('hex')}`; 51 | 52 | if (expected !== computed) { 53 | throw new Error('Invalid signature'); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /helpers/inspect.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | module.exports = object => util.inspect(object, { breakLength: Infinity }); 4 | -------------------------------------------------------------------------------- /helpers/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const prependMessage = (log, pre) => (...args) => { 4 | const message = args.shift(); 5 | 6 | log(`${pre} ${message}`, ...args); 7 | }; 8 | 9 | // basic logger 10 | const prependableLogger = (log, preInfo, preError) => ({ 11 | info: (...args) => prependMessage(log, preInfo)(...args), 12 | error: (...args) => prependMessage(log, preError)(...args), 13 | }); 14 | 15 | const nullLogger = { 16 | info: () => {}, 17 | error: () => {}, 18 | }; 19 | 20 | const withRequestId = (logger, requestId) => { 21 | if (!requestId) { 22 | return logger; 23 | } 24 | 25 | return { 26 | info: (...args) => prependMessage(logger.info, `request_id=${requestId}`)(...args), 27 | error: (...args) => prependMessage(logger.error, `request_id=${requestId}`)(...args), 28 | }; 29 | }; 30 | 31 | exports.prependableLogger = prependableLogger; 32 | 33 | exports.withRequestId = withRequestId; 34 | 35 | exports.consoleLogger = (env) => { 36 | if (env === 'test') { 37 | return nullLogger; 38 | } 39 | 40 | return prependableLogger(console.log, '[info]', '[error]'); 41 | }; 42 | -------------------------------------------------------------------------------- /middlewares/asset.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json'); 2 | 3 | module.exports = () => { 4 | if (process.env.NODE_ENV === 'production') { 5 | const version = pkg.version; 6 | 7 | return url => `${url}?v=${version}`; 8 | } 9 | 10 | return url => url; 11 | }; 12 | -------------------------------------------------------------------------------- /middlewares/error.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | const util = require('util'); 3 | 4 | const inspect = require('../helpers/inspect'); 5 | 6 | module.exports = (err, req, res, next) => { 7 | const info = []; 8 | 9 | if (req.user) { 10 | info.push(`user_id=${req.user.id}`); 11 | info.push(`user_login=${req.user.github_login}`); 12 | info.push(`user_github_id=${req.user.github}`); 13 | } 14 | 15 | info.push([ 16 | `request_method=${req.method}`, 17 | `request_body=${inspect(req.body)}`, 18 | `request_headers=${inspect(req.headers)}`, 19 | ].join(' ')); 20 | 21 | Object.getOwnPropertyNames(err).forEach( 22 | k => info.push(`error_${k}=${inspect(err[k])}`) 23 | ); 24 | 25 | req.logger.error(info.join(' ')); 26 | 27 | return res.format({ 28 | json: () => { 29 | res.status(err.statusCode || 500).send({ 30 | message: 'Server Error', 31 | }); 32 | }, 33 | html: () => { 34 | res.status(500).render('error/500', { 35 | title: 'Server Error', 36 | }); 37 | }, 38 | default: () => { 39 | res.status(406).send('Not Acceptable'); 40 | }, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /middlewares/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: 0 */ 2 | const { withRequestId } = require('../helpers/logger'); 3 | 4 | module.exports = logger => (req, res, next) => { 5 | req.logger = withRequestId(logger, req.id); 6 | next(); 7 | }; 8 | -------------------------------------------------------------------------------- /models/Repository.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const octicons = require('octicons'); 3 | 4 | const repositorySchema = new mongoose.Schema({ 5 | name: String, 6 | owner: String, 7 | 8 | // github 9 | github_id: Number, 10 | private: Boolean, 11 | fork: Boolean, 12 | github_hook_id: Number, 13 | 14 | // assignees config 15 | enabled: { type: Boolean, default: false }, 16 | enabled_by: { 17 | user_id: mongoose.Schema.Types.ObjectId, 18 | login: String, 19 | }, 20 | max_reviewers: { type: Number, default: 1 }, 21 | }); 22 | 23 | repositorySchema.methods.getIconSVG = function getIconSVG() { 24 | let icon = 'repo'; 25 | if (this.fork === true) { 26 | icon = 'repo-forked'; 27 | } 28 | 29 | if (this.private === true) { 30 | icon = 'lock'; 31 | } 32 | 33 | return octicons[icon].toSVG(); 34 | }; 35 | 36 | repositorySchema.methods.getURL = function getURL() { 37 | return `https://github.com/${this.owner}/${this.name}`; 38 | }; 39 | 40 | repositorySchema.statics.findOneByGitHubId = function findOneByGitHubId(id) { 41 | return this.findOne({ github_id: id }).catch(() => null); 42 | }; 43 | 44 | const Repository = mongoose.model('Repository', repositorySchema); 45 | 46 | module.exports = Repository; 47 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const mongoose = require('mongoose'); 3 | 4 | const organizationSchema = new mongoose.Schema({ 5 | name: String, 6 | description: String, 7 | github_id: Number, 8 | avatar_url: String, 9 | last_synchronized_at: Date, 10 | }); 11 | 12 | const userSchema = new mongoose.Schema({ 13 | email: String, 14 | 15 | tokens: Array, 16 | 17 | github: { type: String, unique: true }, 18 | github_login: String, 19 | 20 | repositories: Array, 21 | organizations: [organizationSchema], 22 | last_synchronized_at: Date, 23 | 24 | profile: { 25 | name: String, 26 | location: String, 27 | website: String, 28 | picture: String, 29 | }, 30 | 31 | features: Array, 32 | }, { timestamps: true }); 33 | 34 | /** 35 | * Helper method for getting user's gravatar. 36 | */ 37 | userSchema.methods.gravatar = function gravatar(size) { 38 | const s = size || 200; 39 | 40 | if (!this.email) { 41 | return `https://gravatar.com/avatar/?s=${s}&d=retro`; 42 | } 43 | 44 | const md5 = crypto.createHash('md5').update(this.email).digest('hex'); 45 | 46 | return `https://gravatar.com/avatar/${md5}?s=${s}&d=retro`; 47 | }; 48 | 49 | userSchema.methods.isAdmin = function isAdmin() { 50 | const admins = process.env.ADMIN_IDS ? process.env.ADMIN_IDS.split(',') : []; 51 | 52 | return this.github && admins.includes(this.github); 53 | }; 54 | 55 | userSchema.methods.canSee = function canSee(repository) { 56 | return this.repositories.includes(repository.github_id); 57 | }; 58 | 59 | userSchema.methods.getGitHubToken = function getGitHubToken() { 60 | return this.tokens.find(t => t.kind === 'github').accessToken || null; 61 | }; 62 | 63 | // inclusion is checked here, not equality 64 | userSchema.methods.hasGitHubScopes = function hasGitHubScopes(scopes) { 65 | const token = this.tokens.find(t => t.kind === 'github'); 66 | const userScopes = token ? (token.scopes || []) : []; 67 | 68 | return scopes.filter(s => userScopes.includes(s) !== true).length === 0; 69 | }; 70 | 71 | userSchema.methods.hasAccessTo = function hasAccessTo(feature) { 72 | if (!this.features || this.features.length === 0) { 73 | return false; 74 | } 75 | 76 | return this.features.includes(feature); 77 | }; 78 | 79 | userSchema.methods.getUsername = function getUsername() { 80 | return this.github_login; 81 | }; 82 | 83 | userSchema.statics.findOneById = function findOneById(id) { 84 | return this.findOne({ _id: id }).catch(() => null); 85 | }; 86 | 87 | const User = mongoose.model('User', userSchema); 88 | 89 | module.exports = User; 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assignees", 3 | "version": "1.2.0", 4 | "description": "Code reviews for everyone.", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node --harmony app.js", 9 | "test": "NODE_ENV=test mocha --harmony --reporter spec --recursive", 10 | "assignees": "node bin/assignees" 11 | }, 12 | "dependencies": { 13 | "async": "^2.1.2", 14 | "bcrypt-nodejs": "^0.0.3", 15 | "body-parser": "^1.15.2", 16 | "chalk": "^1.1.3", 17 | "cheerio": "^0.22.0", 18 | "clockwork": "^0.1.4", 19 | "commander": "~2.9.0", 20 | "compression": "^1.6.2", 21 | "connect-mongo": "^1.3.2", 22 | "d3-format": "~1.0.2", 23 | "deck": "~0.0.4", 24 | "express": "^4.14.0", 25 | "express-flash": "^0.0.2", 26 | "express-request-id": "~1.2.0", 27 | "express-session": "^1.14.2", 28 | "express-sslify": "~1.2.0", 29 | "express-status-monitor": "^0.1.5", 30 | "express-validator": "^3.1.2", 31 | "github": "8.1.0", 32 | "lastfm": "^0.9.2", 33 | "lob": "^3.9.0", 34 | "lodash": "^4.16.6", 35 | "lusca": "^1.4.1", 36 | "moment": "^2.22.1", 37 | "mongoose": "^4.6.6", 38 | "multer": "^1.2.0", 39 | "node-sass": "^4.5.3", 40 | "node-sass-middleware": "^0.10.0", 41 | "nodemailer": "^2.6.4", 42 | "octicons": "~5.0.1", 43 | "passport": "0.3.2", 44 | "passport-github": "^1.1.0", 45 | "passport-local": "^1.0.0", 46 | "passport-oauth": "^1.0.0", 47 | "pug": "^2.0.0-beta6", 48 | "request": "^2.78.0", 49 | "validator": "^6.1.0" 50 | }, 51 | "devDependencies": { 52 | "chai": "^3.5.0", 53 | "errorhandler": "~1.5.0", 54 | "eslint": "^3.9.1", 55 | "eslint-config-airbnb-base": "^11.0.0", 56 | "eslint-plugin-import": "^2.1.0", 57 | "mocha": "^3.1.2", 58 | "morgan": "~1.7.0", 59 | "nock": "~9.0.2", 60 | "sinon": "^1.17.6", 61 | "sinon-as-promised": "~4.0.2", 62 | "sinon-mongoose": "^1.3.0", 63 | "supertest": "^2.0.1" 64 | }, 65 | "eslintConfig": { 66 | "extends": "airbnb-base", 67 | "rules": { 68 | "consistent-return": 0, 69 | "comma-dangle": 0, 70 | "no-param-reassign": 0, 71 | "no-underscore-dangle": 0, 72 | "no-shadow": 0, 73 | "no-plusplus": 0, 74 | "max-len": [ 75 | "error", 76 | 120 77 | ] 78 | } 79 | }, 80 | "engines": { 81 | "node": "8.x || 9.x", 82 | "yarn": "1.x" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /public/css/assignees/_contact-us.scss: -------------------------------------------------------------------------------- 1 | .contact-us { 2 | padding: 0 4px 0 4px; 3 | vertical-align: inherit; 4 | font-size: inherit; 5 | } 6 | -------------------------------------------------------------------------------- /public/css/assignees/_home.scss: -------------------------------------------------------------------------------- 1 | .feature { 2 | .fa { 3 | color: $brand-primary; 4 | } 5 | } 6 | 7 | .call-for-action { 8 | margin-top: 12%; 9 | margin-bottom: 10%; 10 | 11 | text-align: center; 12 | 13 | button { 14 | @extend .btn; 15 | @extend .btn-primary; 16 | @extend .btn-lg; 17 | } 18 | } 19 | 20 | .screenshots { 21 | > button { 22 | background-color: transparent; 23 | border: 0; 24 | } 25 | 26 | .screenshot-projects img { 27 | width: 1000px; 28 | } 29 | } 30 | 31 | .big-picture { 32 | margin-top: 5%; 33 | margin-bottom: 5%; 34 | } 35 | -------------------------------------------------------------------------------- /public/css/assignees/_main.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | // components 4 | @import './signin'; 5 | @import './notifications'; 6 | @import './contact-us'; 7 | 8 | // pages 9 | @import './home'; 10 | @import './projects'; 11 | -------------------------------------------------------------------------------- /public/css/assignees/_notifications.scss: -------------------------------------------------------------------------------- 1 | .notifications { 2 | display: block; 3 | width: 100%; 4 | 5 | text-align: center; 6 | 7 | .alert { 8 | margin: 0; 9 | padding: 8px 13px; 10 | 11 | border: 0; 12 | border-radius: 0; 13 | 14 | font-size: 12px; 15 | } 16 | 17 | .alert-info { 18 | .btn { 19 | font-size: inherit!important; 20 | } 21 | 22 | a { 23 | color: inherit; 24 | text-decoration: underline; 25 | } 26 | 27 | a:hover, 28 | a:focus { 29 | color: $gray-darker; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/css/assignees/_projects.scss: -------------------------------------------------------------------------------- 1 | .organizations { 2 | @extend .list-group; 3 | 4 | a { 5 | @extend .list-group-item; 6 | 7 | img { 8 | @extend .img-circle; 9 | 10 | width: 50px; 11 | } 12 | 13 | .name { 14 | margin-left: 1rem; 15 | font-weight: 500; 16 | } 17 | } 18 | } 19 | 20 | .repositories { 21 | @extend .list-group; 22 | 23 | .repository { 24 | @extend .row; 25 | @extend .list-group-item; 26 | 27 | margin: 0; 28 | padding-left: 0; 29 | padding-right: 0; 30 | 31 | font-weight: 500; 32 | 33 | &.is-private { 34 | background-color: $private-repository-color; 35 | } 36 | 37 | &-name { 38 | margin: 5px 0 6px 0px; 39 | 40 | a { 41 | color: $gray-darker; 42 | 43 | &:focus, 44 | &:hover { 45 | color: $brand-primary; 46 | } 47 | } 48 | } 49 | 50 | &-configuration { 51 | margin-top: 1rem; 52 | 53 | form { 54 | > * { 55 | padding-right: 1rem; 56 | } 57 | 58 | label { 59 | margin-bottom: 0; 60 | font-weight: 400; 61 | } 62 | 63 | .max-reviewers-input { 64 | width: 50px; 65 | } 66 | } 67 | } 68 | 69 | .label { 70 | display: inline-block; 71 | max-width: 80%; 72 | 73 | overflow: hidden; 74 | text-overflow: ellipsis; 75 | text-transform: uppercase; 76 | vertical-align: text-bottom; 77 | 78 | background-color: $gray-darker; 79 | } 80 | 81 | .actions { 82 | text-align: right; 83 | padding-left: 0; 84 | } 85 | } 86 | 87 | .no-repository { 88 | @extend .panel; 89 | @extend .panel-default; 90 | 91 | list-style-type: none; 92 | } 93 | } 94 | 95 | .last-sync { 96 | @extend .row; 97 | @extend .hidden-xs; 98 | 99 | margin-bottom: 20px; 100 | 101 | & > form { 102 | @extend .col-md-12; 103 | } 104 | 105 | .btn-link { 106 | padding: 0 5px 0 0; 107 | vertical-align: initial; 108 | } 109 | } 110 | 111 | .application-link { 112 | margin-bottom: 2rem; 113 | } 114 | 115 | @media (max-width: 768px) { 116 | .organizations .active { 117 | border-top-left-radius: 4px; 118 | border-top-right-radius: 4px; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /public/css/assignees/_signin.scss: -------------------------------------------------------------------------------- 1 | .sign-in { 2 | strong { 3 | padding: 2px; 4 | background-color: $private-repository-color; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/css/assignees/_variables.scss: -------------------------------------------------------------------------------- 1 | $private-repository-color: #fff9ea; 2 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap-select.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.12.1 (http://silviomoreto.github.io/bootstrap-select) 3 | * 4 | * Copyright 2013-2016 bootstrap-select 5 | * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) 6 | */select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\9}.bootstrap-select>.dropdown-toggle{width:100%;padding-right:25px;z-index:1}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle{border-color:#b94a48}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select.btn-group:not(.input-group-btn),.bootstrap-select.btn-group[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.btn-group.dropdown-menu-right,.bootstrap-select.btn-group[class*=col-].dropdown-menu-right,.row .bootstrap-select.btn-group[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select.btn-group,.form-horizontal .bootstrap-select.btn-group,.form-inline .bootstrap-select.btn-group{margin-bottom:0}.form-group-lg .bootstrap-select.btn-group.form-control,.form-group-sm .bootstrap-select.btn-group.form-control{padding:0}.form-inline .bootstrap-select.btn-group .form-control{width:100%}.bootstrap-select.btn-group.disabled,.bootstrap-select.btn-group>.disabled{cursor:not-allowed}.bootstrap-select.btn-group.disabled:focus,.bootstrap-select.btn-group>.disabled:focus{outline:0!important}.bootstrap-select.btn-group.bs-container{position:absolute;height:0!important;padding:0!important}.bootstrap-select.btn-group.bs-container .dropdown-menu{z-index:1060}.bootstrap-select.btn-group .dropdown-toggle .filter-option{display:inline-block;overflow:hidden;width:100%;text-align:left}.bootstrap-select.btn-group .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.bootstrap-select.btn-group[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select.btn-group .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select.btn-group .dropdown-menu li{position:relative}.bootstrap-select.btn-group .dropdown-menu li.active small{color:#fff}.bootstrap-select.btn-group .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select.btn-group .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select.btn-group .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select.btn-group .dropdown-menu li a span.check-mark{display:none}.bootstrap-select.btn-group .dropdown-menu li a span.text{display:inline-block}.bootstrap-select.btn-group .dropdown-menu li small{padding-left:.5em}.bootstrap-select.btn-group .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option{position:static}.bootstrap-select.btn-group.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark{position:absolute;display:inline-block;right:15px;margin-top:5px}.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before{bottom:auto;top:-3px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after{bottom:auto;top:-3px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} -------------------------------------------------------------------------------- /public/css/lib/bootstrap-social.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Social Buttons for Bootstrap 3 | * 4 | * Copyright 2013-2015 Panayiotis Lipiridis 5 | * Licensed under the MIT License 6 | * 7 | * https://github.com/lipis/bootstrap-social 8 | */ 9 | 10 | $bs-height-base: ($line-height-computed + $padding-base-vertical * 2); 11 | $bs-height-lg: (floor($font-size-large * $line-height-base) + $padding-large-vertical * 2); 12 | $bs-height-sm: (floor($font-size-small * 1.5) + $padding-small-vertical * 2); 13 | $bs-height-xs: (floor($font-size-small * 1.2) + $padding-small-vertical + 1); 14 | 15 | .btn-social { 16 | position: relative; 17 | padding-left: ($bs-height-base + $padding-base-horizontal); 18 | text-align: left; 19 | white-space: nowrap; 20 | overflow: hidden; 21 | text-overflow: ellipsis; 22 | > :first-child { 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | bottom: 0; 27 | width: $bs-height-base; 28 | line-height: ($bs-height-base + 2); 29 | font-size: 1.6em; 30 | text-align: center; 31 | border-right: 1px solid rgba(0, 0, 0, 0.2); 32 | } 33 | &.btn-lg { 34 | padding-left: ($bs-height-lg + $padding-large-horizontal); 35 | > :first-child { 36 | line-height: $bs-height-lg; 37 | width: $bs-height-lg; 38 | font-size: 1.8em; 39 | } 40 | } 41 | &.btn-sm { 42 | padding-left: ($bs-height-sm + $padding-small-horizontal); 43 | > :first-child { 44 | line-height: $bs-height-sm; 45 | width: $bs-height-sm; 46 | font-size: 1.4em; 47 | } 48 | } 49 | &.btn-xs { 50 | padding-left: ($bs-height-xs + $padding-small-horizontal); 51 | > :first-child { 52 | line-height: $bs-height-xs; 53 | width: $bs-height-xs; 54 | font-size: 1.2em; 55 | } 56 | } 57 | } 58 | 59 | .btn-social-icon { 60 | @extend .btn-social; 61 | height: ($bs-height-base + 2); 62 | width: ($bs-height-base + 2); 63 | padding: 0; 64 | > :first-child { 65 | border: none; 66 | text-align: center; 67 | width: 100%!important; 68 | } 69 | &.btn-lg { 70 | height: $bs-height-lg; 71 | width: $bs-height-lg; 72 | padding-left: 0; 73 | padding-right: 0; 74 | } 75 | &.btn-sm { 76 | height: ($bs-height-sm + 2); 77 | width: ($bs-height-sm + 2); 78 | padding-left: 0; 79 | padding-right: 0; 80 | } 81 | &.btn-xs { 82 | height: ($bs-height-xs + 2); 83 | width: ($bs-height-xs + 2); 84 | padding-left: 0; 85 | padding-right: 0; 86 | } 87 | } 88 | 89 | @mixin btn-social($color-bg, $color: #fff) { 90 | background-color: $color-bg; 91 | @include button-variant($color, $color-bg, rgba(0,0,0,.2)); 92 | } 93 | 94 | 95 | .btn-adn { @include btn-social(#d87a68); } 96 | .btn-bitbucket { @include btn-social(#205081); } 97 | .btn-dropbox { @include btn-social(#1087dd); } 98 | .btn-facebook { @include btn-social(#3b5998); } 99 | .btn-flickr { @include btn-social(#ff0084); } 100 | .btn-foursquare { @include btn-social(#f94877); } 101 | .btn-github { @include btn-social(#444444); } 102 | .btn-google { @include btn-social(#dd4b39); } 103 | .btn-instagram { @include btn-social(#3f729b); } 104 | .btn-linkedin { @include btn-social(#007bb6); } 105 | .btn-microsoft { @include btn-social(#2672ec); } 106 | .btn-odnoklassniki { @include btn-social(#f4731c); } 107 | .btn-openid { @include btn-social(#f7931e); } 108 | .btn-pinterest { @include btn-social(#cb2027); } 109 | .btn-reddit { @include btn-social(#eff7ff, #000); } 110 | .btn-soundcloud { @include btn-social(#ff5500); } 111 | .btn-tumblr { @include btn-social(#2c4762); } 112 | .btn-twitter { @include btn-social(#55acee); } 113 | .btn-vimeo { @include btn-social(#1ab7ea); } 114 | .btn-vk { @include btn-social(#587ea3); } 115 | .btn-yahoo { @include btn-social(#720e9e); } 116 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headings-color 19 | color: inherit; 20 | } 21 | 22 | // Provide class for links that match alerts 23 | .alert-link { 24 | font-weight: $alert-link-font-weight; 25 | } 26 | 27 | // Improve alignment and spacing of inner content 28 | > p, 29 | > ul { 30 | margin-bottom: 0; 31 | } 32 | 33 | > p + p { 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | // Dismissible alerts 39 | // 40 | // Expand the right padding and account for the close button's positioning. 41 | 42 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 43 | .alert-dismissible { 44 | padding-right: ($alert-padding + 20); 45 | 46 | // Adjust close link position 47 | .close { 48 | position: relative; 49 | top: -2px; 50 | right: -21px; 51 | color: inherit; 52 | } 53 | } 54 | 55 | // Alternate styles 56 | // 57 | // Generate contextual modifier classes for colorizing the alert. 58 | 59 | .alert-success { 60 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 61 | } 62 | 63 | .alert-info { 64 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 65 | } 66 | 67 | .alert-warning { 68 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 69 | } 70 | 71 | .alert-danger { 72 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 73 | } 74 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: middle; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | 32 | .btn-xs &, 33 | .btn-group-xs > .btn & { 34 | top: 0; 35 | padding: 1px 5px; 36 | } 37 | 38 | // [converter] extracted a& to a.badge 39 | 40 | // Account for badges in navs 41 | .list-group-item.active > &, 42 | .nav-pills > .active > a > & { 43 | color: $badge-active-color; 44 | background-color: $badge-active-bg; 45 | } 46 | 47 | .list-group-item > & { 48 | float: right; 49 | } 50 | 51 | .list-group-item > & + & { 52 | margin-right: 5px; 53 | } 54 | 55 | .nav-pills > li > a > & { 56 | margin-left: 3px; 57 | } 58 | } 59 | 60 | // Hover state, but only for links 61 | a.badge { 62 | &:hover, 63 | &:focus { 64 | color: $badge-link-hover-color; 65 | text-decoration: none; 66 | cursor: pointer; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | // [converter] Workaround for https://github.com/sass/libsass/issues/1115 18 | $nbsp: "\00a0"; 19 | content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space 20 | padding: 0 5px; 21 | color: $breadcrumb-color; 22 | } 23 | } 24 | 25 | > .active { 26 | color: $breadcrumb-active-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_buttons.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // -------------------------------------------------- 8 | 9 | .btn { 10 | display: inline-block; 11 | margin-bottom: 0; // For input.btn 12 | font-weight: $btn-font-weight; 13 | text-align: center; 14 | vertical-align: middle; 15 | touch-action: manipulation; 16 | cursor: pointer; 17 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 18 | border: 1px solid transparent; 19 | white-space: nowrap; 20 | @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base); 21 | @include user-select(none); 22 | 23 | &, 24 | &:active, 25 | &.active { 26 | &:focus, 27 | &.focus { 28 | @include tab-focus; 29 | } 30 | } 31 | 32 | &:hover, 33 | &:focus, 34 | &.focus { 35 | color: $btn-default-color; 36 | text-decoration: none; 37 | } 38 | 39 | &:active, 40 | &.active { 41 | outline: 0; 42 | background-image: none; 43 | @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 44 | } 45 | 46 | &.disabled, 47 | &[disabled], 48 | fieldset[disabled] & { 49 | cursor: $cursor-disabled; 50 | @include opacity(.65); 51 | @include box-shadow(none); 52 | } 53 | 54 | // [converter] extracted a& to a.btn 55 | } 56 | 57 | a.btn { 58 | &.disabled, 59 | fieldset[disabled] & { 60 | pointer-events: none; // Future-proof disabling of clicks on `` elements 61 | } 62 | } 63 | 64 | 65 | // Alternate buttons 66 | // -------------------------------------------------- 67 | 68 | .btn-default { 69 | @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border); 70 | } 71 | .btn-primary { 72 | @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border); 73 | } 74 | // Success appears as green 75 | .btn-success { 76 | @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border); 77 | } 78 | // Info appears as blue-green 79 | .btn-info { 80 | @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border); 81 | } 82 | // Warning appears as orange 83 | .btn-warning { 84 | @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border); 85 | } 86 | // Danger and error appear as red 87 | .btn-danger { 88 | @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border); 89 | } 90 | 91 | 92 | // Link buttons 93 | // ------------------------- 94 | 95 | // Make a button look and behave like a link 96 | .btn-link { 97 | color: $link-color; 98 | font-weight: normal; 99 | border-radius: 0; 100 | 101 | &, 102 | &:active, 103 | &.active, 104 | &[disabled], 105 | fieldset[disabled] & { 106 | background-color: transparent; 107 | @include box-shadow(none); 108 | } 109 | &, 110 | &:hover, 111 | &:focus, 112 | &:active { 113 | border-color: transparent; 114 | } 115 | &:hover, 116 | &:focus { 117 | color: $link-hover-color; 118 | text-decoration: $link-hover-decoration; 119 | background-color: transparent; 120 | } 121 | &[disabled], 122 | fieldset[disabled] & { 123 | &:hover, 124 | &:focus { 125 | color: $btn-link-disabled-color; 126 | text-decoration: none; 127 | } 128 | } 129 | } 130 | 131 | 132 | // Button Sizes 133 | // -------------------------------------------------- 134 | 135 | .btn-lg { 136 | // line-height: ensure even-numbered height of button next to large input 137 | @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large); 138 | } 139 | .btn-sm { 140 | // line-height: ensure proper height of button next to small input 141 | @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); 142 | } 143 | .btn-xs { 144 | @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); 145 | } 146 | 147 | 148 | // Block button 149 | // -------------------------------------------------- 150 | 151 | .btn-block { 152 | display: block; 153 | width: 100%; 154 | } 155 | 156 | // Vertically space out multiple block buttons 157 | .btn-block + .btn-block { 158 | margin-top: 5px; 159 | } 160 | 161 | // Specificity overrides 162 | input[type="submit"], 163 | input[type="reset"], 164 | input[type="button"] { 165 | &.btn-block { 166 | width: 100%; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | } 37 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: $font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: $code-color; 19 | background-color: $code-bg; 20 | border-radius: $border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: $kbd-color; 28 | background-color: $kbd-bg; 29 | border-radius: $border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | font-weight: bold; 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | // Blocks of code 41 | pre { 42 | display: block; 43 | padding: (($line-height-computed - 1) / 2); 44 | margin: 0 0 ($line-height-computed / 2); 45 | font-size: ($font-size-base - 1); // 14px to 13px 46 | line-height: $line-height-base; 47 | word-break: break-all; 48 | word-wrap: break-word; 49 | color: $pre-color; 50 | background-color: $pre-bg; 51 | border: 1px solid $pre-border-color; 52 | border-radius: $border-radius-base; 53 | 54 | // Account for some code outputs that place code tags in pre tags 55 | code { 56 | padding: 0; 57 | font-size: inherit; 58 | color: inherit; 59 | white-space: pre-wrap; 60 | background-color: transparent; 61 | border-radius: 0; 62 | } 63 | } 64 | 65 | // Enable scrollable blocks of code 66 | .pre-scrollable { 67 | max-height: $pre-scrollable-max-height; 68 | overflow-y: scroll; 69 | } 70 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | // [converter] extracted tr&.in to tr.collapse.in 23 | // [converter] extracted tbody&.in to tbody.collapse.in 24 | } 25 | 26 | tr.collapse.in { display: table-row; } 27 | 28 | tbody.collapse.in { display: table-row-group; } 29 | 30 | .collapsing { 31 | position: relative; 32 | height: 0; 33 | overflow: hidden; 34 | @include transition-property(height, visibility); 35 | @include transition-duration(.35s); 36 | @include transition-timing-function(ease); 37 | } 38 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Dropdown menus 3 | // -------------------------------------------------- 4 | 5 | 6 | // Dropdown arrow/caret 7 | .caret { 8 | display: inline-block; 9 | width: 0; 10 | height: 0; 11 | margin-left: 2px; 12 | vertical-align: middle; 13 | border-top: $caret-width-base dashed; 14 | border-top: $caret-width-base solid \9; // IE8 15 | border-right: $caret-width-base solid transparent; 16 | border-left: $caret-width-base solid transparent; 17 | } 18 | 19 | // The dropdown wrapper (div) 20 | .dropup, 21 | .dropdown { 22 | position: relative; 23 | } 24 | 25 | // Prevent the focus on the dropdown toggle when closing dropdowns 26 | .dropdown-toggle:focus { 27 | outline: 0; 28 | } 29 | 30 | // The dropdown menu (ul) 31 | .dropdown-menu { 32 | position: absolute; 33 | top: 100%; 34 | left: 0; 35 | z-index: $zindex-dropdown; 36 | display: none; // none by default, but block on "open" of the menu 37 | float: left; 38 | min-width: 160px; 39 | padding: 5px 0; 40 | margin: 2px 0 0; // override default ul 41 | list-style: none; 42 | font-size: $font-size-base; 43 | text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) 44 | background-color: $dropdown-bg; 45 | border: 1px solid $dropdown-fallback-border; // IE8 fallback 46 | border: 1px solid $dropdown-border; 47 | border-radius: $border-radius-base; 48 | @include box-shadow(0 6px 12px rgba(0,0,0,.175)); 49 | background-clip: padding-box; 50 | 51 | // Aligns the dropdown menu to right 52 | // 53 | // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` 54 | &.pull-right { 55 | right: 0; 56 | left: auto; 57 | } 58 | 59 | // Dividers (basically an hr) within the dropdown 60 | .divider { 61 | @include nav-divider($dropdown-divider-bg); 62 | } 63 | 64 | // Links within the dropdown menu 65 | > li > a { 66 | display: block; 67 | padding: 3px 20px; 68 | clear: both; 69 | font-weight: normal; 70 | line-height: $line-height-base; 71 | color: $dropdown-link-color; 72 | white-space: nowrap; // prevent links from randomly breaking onto new lines 73 | } 74 | } 75 | 76 | // Hover/Focus state 77 | .dropdown-menu > li > a { 78 | &:hover, 79 | &:focus { 80 | text-decoration: none; 81 | color: $dropdown-link-hover-color; 82 | background-color: $dropdown-link-hover-bg; 83 | } 84 | } 85 | 86 | // Active state 87 | .dropdown-menu > .active > a { 88 | &, 89 | &:hover, 90 | &:focus { 91 | color: $dropdown-link-active-color; 92 | text-decoration: none; 93 | outline: 0; 94 | background-color: $dropdown-link-active-bg; 95 | } 96 | } 97 | 98 | // Disabled state 99 | // 100 | // Gray out text and ensure the hover/focus state remains gray 101 | 102 | .dropdown-menu > .disabled > a { 103 | &, 104 | &:hover, 105 | &:focus { 106 | color: $dropdown-link-disabled-color; 107 | } 108 | 109 | // Nuke hover/focus effects 110 | &:hover, 111 | &:focus { 112 | text-decoration: none; 113 | background-color: transparent; 114 | background-image: none; // Remove CSS gradient 115 | @include reset-filter; 116 | cursor: $cursor-disabled; 117 | } 118 | } 119 | 120 | // Open state for the dropdown 121 | .open { 122 | // Show the menu 123 | > .dropdown-menu { 124 | display: block; 125 | } 126 | 127 | // Remove the outline when :focus is triggered 128 | > a { 129 | outline: 0; 130 | } 131 | } 132 | 133 | // Menu positioning 134 | // 135 | // Add extra class to `.dropdown-menu` to flip the alignment of the dropdown 136 | // menu with the parent. 137 | .dropdown-menu-right { 138 | left: auto; // Reset the default from `.dropdown-menu` 139 | right: 0; 140 | } 141 | // With v3, we enabled auto-flipping if you have a dropdown within a right 142 | // aligned nav component. To enable the undoing of that, we provide an override 143 | // to restore the default dropdown menu alignment. 144 | // 145 | // This is only for left-aligning a dropdown menu within a `.navbar-right` or 146 | // `.pull-right` nav component. 147 | .dropdown-menu-left { 148 | left: 0; 149 | right: auto; 150 | } 151 | 152 | // Dropdown section headers 153 | .dropdown-header { 154 | display: block; 155 | padding: 3px 20px; 156 | font-size: $font-size-small; 157 | line-height: $line-height-base; 158 | color: $dropdown-header-color; 159 | white-space: nowrap; // as with > li > a 160 | } 161 | 162 | // Backdrop to catch body clicks on mobile, etc. 163 | .dropdown-backdrop { 164 | position: fixed; 165 | left: 0; 166 | right: 0; 167 | bottom: 0; 168 | top: 0; 169 | z-index: ($zindex-dropdown - 10); 170 | } 171 | 172 | // Right aligned dropdowns 173 | .pull-right > .dropdown-menu { 174 | right: 0; 175 | left: auto; 176 | } 177 | 178 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 179 | // 180 | // Just add .dropup after the standard .dropdown class and you're set, bro. 181 | // TODO: abstract this so that the navbar fixed styles are not placed here? 182 | 183 | .dropup, 184 | .navbar-fixed-bottom .dropdown { 185 | // Reverse the caret 186 | .caret { 187 | border-top: 0; 188 | border-bottom: $caret-width-base dashed; 189 | border-bottom: $caret-width-base solid \9; // IE8 190 | content: ""; 191 | } 192 | // Different positioning for bottom up menu 193 | .dropdown-menu { 194 | top: auto; 195 | bottom: 100%; 196 | margin-bottom: 2px; 197 | } 198 | } 199 | 200 | 201 | // Component alignment 202 | // 203 | // Reiterate per navbar.less and the modified component alignment there. 204 | 205 | @media (min-width: $grid-float-breakpoint) { 206 | .navbar-right { 207 | .dropdown-menu { 208 | right: 0; left: auto; 209 | } 210 | // Necessary for overrides of the default right aligned menu. 211 | // Will remove come v4 in all likelihood. 212 | .dropdown-menu-left { 213 | left: 0; right: auto; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | @include container-fixed; 12 | 13 | @media (min-width: $screen-sm-min) { 14 | width: $container-sm; 15 | } 16 | @media (min-width: $screen-md-min) { 17 | width: $container-md; 18 | } 19 | @media (min-width: $screen-lg-min) { 20 | width: $container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | @include container-fixed; 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | @include make-row; 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | @include make-grid-columns; 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | @include make-grid(xs); 57 | 58 | 59 | // Small grid 60 | // 61 | // Columns, offsets, pushes, and pulls for the small device range, from phones 62 | // to tablets. 63 | 64 | @media (min-width: $screen-sm-min) { 65 | @include make-grid(sm); 66 | } 67 | 68 | 69 | // Medium grid 70 | // 71 | // Columns, offsets, pushes, and pulls for the desktop device range. 72 | 73 | @media (min-width: $screen-md-min) { 74 | @include make-grid(md); 75 | } 76 | 77 | 78 | // Large grid 79 | // 80 | // Columns, offsets, pushes, and pulls for the large desktop device range. 81 | 82 | @media (min-width: $screen-lg-min) { 83 | @include make-grid(lg); 84 | } 85 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_input-groups.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Input groups 3 | // -------------------------------------------------- 4 | 5 | // Base styles 6 | // ------------------------- 7 | .input-group { 8 | position: relative; // For dropdowns 9 | display: table; 10 | border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table 11 | 12 | // Undo padding and float of grid classes 13 | &[class*="col-"] { 14 | float: none; 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .form-control { 20 | // Ensure that the input is always above the *appended* addon button for 21 | // proper border colors. 22 | position: relative; 23 | z-index: 2; 24 | 25 | // IE9 fubars the placeholder attribute in text inputs and the arrows on 26 | // select elements in input groups. To fix it, we float the input. Details: 27 | // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 28 | float: left; 29 | 30 | width: 100%; 31 | margin-bottom: 0; 32 | 33 | &:focus { 34 | z-index: 3; 35 | } 36 | } 37 | } 38 | 39 | // Sizing options 40 | // 41 | // Remix the default form control sizing classes into new ones for easier 42 | // manipulation. 43 | 44 | .input-group-lg > .form-control, 45 | .input-group-lg > .input-group-addon, 46 | .input-group-lg > .input-group-btn > .btn { 47 | @extend .input-lg; 48 | } 49 | .input-group-sm > .form-control, 50 | .input-group-sm > .input-group-addon, 51 | .input-group-sm > .input-group-btn > .btn { 52 | @extend .input-sm; 53 | } 54 | 55 | 56 | // Display as table-cell 57 | // ------------------------- 58 | .input-group-addon, 59 | .input-group-btn, 60 | .input-group .form-control { 61 | display: table-cell; 62 | 63 | &:not(:first-child):not(:last-child) { 64 | border-radius: 0; 65 | } 66 | } 67 | // Addon and addon wrapper for buttons 68 | .input-group-addon, 69 | .input-group-btn { 70 | width: 1%; 71 | white-space: nowrap; 72 | vertical-align: middle; // Match the inputs 73 | } 74 | 75 | // Text input groups 76 | // ------------------------- 77 | .input-group-addon { 78 | padding: $padding-base-vertical $padding-base-horizontal; 79 | font-size: $font-size-base; 80 | font-weight: normal; 81 | line-height: 1; 82 | color: $input-color; 83 | text-align: center; 84 | background-color: $input-group-addon-bg; 85 | border: 1px solid $input-group-addon-border-color; 86 | border-radius: $input-border-radius; 87 | 88 | // Sizing 89 | &.input-sm { 90 | padding: $padding-small-vertical $padding-small-horizontal; 91 | font-size: $font-size-small; 92 | border-radius: $input-border-radius-small; 93 | } 94 | &.input-lg { 95 | padding: $padding-large-vertical $padding-large-horizontal; 96 | font-size: $font-size-large; 97 | border-radius: $input-border-radius-large; 98 | } 99 | 100 | // Nuke default margins from checkboxes and radios to vertically center within. 101 | input[type="radio"], 102 | input[type="checkbox"] { 103 | margin-top: 0; 104 | } 105 | } 106 | 107 | // Reset rounded corners 108 | .input-group .form-control:first-child, 109 | .input-group-addon:first-child, 110 | .input-group-btn:first-child > .btn, 111 | .input-group-btn:first-child > .btn-group > .btn, 112 | .input-group-btn:first-child > .dropdown-toggle, 113 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), 114 | .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { 115 | @include border-right-radius(0); 116 | } 117 | .input-group-addon:first-child { 118 | border-right: 0; 119 | } 120 | .input-group .form-control:last-child, 121 | .input-group-addon:last-child, 122 | .input-group-btn:last-child > .btn, 123 | .input-group-btn:last-child > .btn-group > .btn, 124 | .input-group-btn:last-child > .dropdown-toggle, 125 | .input-group-btn:first-child > .btn:not(:first-child), 126 | .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { 127 | @include border-left-radius(0); 128 | } 129 | .input-group-addon:last-child { 130 | border-left: 0; 131 | } 132 | 133 | // Button input groups 134 | // ------------------------- 135 | .input-group-btn { 136 | position: relative; 137 | // Jankily prevent input button groups from wrapping with `white-space` and 138 | // `font-size` in combination with `inline-block` on buttons. 139 | font-size: 0; 140 | white-space: nowrap; 141 | 142 | // Negative margin for spacing, position for bringing hovered/focused/actived 143 | // element above the siblings. 144 | > .btn { 145 | position: relative; 146 | + .btn { 147 | margin-left: -1px; 148 | } 149 | // Bring the "active" button to the front 150 | &:hover, 151 | &:focus, 152 | &:active { 153 | z-index: 2; 154 | } 155 | } 156 | 157 | // Negative margin to only have a 1px border between the two 158 | &:first-child { 159 | > .btn, 160 | > .btn-group { 161 | margin-right: -1px; 162 | } 163 | } 164 | &:last-child { 165 | > .btn, 166 | > .btn-group { 167 | z-index: 2; 168 | margin-left: -1px; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding-top: $jumbotron-padding; 8 | padding-bottom: $jumbotron-padding; 9 | margin-bottom: $jumbotron-padding; 10 | color: $jumbotron-color; 11 | background-color: $jumbotron-bg; 12 | 13 | h1, 14 | .h1 { 15 | color: $jumbotron-heading-color; 16 | } 17 | 18 | p { 19 | margin-bottom: ($jumbotron-padding / 2); 20 | font-size: $jumbotron-font-size; 21 | font-weight: 200; 22 | } 23 | 24 | > hr { 25 | border-top-color: darken($jumbotron-bg, 10%); 26 | } 27 | 28 | .container &, 29 | .container-fluid & { 30 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 31 | padding-left: ($grid-gutter-width / 2); 32 | padding-right: ($grid-gutter-width / 2); 33 | } 34 | 35 | .container { 36 | max-width: 100%; 37 | } 38 | 39 | @media screen and (min-width: $screen-sm-min) { 40 | padding-top: ($jumbotron-padding * 1.6); 41 | padding-bottom: ($jumbotron-padding * 1.6); 42 | 43 | .container &, 44 | .container-fluid & { 45 | padding-left: ($jumbotron-padding * 2); 46 | padding-right: ($jumbotron-padding * 2); 47 | } 48 | 49 | h1, 50 | .h1 { 51 | font-size: $jumbotron-heading-font-size; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // [converter] extracted a& to a.label 18 | 19 | // Empty labels collapse automatically (not available in IE8) 20 | &:empty { 21 | display: none; 22 | } 23 | 24 | // Quick fix for labels in buttons 25 | .btn & { 26 | position: relative; 27 | top: -1px; 28 | } 29 | } 30 | 31 | // Add hover effects, but only for links 32 | a.label { 33 | &:hover, 34 | &:focus { 35 | color: $label-link-hover-color; 36 | text-decoration: none; 37 | cursor: pointer; 38 | } 39 | } 40 | 41 | // Colors 42 | // Contextual variations (linked labels get darker on :hover) 43 | 44 | .label-default { 45 | @include label-variant($label-default-bg); 46 | } 47 | 48 | .label-primary { 49 | @include label-variant($label-primary-bg); 50 | } 51 | 52 | .label-success { 53 | @include label-variant($label-success-bg); 54 | } 55 | 56 | .label-info { 57 | @include label-variant($label-info-bg); 58 | } 59 | 60 | .label-warning { 61 | @include label-variant($label-warning-bg); 62 | } 63 | 64 | .label-danger { 65 | @include label-variant($label-danger-bg); 66 | } 67 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/_list-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | // 8 | // Easily usable on