├── src
├── tests
│ ├── server
│ │ ├── controller
│ │ │ ├── badge.js
│ │ │ └── user.js
│ │ ├── api
│ │ │ └── github.js
│ │ ├── documents
│ │ │ ├── utils.js
│ │ │ └── org.js
│ │ └── services
│ │ │ ├── org.js
│ │ │ └── github.js
│ ├── .eslintrc
│ ├── acceptance_tests
│ │ ├── authorize_claowner.spec.js
│ │ ├── zzz_cleanup.spec.js
│ │ ├── link_repo1_sign_cla.spec.js
│ │ ├── claUtils.js
│ │ ├── whitelist_user_on_repo2.spec.js
│ │ └── githubUtils.js
│ ├── client
│ │ └── services
│ │ │ └── utils.spec.js
│ └── karma.config.js
├── server
│ ├── templates
│ │ ├── cla.ejs
│ │ ├── badge.svg
│ │ ├── badge_signed.svg
│ │ └── badge_not_signed.svg
│ ├── middleware
│ │ ├── param.js
│ │ ├── cleanup.js
│ │ ├── utils.js
│ │ └── authenticated.js
│ ├── webhooks
│ │ └── ping.js
│ ├── api
│ │ ├── github.js
│ │ ├── repo.js
│ │ └── org.js
│ ├── controller
│ │ ├── health.js
│ │ ├── badge.js
│ │ ├── user.js
│ │ └── default.js
│ ├── documents
│ │ ├── user.js
│ │ ├── org.js
│ │ ├── utils.js
│ │ ├── repo.js
│ │ └── cla.js
│ ├── services
│ │ ├── logger.js
│ │ ├── org.js
│ │ ├── url.js
│ │ └── github.js
│ ├── passports
│ │ ├── token.js
│ │ └── github.js
│ └── graphQueries
│ │ └── github.js
├── client
│ ├── favicon.ico
│ ├── assets
│ │ ├── robots.txt
│ │ ├── images
│ │ │ ├── preview.gif
│ │ │ ├── background.jpg
│ │ │ ├── add_custom_fields.gif
│ │ │ ├── nervous_remove
│ │ │ │ ├── nervous_eye1-37.svg
│ │ │ │ ├── nervous_eye2-39.svg
│ │ │ │ ├── nervous_drop-38.svg
│ │ │ │ ├── nervous_drop-382.svg
│ │ │ │ └── nervous-36.svg
│ │ │ ├── browser.svg
│ │ │ ├── CLA_logo.svg
│ │ │ ├── link_button.svg
│ │ │ ├── link_active.svg
│ │ │ ├── linked.svg
│ │ │ ├── link_inactive.svg
│ │ │ ├── feature1.svg
│ │ │ ├── error.svg
│ │ │ └── feature2.svg
│ │ ├── styles
│ │ │ ├── _well.scss
│ │ │ ├── _list-group.scss
│ │ │ ├── _tabs.scss
│ │ │ ├── _breadcrumb.scss
│ │ │ ├── _link.scss
│ │ │ ├── _browser.scss
│ │ │ ├── _panel.scss
│ │ │ ├── _table.scss
│ │ │ ├── _settings.scss
│ │ │ ├── _animation.scss
│ │ │ ├── _btn.scss
│ │ │ ├── _navbar.scss
│ │ │ └── _modal.scss
│ │ └── js
│ │ │ └── CLA_signature_MouseOver_edgeActions.js
│ ├── templates
│ │ ├── 404.html
│ │ ├── feature.html
│ │ ├── customField.html
│ │ ├── cla.html
│ │ └── settings.html
│ ├── modals
│ │ ├── info.js
│ │ ├── link.js
│ │ ├── error.js
│ │ ├── addScope.js
│ │ ├── confirm.js
│ │ ├── report.js
│ │ ├── templates
│ │ │ ├── claView.html
│ │ │ ├── info_gist.html
│ │ │ ├── info_share_gist.html
│ │ │ ├── error_modal.html
│ │ │ ├── add_scope.html
│ │ │ ├── confirmLink.html
│ │ │ ├── badge.html
│ │ │ ├── confirmRemove.html
│ │ │ ├── linkSuccess.html
│ │ │ ├── versionView.html
│ │ │ ├── whitelist_info.html
│ │ │ ├── upload.html
│ │ │ └── report.html
│ │ ├── versionView.js
│ │ ├── claView.js
│ │ ├── badge.js
│ │ ├── editLinkedItem.js
│ │ └── upload.js
│ ├── services
│ │ ├── linkItem.js
│ │ └── utils.js
│ └── app.js
└── config.js
├── .bowerrc
├── .dockerignore
├── .github
├── invite-contributors.yml
├── config.yml
├── stale.yml
├── settings.yml
└── workflows
│ └── build.yml
├── .cfignore
├── manifest.yml
├── .eslintignore
├── cla-assistant.json
├── SAP Corporate Contributor License Agreement (5-26-15).pdf
├── .codeclimate.yml
├── bower.json
├── .env.example
├── .gitignore
├── .cf.sh
├── ISSUE_TEMPLATE.md
├── Dockerfile
├── .travis.yml
├── app.js
├── codecept.json
├── DESCRIPTION.MD
├── .eslintrc.js
├── custom-fields-schema.json
├── Gruntfile.js
└── package.json
/src/tests/server/controller/badge.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/tests/server/controller/user.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "src/bower"
3 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | Dockerfile
3 |
--------------------------------------------------------------------------------
/.github/invite-contributors.yml:
--------------------------------------------------------------------------------
1 | team: contributors
--------------------------------------------------------------------------------
/.cfignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | log
4 | log.*
5 |
--------------------------------------------------------------------------------
/src/server/templates/cla.ejs:
--------------------------------------------------------------------------------
1 |
2 | Please confirm the CLA
3 |
--------------------------------------------------------------------------------
/src/tests/.eslintrc:
--------------------------------------------------------------------------------
1 | globals:
2 | sinon: true
3 | rules:
4 | no-unused-expressions: 0
5 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: cla-assistant-staging
3 | services:
4 | - cla-staging-env
5 | - my-logs
--------------------------------------------------------------------------------
/src/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Unity-Technologies/cla-assistant/master/src/client/favicon.ico
--------------------------------------------------------------------------------
/src/client/assets/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /api/
3 | Disallow: /check/
4 | Disallow: /*?pullRequest=*
5 |
--------------------------------------------------------------------------------
/src/client/assets/images/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Unity-Technologies/cla-assistant/master/src/client/assets/images/preview.gif
--------------------------------------------------------------------------------
/src/client/assets/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Unity-Technologies/cla-assistant/master/src/client/assets/images/background.jpg
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 | src/bower/**
3 | src/client/assets/js/**
4 | src/client/app.min.*
5 | src/tests/acceptance_tests/**/*
6 | output/**
7 |
--------------------------------------------------------------------------------
/src/client/assets/images/add_custom_fields.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Unity-Technologies/cla-assistant/master/src/client/assets/images/add_custom_fields.gif
--------------------------------------------------------------------------------
/cla-assistant.json:
--------------------------------------------------------------------------------
1 | {
2 | "default-cla": [
3 | {
4 | "name": "SAP individual CLA",
5 | "url": "https://gist.github.com/CLAassistant/bd1ea8ec8aa0357414e8"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/src/server/middleware/param.js:
--------------------------------------------------------------------------------
1 | const merge = require('merge')
2 |
3 | module.exports = (req, _res, next) => {
4 | req.args = merge(req.body, req.query)
5 | next()
6 | }
7 |
--------------------------------------------------------------------------------
/SAP Corporate Contributor License Agreement (5-26-15).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Unity-Technologies/cla-assistant/master/SAP Corporate Contributor License Agreement (5-26-15).pdf
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | languages:
2 | Ruby: true
3 | JavaScript: true
4 | PHP: true
5 | Python: true
6 | exclude_paths:
7 | - src/client/assets/js/edge.5.0.1.min.js
8 | - src/tests/**/*
9 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_well.scss:
--------------------------------------------------------------------------------
1 | .well {
2 | -moz-box-shadow: none;
3 | -webkit-box-shadow: none;
4 | box-shadow: none;
5 | /*border-radius: 5px;*/
6 | margin-bottom: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_list-group.scss:
--------------------------------------------------------------------------------
1 | .list-group {
2 | border: 1px solid $input-border;
3 | z-index: 10;
4 | position: absolute;
5 | width: 100%;
6 | margin-top: 33px;
7 | margin-bottom: 15px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_tabs.scss:
--------------------------------------------------------------------------------
1 | .nav-tabs {
2 | > li > a {
3 | border-bottom: 0;
4 | color: $brand-primary-muted;
5 | }
6 |
7 | a:hover {
8 | cursor: pointer;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/client/templates/404.html:
--------------------------------------------------------------------------------
1 |
2 |
404
3 |
File Not Found
4 |
Sorry, an error has occurred. Requested page not found!
5 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cla-assistant",
3 | "version": "1.0.0",
4 | "dependencies": {
5 | "bootstrap-sass-official": "~3.2.0"
6 | },
7 | "devDependencies": {
8 | "angular-mocks": "1.7.0",
9 | "should": "~3.3.1"
10 | }
11 | }
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | export PORT=5000
2 | export PROTOCOL=http
3 | export HOST=localhost
4 | export MONGODB=mongodb://cla_assistant:cla_assistant@localhost:27017/cla_assistant
5 | export GITHUB_CLIENT=
6 | export GITHUB_SECRET=
7 |
8 | export GITHUB_USER=
9 | export GITHUB_PASS=
10 |
--------------------------------------------------------------------------------
/src/client/modals/info.js:
--------------------------------------------------------------------------------
1 | module.controller('InfoCtrl',
2 | function($scope, $modalInstance, $window) {
3 |
4 | $scope.origin = $window.location.origin;
5 |
6 | $scope.cancel = function () {
7 | $modalInstance.dismiss('cancel');
8 | };
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/.github/config.yml:
--------------------------------------------------------------------------------
1 | requestInfoReplyComment: >
2 | We would appreciate it if you could provide us with more info about this issue or pull request.
3 |
4 | requestInfoDefaultTitles:
5 | - update readme.md
6 | - updates
7 |
8 | requestInfoOn:
9 | pullRequest: true
10 | issue: true
--------------------------------------------------------------------------------
/src/client/modals/link.js:
--------------------------------------------------------------------------------
1 | module.controller('LinkCtrl',
2 | function($scope, $modalInstance) {
3 | $scope.ok = function () {
4 | $modalInstance.close();
5 | };
6 |
7 | $scope.cancel = function () {
8 | $modalInstance.dismiss('cancel');
9 | };
10 | }
11 | );
12 |
--------------------------------------------------------------------------------
/src/client/modals/error.js:
--------------------------------------------------------------------------------
1 | module.controller('ErrorCtrl',
2 | function($scope, $modalInstance) {
3 |
4 | $scope.ok = function () {
5 | $modalInstance.close();
6 | };
7 |
8 | $scope.cancel = function () {
9 | $modalInstance.dismiss('cancel');
10 | };
11 | }
12 | );
13 |
--------------------------------------------------------------------------------
/src/server/webhooks/ping.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////////////////////////////////////////////////////////
2 | // GitHub Ping Webhook Handler
3 | //////////////////////////////////////////////////////////////////////////////////////////////
4 |
5 | module.exports = function (req, res) {
6 | res.status(200).send('OK');
7 | };
--------------------------------------------------------------------------------
/src/server/api/github.js:
--------------------------------------------------------------------------------
1 | // module
2 | const github = require('../services/github')
3 | const merge = require('merge')
4 |
5 | module.exports = {
6 | call: async (req) => {
7 | const res = await github.call(merge(req.args, { token: req.user.token }))
8 |
9 | return { data: res.data, meta: res.headers }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/modals/addScope.js:
--------------------------------------------------------------------------------
1 | module.controller('AddScopeCtrl',
2 | function($scope, $modalInstance, $window) {
3 |
4 | $scope.origin = $window.location.origin;
5 |
6 | $scope.cancel = function () {
7 | $modalInstance.dismiss('cancel');
8 | };
9 | $scope.ok = function () {
10 | $modalInstance.close();
11 | };
12 | }
13 | );
14 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_breadcrumb.scss:
--------------------------------------------------------------------------------
1 | .breadcrumb {
2 |
3 | margin-bottom: 0;
4 | padding: 30px 0 0;
5 |
6 | a {
7 | color: $brand-primary;
8 | font-size: $font-size-h4;
9 |
10 | .octicon {
11 | font-size: 20px;
12 | }
13 | }
14 |
15 | a:hover {
16 | color: $brand-primary-hover;
17 | text-decoration: none;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | daysUntilStale: 90
2 | daysUntilClose: 7
3 | exemptLabels:
4 | - greenkeeper
5 | - bug
6 | - feature
7 | staleLabel: wontfix
8 | markComment: >
9 | This issue has been automatically marked as stale because it has not had
10 | recent activity. It will be closed if no further activity occurs. Thank you
11 | for your contributions.
12 | closeComment: false
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | src/bower
4 | output
5 | venv
6 | .env
7 | report
8 | npm-debug.log
9 | test
10 | coverage
11 | src/client/assets/styles/app.css
12 | src/client/assets/styles/app.css.map
13 | src/client/app.min.js
14 | src/client/app.min.js.map
15 | .sass-cache
16 | .cfenv
17 | *.log
18 | log
19 | log.*
20 | .vscode/*
21 | jsconfig.json
22 | src/tests/acceptance_tests/codecept.conf.js
23 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_link.scss:
--------------------------------------------------------------------------------
1 | .link-topic {
2 | font-size: 18px;
3 | }
4 |
5 | .link-topic-1,
6 | .link-topic-2 {
7 | font-size: 18px;
8 | margin-left: -10px;
9 | }
10 |
11 | .link-topic-2 {
12 | margin-top: 40px;
13 | padding-left: 0px;
14 | }
15 |
16 | .link-topic-number {
17 | border: 1px solid;
18 | border-radius: 50%;
19 | display: inline-block;
20 | text-align: center;
21 | width: 21px;
22 | }
--------------------------------------------------------------------------------
/src/client/assets/styles/_browser.scss:
--------------------------------------------------------------------------------
1 | .browser {
2 | margin-top: -2px;
3 |
4 | .panel-body {
5 | overflow-x: scroll;
6 | padding: 0;
7 | }
8 |
9 | table.browse {
10 | margin: 0;
11 | width: 100%;
12 |
13 | td {
14 | border-bottom: 1px solid $gray-light;
15 | margin: 0;
16 | padding: 8px 10px;
17 | }
18 |
19 | .select i {
20 | font-size: inherit;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/client/templates/feature.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{getGistName(claObj.gistObj)}}
5 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/client/assets/images/nervous_remove/nervous_eye1-37.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
3 |
4 | EXPOSE 5000
5 |
6 | COPY . /cla-assistant
7 | WORKDIR /cla-assistant
8 |
9 | RUN \
10 | apk add --update nodejs su-exec git curl bzip2 patch make g++ && \
11 | addgroup -S cla-assistant && \
12 | adduser -S -D -G cla-assistant cla-assistant && \
13 | chown -R cla-assistant:cla-assistant /cla-assistant && \
14 | su-exec cla-assistant /bin/sh -c 'cd /cla-assistant && npm install && node_modules/grunt-cli/bin/grunt build && rm -rf /home/cla-assistant/.npm .git' && \
15 | apk del git curl bzip2 patch make g++ && \
16 | rm -rf /var/cache/apk/*
17 |
18 | USER cla-assistant
19 | CMD npm start
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - 12
5 | cache:
6 | directories:
7 | - src/bower
8 | notifications:
9 | slack: sap-pi-tools:$SLACK_TOKEN
10 | before_script:
11 | - bower install
12 | after_script:
13 | - grunt uglify && grunt mocha_istanbul && grunt coveralls
14 | - if [[ $TRAVIS_PULL_REQUEST == 'false']]; then npm run snyk-monitor; fi
15 | - if [[ $TRAVIS_PULL_REQUEST == 'false' && $TRAVIS_BRANCH == 'release-green' ]]; then ./.cf.sh cla-assistant-green; fi
16 | - if [[ $TRAVIS_PULL_REQUEST == 'false' && $TRAVIS_BRANCH == 'master' ]]; then ./.cf.sh cla-assistant-staging; fi
17 | - if [[ $TRAVIS_PULL_REQUEST == 'false' && $TRAVIS_BRANCH == 'release' ]]; then ./.cf.sh cla-assistant; fi
18 |
--------------------------------------------------------------------------------
/src/client/assets/images/nervous_remove/nervous_eye2-39.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////////////////////////////////////////////////////////
2 | // Initialize server
3 | //////////////////////////////////////////////////////////////////////////////////////////////
4 |
5 | let app = require('./src/server/app.js')
6 | // let http = require('http')
7 |
8 | // // eslint-disable-next-line no-console
9 | // console.log("App is initialized")
10 | // let server = http.createServer(app)
11 | // // eslint-disable-next-line no-console
12 | // console.log("Server is created")
13 | // const listener = server.listen(config.server.localport, function () {
14 | // // eslint-disable-next-line no-console
15 | // console.log('Listening on port ' + listener.address().port)
16 | // });
--------------------------------------------------------------------------------
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | repository:
2 | name: cla-assistant
3 | description: Contributor License Agreement assistant (CLA assistant)
4 | homepage: https://cla-assistant.io/
5 | private: false
6 | has_issues: true
7 | has_wiki: false
8 | has_downloads: true
9 | default_branch: master
10 | allow_squash_merge: true
11 | allow_merge_commit: true
12 | allow_rebase_merge: true
13 |
14 | labels:
15 | - name: architecture
16 | color: 5319e7
17 | - name: bug
18 | color: d93f0b
19 | - name: design
20 | color: 009800
21 | - name: feature
22 | color: 84b6eb
23 | - name: greenkeeper
24 | color: 00c775
25 | - name: help wanted
26 | color: 006b75
27 | - name: wontfix
28 | color: eeeeee
29 |
--------------------------------------------------------------------------------
/src/client/modals/versionView.js:
--------------------------------------------------------------------------------
1 | module.controller('VersionViewCtrl', function($scope, $rootScope, $modalInstance, cla, $location, noCLA, showCLA, $window) {
2 | $scope.claObj = cla;
3 | $scope.noCLA = noCLA;
4 | $scope.showCLA = showCLA;
5 | $scope.cla = null;
6 | $scope.modalInstance = $modalInstance;
7 | $scope.newCLA = null;
8 |
9 | $scope.openNewCla = function () {
10 | $modalInstance.dismiss('Link opened');
11 | $window.location.href = '/' + $scope.claObj.owner + '/' + $scope.claObj.repo;
12 | };
13 |
14 | $scope.openRevision = function () {
15 | $window.open($scope.claObj.gist_url + '/revisions');
16 | };
17 |
18 | $scope.cancel = function () {
19 | $modalInstance.dismiss('cancel');
20 | };
21 | });
22 |
--------------------------------------------------------------------------------
/src/client/assets/images/browser.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/codecept.json:
--------------------------------------------------------------------------------
1 | {
2 | "tests": "./src/tests/acceptance_tests/*spec.js",
3 | "timeout": 10000,
4 | "output": "./src/tests/acceptance_tests/output",
5 | "helpers": {
6 | "WebDriverIO": {
7 | "url": "/",
8 | "browser": "chrome",
9 | "windowSize": "maximize",
10 | "smartWait": 5000,
11 | "restart": false,
12 | "keepCookies": true,
13 | "desiredCapabilities": {
14 | "chromeOptions": {
15 | "args": [
16 | "--disable-gpu",
17 | "--window-size=800,600"
18 | ]
19 | }
20 | }
21 | }
22 | },
23 | "include": {},
24 | "bootstrap": false,
25 | "debug": true,
26 | "mocha": {},
27 | "name": "cla-assistant"
28 | }
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/authorize_claowner.spec.js:
--------------------------------------------------------------------------------
1 | const github = require('./githubUtils')
2 |
3 | let testUserName = process.env.TEST_USER_NAME
4 | let testUserPass = process.env.TEST_USER_PASS
5 |
6 | Feature('Authorize claowner1')
7 |
8 |
9 | Scenario('Authorize cla assistant for claowner1', (I) => {
10 | github.login(I, testUserName, testUserPass)
11 | I.amOnPage('https://preview.cla-assistant.io/')
12 | I.click('Sign in')
13 | I.seeInCurrentUrl('/login/oauth/authorize')
14 | I.waitForEnabled('button#js-oauth-authorize-btn', 5)
15 | I.click('button#js-oauth-authorize-btn')
16 | I.wait(1)
17 | I.seeInCurrentUrl('/login/oauth/authorize')
18 | I.waitForEnabled('button#js-oauth-authorize-btn', 5)
19 | I.click('button#js-oauth-authorize-btn')
20 | I.seeInCurrentUrl('https://preview.cla-assistant.io/')
21 | })
22 |
--------------------------------------------------------------------------------
/src/client/assets/images/nervous_remove/nervous_drop-38.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/client/assets/images/nervous_remove/nervous_drop-382.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/client/modals/templates/info_gist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
How can I create a CLA Gist?
6 |
7 | At
gist.github.com enter a file name and paste the content of your CLA.
8 |
9 |
10 |
11 |
12 |
What happens if I edit the Gist file?
13 |
14 | CLA assistant will always show you the current version of your Gist file. Users who accept your CLA sign the current version. If you change the content of your CLA, each contributor has to accept the new version when they create a new pull request.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/tests/server/api/github.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, beforeEach, afterEach*/
2 |
3 | // unit test
4 | const assert = require('assert')
5 | const sinon = require('sinon')
6 |
7 | // api
8 | const github_api = require('../../../server/api/github')
9 |
10 | // module
11 | const github = require('../../../server/services/github')
12 |
13 | describe('github:call', () => {
14 | beforeEach(() => sinon.stub(github, 'call').callsFake(async args => {
15 | assert.deepEqual(args, { obj: 'gists', fun: 'list', token: 'abc' })
16 | return { data: '', headers: '' }
17 | }))
18 |
19 | afterEach(() => github.call.restore())
20 |
21 | it('should call github service with user token', async () => {
22 | const req = { user: { id: 1, login: 'login', token: 'abc' }, args: { obj: 'gists', fun: 'list' } }
23 |
24 | await github_api.call(req)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/client/assets/styles/_settings.scss:
--------------------------------------------------------------------------------
1 | .hint {
2 | color: $gray;
3 | }
4 |
5 | .separator {
6 | width: 100%;
7 | height: 1px;
8 | background-color: white;
9 | display: flex;
10 | }
11 |
12 | .settings h4,
13 | .settings .h4 {
14 | color: $text-color;
15 | }
16 |
17 | .settings {
18 | img {
19 | height: 1.5em;
20 | min-height: 15px;
21 | }
22 |
23 | .btn {
24 | margin-top: 5px;
25 | width: 100%
26 | }
27 |
28 | .btn:first-of-type {
29 | margin-top: 0px;
30 | }
31 | }
32 |
33 |
34 | .settings img.org {
35 | height: 1em;
36 | vertical-align: inherit;
37 | }
38 |
39 | .settings {
40 | .action {
41 | padding: 0;
42 | }
43 |
44 | .action-icon {
45 | float: right;
46 | font-size: 18px;
47 | margin-right: 15px;
48 | }
49 |
50 | .action-icon:first-of-type {
51 | margin-right: 0;
52 | }
53 |
54 | .edit-icon {
55 | text-align: right;
56 | padding-right: 30px;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/server/documents/org.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const utils = require('./utils')
3 |
4 | const OrgSchema = mongoose.Schema({
5 | orgId: String,
6 | org: String,
7 | gist: String,
8 | token: String,
9 | excludePattern: String,
10 | sharedGist: Boolean,
11 | minFileChanges: Number,
12 | minCodeChanges: Number,
13 | whiteListPattern: String,
14 | privacyPolicy: String,
15 | updated_at: Date
16 | })
17 |
18 | OrgSchema.methods.isRepoExcluded = function (repo) {
19 | return utils.checkPattern(this.excludePattern, repo)
20 | }
21 |
22 | OrgSchema.methods.isUserWhitelisted = function (user) {
23 | return utils.checkPatternWildcard(this.whiteListPattern, user)
24 | }
25 |
26 | OrgSchema.index({
27 | orgId: 1,
28 | }, {
29 | unique: true
30 | })
31 |
32 | const Org = mongoose.model('Org', OrgSchema)
33 |
34 | module.exports = {
35 | Org: Org
36 | }
37 |
--------------------------------------------------------------------------------
/src/server/documents/utils.js:
--------------------------------------------------------------------------------
1 | const _ = require('lodash')
2 |
3 | module.exports = {
4 | checkPattern: (patternList, item) => {
5 | if (!patternList || !item || !item.includes) {
6 | return false
7 | }
8 | const patterns = patternList.split(',')
9 |
10 | return patterns.filter(pattern => item.includes(pattern)).length > 0
11 | },
12 | checkPatternWildcard: (patternList, item) => {
13 | if (!patternList || !item || !item.includes) {
14 | return false
15 | }
16 | const patterns = patternList.split(',')
17 |
18 | return patterns.filter(function (pattern) {
19 | pattern = pattern.trim()
20 | if (pattern.includes('*')) {
21 | const regex = _.escapeRegExp(pattern).split('\\*').join('.*')
22 |
23 | return new RegExp(regex).test(item)
24 | }
25 |
26 | return pattern === item
27 | }).length > 0
28 | }
29 | }
--------------------------------------------------------------------------------
/src/server/services/logger.js:
--------------------------------------------------------------------------------
1 | const bunyan = require('bunyan')
2 | const BunyanSlack = require('bunyan-slack')
3 | let log
4 |
5 | const formatter = (record, levelName) => {
6 | return {
7 | text: `[${levelName}] ${record.msg} (source: ${record.src.file} line: ${record.src.line})`
8 | }
9 | }
10 |
11 | log = bunyan.createLogger({
12 | src: true,
13 | name: config.server.http.host,
14 | streams: [{
15 | name: 'stdout',
16 | level: process.env.ENV == 'debug' ? 'info' : 'debug',
17 | stream: process.stdout
18 | }]
19 | });
20 |
21 | try {
22 | log.addStream({
23 | name: 'slack',
24 | level: 'error',
25 | stream: new BunyanSlack({
26 | webhook_url: config.server.slack.url,
27 | channel: config.server.slack.channel,
28 | username: 'CLA Assistant',
29 | customFormatter: formatter
30 | })
31 | })
32 | } catch (e) {
33 | log.info(e)
34 | }
35 |
36 | module.exports = log
--------------------------------------------------------------------------------
/src/tests/server/documents/utils.js:
--------------------------------------------------------------------------------
1 | const utils = require('../../../server/documents/utils');
2 | const assert = require('assert');
3 |
4 | describe('documents:utils', () => {
5 | describe('When patterns do NOT include wildcards', () => {
6 | it('should do the exact match', () => {
7 | const patterns = 'Intel, Microsoft';
8 | assert.equal(utils.checkPatternWildcard(patterns, 'Intel-like-org'), false);
9 | assert.equal(utils.checkPatternWildcard(patterns, 'Intel'), true);
10 | assert.equal(utils.checkPatternWildcard(patterns, 'Microsoft'), true);
11 | });
12 | });
13 |
14 | describe('When patterns include wildcards', () => {
15 | it('should do the regex match', () => {
16 | const patterns = 'Intel*, Microsoft*';
17 | assert.equal(utils.checkPatternWildcard(patterns, 'Intel-like-user-name'), true);
18 | assert.equal(utils.checkPatternWildcard(patterns, 'Microsoft-like-user-name'), true);
19 | });
20 | });
21 | });
--------------------------------------------------------------------------------
/src/client/assets/styles/_animation.scss:
--------------------------------------------------------------------------------
1 | .counter1 {
2 | -webkit-animation: counter 1s 1s 1 ;
3 | -moz-animation: counter 1s 1s 1 ;
4 | -o-animation: counter 1s 1s 1 ;
5 | animation: counter 1s 1s 1 ;
6 | }
7 |
8 | .counter2 {
9 | -webkit-animation: counter 1s 3s 1 ;
10 | -moz-animation: counter 1s 3s 1 ;
11 | -o-animation: counter 1s 3s 1 ;
12 | animation: counter 1s 3s 1 ;
13 | }
14 |
15 | .counter3 {
16 | -webkit-animation: counter 1s 5s 1 ;
17 | -moz-animation: counter 1s 5s 1 ;
18 | -o-animation: counter 1s 5s 1 ;
19 | animation: counter 1s 5s 1 ;
20 | }
21 |
22 | @-webkit-keyframes counter {
23 | 0%, 100% { color: $gray; }
24 | 50% { color: $gray-darkest; }
25 | }
26 | @-moz-keyframes counter {
27 | 0%, 100% { color: $gray; }
28 | 50% { color: $gray-darkest; }
29 | }
30 | @-o-keyframes counter {
31 | 0%, 100% { color: $gray; }
32 | 50% { color: $gray-darkest; }
33 | }
34 | @keyframes counter {
35 | 0%, 100% { color: $gray; }
36 | 50% { color: $gray-darkest; }
37 | }
--------------------------------------------------------------------------------
/src/client/modals/templates/info_share_gist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
What happens if I choose to share the gist with multiple repos or orgs?
5 |
6 | Contributors will simply need to sign only once for any of the repos or orgs linked with the same shared gist.
7 |
8 |
9 |
10 |
Are previous CLA signatures still valid after I choose to share the gist with multiple repos or orgs?
11 |
12 | Yes, but the scope of the previous signatures are still limited to the previous repo or org.
13 |
14 |
15 |
16 |
What happens if I uncheck the box and choose NOT to share the gist any more?
17 |
18 | Previous contributors that have signed the shared gist will have to sign again.
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/client/modals/templates/error_modal.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Oops, something went wrong...
7 |
8 |
9 |
14 |
15 |
What could be the problem?
16 |
17 | The Gist has been deleted
18 | The Webhook in your repository could not be found
19 |
20 |
21 |
22 | OK
23 |
24 |
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/zzz_cleanup.spec.js:
--------------------------------------------------------------------------------
1 | const github = require('./githubUtils')
2 | const cla = require('./claUtils')
3 |
4 | let testUserName = process.env.TEST_USER_NAME
5 | let testUserPass = process.env.TEST_USER_PASS
6 | let testContributorName = process.env.TEST_CONTRIBUTOR_NAME
7 | let testContributorPass = process.env.TEST_CONTRIBUTOR_PASS
8 |
9 |
10 | Feature('Cleanup Linked Repos')
11 |
12 | Scenario('cleanup CLA assistant', (I) => {
13 | cla.removeLinkedRepo(I, testUserName, 'repo1')
14 | cla.removeLinkedRepo(I, testUserName, 'repo2')
15 | })
16 |
17 | Scenario('cleanup GitHub', (I) => {
18 | session('owner', () => {
19 | github.login(I, testUserName, testUserPass)
20 | github.deleteRepo(I, testUserName, 'repo1')
21 | github.deleteRepo(I, testUserName, 'repo2')
22 | github.revokePermissions(I)
23 | })
24 |
25 | session('contributor', () => {
26 | github.login(I, testContributorName, testContributorPass)
27 | github.deleteRepo(I, testContributorName, 'repo1')
28 | github.deleteRepo(I, testContributorName, 'repo2')
29 | github.revokePermissions(I)
30 | })
31 | })
--------------------------------------------------------------------------------
/src/server/templates/badge.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | CLA
16 | CLA
17 | signed
18 | signed
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/server/templates/badge_signed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | CLA
16 | CLA
17 | signed
18 | signed
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/server/documents/repo.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const utils = require('./utils')
3 | // const logger = require('../services/logger')
4 |
5 | const RepoSchema = mongoose.Schema({
6 | repoId: String,
7 | repo: String,
8 | owner: String,
9 | gist: String,
10 | token: String,
11 | sharedGist: Boolean,
12 | minFileChanges: Number,
13 | minCodeChanges: Number,
14 | whiteListPattern: String,
15 | privacyPolicy: String,
16 | updated_at: Date
17 | })
18 |
19 | const index = {
20 | repoId: 1,
21 | repo: 1,
22 | owner: 1
23 | }
24 | const indexOptions = {
25 | unique: true
26 | }
27 |
28 | RepoSchema.methods.isUserWhitelisted = function (user) {
29 | return utils.checkPatternWildcard(this.whiteListPattern, user)
30 | }
31 |
32 | const Repo = mongoose.model('Repo', RepoSchema)
33 |
34 | // Repo.collection.dropAllIndexes(function (err, results) {
35 | // if (err) {
36 | // logger.warn('Repo collection dropAllIndexes error: ', err)
37 | // logger.warn('dropAllIndexes results: ', results)
38 | // }
39 | // })
40 |
41 | Repo.collection.createIndex(index, indexOptions)
42 |
43 | module.exports = {
44 | Repo: Repo
45 | }
--------------------------------------------------------------------------------
/src/server/templates/badge_not_signed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | CLA
16 | CLA
17 | not signed yet
18 | not signed yet
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/client/modals/templates/add_scope.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Why link organizations?
6 |
7 | If you link an organization with your CLA, CLA assistant sets a web hook on your organization and listens to Pull Requests of all repositories in the organization. That means that your CLA becomes active for each existing and future repositories of your organization.
8 |
9 |
10 |
11 |
12 |
How can I link an organization?
13 |
14 | CLA assistant needs an additional authorization from you to be able to create web hooks for organizations. To grant CLA assistant appropriate rights just click on the button below. For more information on Authorization scopes see
github documentation
15 |
16 |
17 |
18 |
19 |
20 | Yes, let's go for it!
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/server/documents/cla.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | // const logger = require('../services/logger')
3 | mongoose.Promise = require('q').Promise
4 |
5 | const CLASchema = mongoose.Schema({
6 | created_at: Date,
7 | end_at: Date,
8 | custom_fields: String,
9 | gist_url: String,
10 | gist_version: String,
11 | owner: String,
12 | ownerId: String,
13 | repo: String,
14 | repoId: String,
15 | org_cla: Boolean,
16 | user: String,
17 | userId: String,
18 | origin: String,
19 | updated_at: Date
20 | })
21 |
22 | const index = {
23 | repo: 1,
24 | repoId: 1,
25 | owner: 1,
26 | ownerId: 1,
27 | userId: 1,
28 | gist_url: 1,
29 | gist_version: 1,
30 | org_cla: 1
31 | }
32 | const indexOptions = {
33 | unique: true,
34 | partialFilterExpression: { userId: { $exists: true } },
35 | background: true,
36 | }
37 |
38 | const CLA = mongoose.model('CLA', CLASchema)
39 |
40 | // CLA.collection.dropAllIndexes(function (err, results) {
41 | // if (err) {
42 | // logger.warn('CLA collection dropAllIndexes error: ', err)
43 | // logger.warn('dropAllIndexes results: ', results)
44 | // }
45 | // })
46 | CLA.collection.createIndex(index, indexOptions)
47 |
48 | module.exports = {
49 | CLA: CLA
50 | }
--------------------------------------------------------------------------------
/src/client/assets/images/CLA_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
14 |
15 |
--------------------------------------------------------------------------------
/src/client/modals/templates/confirmLink.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Would you like to link this CLA to your {{item.full_name ? 'repository' : 'organization'}}?
7 |
8 |
9 |
10 |
11 |
12 |
{{gist.name}}
13 |
{{item.full_name ? item.full_name : item.login}}
14 |
15 |
16 |
17 |
18 | CLA assistant will...
19 |
20 | Create a webhook in your {{item.full_name ? 'repository' : 'organization'}} and listen for pull requests
21 | Set a pull request CLA status
22 | Comment on pull requests
23 |
24 |
25 |
26 |
27 | Cancel
28 | Yes, let's do this!
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/client/assets/js/CLA_signature_MouseOver_edgeActions.js:
--------------------------------------------------------------------------------
1 | /***********************
2 | * Adobe Edge Animate Composition Actions
3 | *
4 | * Edit this file with caution, being careful to preserve
5 | * function signatures and comments starting with 'Edge' to maintain the
6 | * ability to interact with these actions from within Adobe Edge Animate
7 | *
8 | ***********************/
9 | (function($, Edge, compId){
10 | var Composition = Edge.Composition, Symbol = Edge.Symbol; // aliases for commonly used Edge classes
11 |
12 | //Edge symbol: 'stage'
13 | (function(symbolName) {
14 |
15 |
16 | Symbol.bindTriggerAction(compId, symbolName, "Default Timeline", 1000, function(sym, e) {
17 | // insert code here
18 | // Play the timeline at a label or specific time. For example:
19 | // sym.play(500); or sym.play("myLabel");
20 | sym.play('mouseover');
21 |
22 | });
23 | //Edge binding end
24 |
25 | Symbol.bindTriggerAction(compId, symbolName, "Default Timeline", 639, function(sym, e) {
26 | // insert code here
27 | sym.stop('start');
28 |
29 | });
30 | //Edge binding end
31 |
32 | Symbol.bindElementAction(compId, symbolName, "${CLA_screen-32}", "mouseover", function(sym, e) {
33 | sym.play('MoveRay');
34 |
35 | });
36 |
37 | })("stage");
38 | //Edge symbol end:'stage'
39 |
40 | })(window.jQuery || AdobeEdge.$, AdobeEdge, "EDGE-110781156");
--------------------------------------------------------------------------------
/src/client/assets/styles/_btn.scss:
--------------------------------------------------------------------------------
1 | .btn {
2 | /*border-radius: 5px;*/
3 | }
4 |
5 | .btn {
6 | &.btn-info {
7 | @include button-variant($brand-primary-lightest, $brand-primary, $brand-primary);
8 | /*@color; @background; @border*/
9 | }
10 | &.btn-success {
11 | @include button-variant($brand-primary-lightest, $green, $green);
12 | /*@color; @background; @border*/
13 | }
14 | &.btn-cancel {
15 | @include button-variant($gray-darker, $gray, $gray);
16 | /*@color; @background; @border*/
17 | }
18 |
19 | &.disabled,
20 | &[disabled],
21 | fieldset[disabled] & {
22 | @include opacity(.3);
23 | }
24 | }
25 |
26 | .btn_padding {
27 | padding: 6px 12px;
28 | }
29 |
30 | .btn_tab,
31 | .btn_tab:active, .btn_tab.active,
32 | .btn_tab:focus, .btn_tab.focus,
33 | .btn_tab:hover, .btn_tab.hover
34 | {
35 | z-index: 1;
36 | background-color: $gray-light;
37 | border-bottom-color: $gray-light;
38 | border-top-color: darken($brand-info, 12%);
39 | border-right-color: darken($brand-info, 12%);
40 | border-left-color: darken($brand-info, 12%);
41 | outline: none;
42 | }
43 |
44 | .btn-file {
45 | position: relative;
46 | overflow: hidden;
47 | margin-right: 15px;
48 | }
49 | .btn-file input[type=file] {
50 | position: absolute;
51 | top: 0;
52 | right: 0;
53 | min-width: 100%;
54 | min-height: 100%;
55 | font-size: 100px;
56 | text-align: right;
57 | filter: alpha(opacity=0);
58 | opacity: 0;
59 | outline: none;
60 | background: white;
61 | cursor: inherit;
62 | display: block;
63 | }
64 |
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/link_repo1_sign_cla.spec.js:
--------------------------------------------------------------------------------
1 | const github = require('./githubUtils')
2 | const cla = require('./claUtils')
3 |
4 | let testUserName = process.env.TEST_USER_NAME
5 | let testUserPass = process.env.TEST_USER_PASS
6 | let testContributorName = process.env.TEST_CONTRIBUTOR_NAME
7 | let testContributorPass = process.env.TEST_CONTRIBUTOR_PASS
8 |
9 |
10 | Feature('Create and link repo1')
11 |
12 | Scenario('create a repo', (I) => {
13 | // github.login(I, testUserName, testUserPass)
14 | github.createRepo(I, testUserName, 'repo1')
15 | })
16 |
17 | Scenario('Link repo1', (I) => {
18 | cla.linkRepo(I, testUserName, 'repo1')
19 | })
20 |
21 | Feature('Pull Request')
22 |
23 | Scenario('create a PR', (I) => {
24 | session('contributor', () => {
25 | github.login(I, testContributorName, testContributorPass)
26 | github.createPR(I, testUserName, 'repo1')
27 |
28 | I.wait(3)
29 | I.waitForElement('a.status-actions', 20)
30 | I.see('Pending')
31 | I.waitForVisible('//h3[contains(@class, "timeline-comment-header-text") and contains(., "CLAassistant")]', 10)
32 | I.see('Thank you for your submission')
33 |
34 | I.click('a.status-actions')
35 | I.seeInCurrentUrl('preview.cla-assistant.io/claowner1/repo1')
36 | I.click('//button[@class="btn btn-info btn-lg"]')
37 | I.waitForEnabled('//button[contains(., "Authorize")]', 5)
38 | I.click('//button[contains(., "Authorize")]')
39 |
40 | I.waitInUrl('github.com', 10)
41 | I.seeInCurrentUrl('github.com/claowner1/repo1')
42 | I.see('All committers have signed the CLA')
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/src/client/modals/templates/badge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Get your badge url
5 |
6 |
7 |
20 |
21 |
22 | {{selectedType.type.url}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
--------------------------------------------------------------------------------
/DESCRIPTION.MD:
--------------------------------------------------------------------------------
1 | ### Tagline
2 | Manages the signing of Contributor License Agreements (CLAs) within GitHub Flow.
3 |
4 | ### Short Description
5 | Streamline your workflow and let CLA assistant handle the legal side of contributions to a repository for you. CLA assistant enables contributors to sign CLAs from within a pull request.
6 |
7 | ### Description
8 | To get started, simply store your CLA as a GitHub Gist file then link it with the repository in CLA assistant. Then sit back and relax while CLA assistant:
9 |
10 | - Comments on each opened pull request to ask the contributor to sign the CLA
11 | - Allows contributors to sign a CLA from within a pull request
12 | - Authenticates the signee with his or her GitHub account
13 | - Updates the status of a pull request when the contributor agrees to the CLA
14 | - Automatically asks users to re-sign the CLA for each new pull request in the event the associated Gist & CLA has changed
15 |
16 |
17 |
18 |
19 |
20 | Repository owners can review a list of users who signed the CLA for each version of it. To get started, visit https://cla-assistant.io.
21 |
22 | ### Contacts
23 | - technical support contact – tools@sap.com
24 | - escalation contact - t.jansen@sap.com
25 |
26 | ### Documentation
27 | - Documentation - https://cla-assistant.io/
28 | - Terms of service - https://gist.github.com/CLAassistant/3a73e4cd729c9d0a6e30
29 | - Privacy Policy - same
30 | - Support - tools@sap.com
31 | - Status - https://twitter.com/cla_assistant
32 | - Pricing - for free. For all. For everything
33 |
34 | ### Category
35 | Collaborate
36 |
--------------------------------------------------------------------------------
/src/client/templates/customField.html:
--------------------------------------------------------------------------------
1 |
2 |
{{title || name}}
3 |
({{description}})
4 |
6 |
7 |
8 |
{{title || name}}
9 |
10 |
13 |
14 |
15 |
16 |
17 | {{title || name}}
18 |
19 |
20 |
21 |
{{title || name}}
22 |
23 | {{prop}}
25 |
26 |
27 |
28 | {{value[name]}}
29 |
--------------------------------------------------------------------------------
/src/client/assets/images/link_button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
22 |
23 |
25 |
26 |
--------------------------------------------------------------------------------
/src/client/services/linkItem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.factory('linkItemService', ['$RPCService',
4 | function ($RPCService) {
5 | function createNewItem(item, options, type) {
6 | if (!type) {
7 | type = options;
8 | options = item;
9 | }
10 | var newItem = {
11 | gist: options.gist.url,
12 | sharedGist: options.sharedGist,
13 | whiteListPattern: options.whiteListPattern,
14 | minFileChanges: options.minFileChanges,
15 | minCodeChanges: options.minCodeChanges,
16 | privacyPolicy: options.privacyPolicy
17 | };
18 | if (type === 'repo') {
19 | newItem.repoId = item.repoId || item.id;
20 | newItem.repo = item.repo || item.name;
21 | newItem.owner = item.owner.login || item.owner;
22 | } else {
23 | newItem.orgId = item.orgId || item.id;
24 | newItem.org = item.org || item.login;
25 | newItem.excludePattern = options.excludePattern;
26 | }
27 |
28 | return newItem;
29 | }
30 |
31 | return {
32 | createLink: function (item, options) {
33 | var type = item.full_name ? 'repo' : 'org';
34 | var newItem = createNewItem(item, options, type);
35 |
36 | return $RPCService.call(type, 'create', newItem);
37 | },
38 |
39 | updateLink: function (item) {
40 | var type = item.repoId ? 'repo' : 'org';
41 | var newItem = createNewItem(item, type);
42 |
43 | return $RPCService.call(type, 'update', newItem);
44 | }
45 | };
46 | }
47 | ]);
--------------------------------------------------------------------------------
/src/server/controller/badge.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | ejs = require('ejs'),
3 | fs = require('fs'),
4 | path = require('path'),
5 | crypto = require('crypto')
6 |
7 | //api
8 | const cla = require('./../api/cla')
9 |
10 | const router = express.Router()
11 |
12 | router.all('/pull/badge/:signed', (req, res) => {
13 | const fileName = req.params.signed === 'signed' ? 'badge_signed.svg' : 'badge_not_signed.svg'
14 | const status = req.params.signed === 'signed' ? 'signed' : 'pending'
15 | const tmp = fs.readFileSync(path.join(__dirname, '..', 'templates', fileName), 'utf-8')
16 | const hash = crypto.createHash('md5').update(status, 'utf8').digest('hex')
17 |
18 | if (req.get('If-None-Match') === hash) {
19 | return res.status(304).send()
20 | }
21 |
22 | const svg = ejs.render(tmp)
23 |
24 | res.set('Content-Type', 'image/svg+xml')
25 | res.set('Cache-Control', 'no-cache')
26 | res.set('Etag', hash)
27 | res.send(svg)
28 | })
29 |
30 |
31 | router.all('/readme/badge/:owner/:repo', async (req, res) => {
32 | req.args = {
33 | owner: req.params.owner,
34 | repo: req.params.repo
35 | }
36 | let count = 0
37 | try {
38 | count = await cla.countCLA(req)
39 | } catch (error) {
40 | count = 0
41 | }
42 | let url = 'https://img.shields.io/badge/CLAs signed-' + count + '-0594c6.svg?'
43 | if (req.query) {
44 | url = req.query.style ? `${url}&style=${req.query.style}` : url
45 | url = req.query.label ? `${url}&label=${req.query.label}` : url
46 | url = req.query.colorB ? `${url}&colorB=${req.query.colorB}` : url
47 | url = req.query.logo ? `${url}&logo=${req.query.logo}` : url
48 | }
49 | res.redirect(url)
50 | })
51 |
52 | module.exports = router
53 |
--------------------------------------------------------------------------------
/src/client/assets/images/link_active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
23 |
24 |
26 |
27 |
--------------------------------------------------------------------------------
/src/client/assets/images/linked.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
20 |
21 |
23 |
24 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/client/modals/templates/confirmRemove.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | Are you sure you want to unlink?
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Unlinking will...
24 |
25 | Remove the CLA assistant webhook in your repository/organization
26 | Remove the link to your list of contributors
27 |
28 |
29 |
30 |
31 | Keep it
32 | Unlink anyway
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/client/assets/images/link_inactive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
26 |
27 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/client/modals/claView.js:
--------------------------------------------------------------------------------
1 | module.controller('ClaViewCtrl', function ($scope, $modalInstance, $window, cla, $state, $RPCService, $sce, utils) {
2 | $scope.claObj = cla;
3 | $scope.cla = null;
4 | $scope.modalInstance = $modalInstance;
5 |
6 | $scope.getGistName = function (gistObj) {
7 | return utils.getGistAttribute(gistObj, 'filename');
8 | };
9 |
10 | function getCLA() {
11 | return $RPCService.call('cla', 'get', {
12 | repo: $scope.claObj.repo,
13 | owner: $scope.claObj.owner,
14 | gist: {
15 | gist_url: $scope.claObj.gist_url,
16 | gist_version: $scope.claObj.gist_version
17 | }
18 | }, function (err, gist) {
19 | if (!err) {
20 | $scope.claText = gist.value.raw;
21 | }
22 | });
23 | }
24 |
25 | getCLA().then(function (data) {
26 | $scope.cla = $sce.trustAsHtml(data.value.raw);
27 | $scope.cla.text = data.value.raw;
28 | //console.log($scope.cla.text);
29 | });
30 |
31 |
32 | $scope.cancel = function () {
33 | $modalInstance.dismiss('cancel');
34 | };
35 |
36 | })
37 | .directive('clacontent', [function () {
38 | return {
39 | link: function (scope, element, attrs) {
40 | scope.$watch(attrs.clacontent, function (content) {
41 | if (content) {
42 | var linkIcons = element[0].getElementsByClassName('octicon octicon-link');
43 | for (var index in linkIcons) {
44 | if (linkIcons.hasOwnProperty(index)) {
45 | angular.element(linkIcons[index]).removeClass('octicon octicon-link');
46 | }
47 | }
48 | }
49 | });
50 | }
51 | };
52 | }]);
53 |
--------------------------------------------------------------------------------
/src/server/controller/user.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport')
2 | const express = require('express')
3 | const utils = require('../middleware/utils')
4 |
5 | const router = express.Router()
6 | let scope
7 |
8 | function checkReturnTo(req, res, next) {
9 | scope = null
10 | req.session.requiredScope = null
11 | if (!req.session) {
12 | req.session = {}
13 | }
14 |
15 | if (req.query.public === 'true') {
16 | scope = config.server.github.user_scope.concat()
17 | req.session.requiredScope = 'public'
18 | }
19 | if (req.query.admin === 'true') {
20 | scope = config.server.github.admin_scope.concat()
21 | req.session.requiredScope = 'admin'
22 | }
23 | if (req.query.org_admin === 'true') {
24 | scope.push('admin:org_hook')
25 | req.session.requiredScope = 'org_admin'
26 | }
27 |
28 | req.session.returnTo = req.query.public === 'true' ? req.session.next || req.headers.referer : '/'
29 |
30 | passport.authenticate('github', {
31 | scope: scope
32 | })(req, res, next)
33 | }
34 |
35 | router.get('/auth/github', checkReturnTo)
36 |
37 | router.get('/auth/github/callback', passport.authenticate('github', {
38 | failureRedirect: '/'
39 | }),
40 | function (req, res) {
41 | if (req.user && req.session.requiredScope != 'public' && utils.couldBeAdmin(req.user.login) && (!req.user.scope || req.user.scope.indexOf('write:repo_hook') < 0)) {
42 | return res.redirect('/auth/github?admin=true')
43 | }
44 | res.redirect(req.session.returnTo || req.headers.referer || '/')
45 | req.session.next = null
46 | })
47 |
48 | router.get('/logout',
49 | function (req, res, next) {
50 | req.logout()
51 | if (!req.query.noredirect) {
52 | res.redirect('/')
53 | } else {
54 | next()
55 | }
56 | }
57 | )
58 |
59 | module.exports = router
--------------------------------------------------------------------------------
/src/client/modals/templates/linkSuccess.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Awesome!
5 |
6 |
7 | Oops! Something went wrong!
8 |
9 |
10 |
19 |
20 |
21 |
{{selected.gist.name}} and {{selected.item.full_name ? selected.item.full_name : selected.item.login}}
22 | are now linked
23 |
24 |
25 |
26 |
27 |
28 |
29 | Error:
30 | {{ error }}
31 |
32 |
33 |
Great, thanks!
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/server/services/org.js:
--------------------------------------------------------------------------------
1 | require('../documents/org');
2 | const mongoose = require('mongoose');
3 | const Org = mongoose.model('Org');
4 |
5 | const selection = function (args) {
6 | const selectArguments = args.orgId ? { orgId: args.orgId } : { org: args.org };
7 |
8 | return selectArguments;
9 | };
10 |
11 | class OrgService {
12 | async create(args) {
13 | return Org.create({
14 | orgId: args.orgId,
15 | org: args.org,
16 | gist: args.gist,
17 | token: args.token,
18 | excludePattern: args.excludePattern,
19 | sharedGist: !!args.sharedGist,
20 | minFileChanges: args.minFileChanges,
21 | minCodeChanges: args.minCodeChanges,
22 | whiteListPattern: args.whiteListPattern,
23 | privacyPolicy: args.privacyPolicy,
24 | updatedAt: new Date()
25 | })
26 | }
27 |
28 | async get(args) {
29 | return Org.findOne(selection(args))
30 | }
31 |
32 | async update(args) {
33 | const org = await this.get(args)
34 | org.gist = args.gist
35 | org.token = args.token ? args.token : org.token
36 | org.sharedGist = !!args.sharedGist
37 | org.excludePattern = args.excludePattern
38 | org.minFileChanges = args.minFileChanges
39 | org.minCodeChanges = args.minCodeChanges
40 | org.whiteListPattern = args.whiteListPattern
41 | org.privacyPolicy = args.privacyPolicy
42 | org.updatedAt = new Date()
43 |
44 | return org.save()
45 | }
46 |
47 | async getMultiple(args) {
48 | return Org.find({ orgId: { $in: args.orgId } })
49 | }
50 |
51 | async getOrgWithSharedGist(gist) {
52 | return Org.find({ gist: gist, sharedGist: true })
53 | }
54 |
55 | remove(args) {
56 | return Org.findOneAndRemove(selection(args))
57 | }
58 | }
59 |
60 | const orgService = new OrgService()
61 | module.exports = orgService
62 |
--------------------------------------------------------------------------------
/src/server/passports/token.js:
--------------------------------------------------------------------------------
1 | const github = require('../services/github')
2 | const passport = require('passport')
3 | const Strategy = require('passport-accesstoken').Strategy
4 | const merge = require('merge')
5 | const User = require('mongoose').model('User')
6 |
7 | function getGHUser(accessToken) {
8 | const args = {
9 | obj: 'users',
10 | fun: 'getAuthenticated',
11 | token: accessToken
12 | }
13 |
14 | return github.call(args)
15 | }
16 |
17 | async function checkToken(accessToken) {
18 | const args = {
19 | obj: 'oauthAuthorizations',
20 | fun: 'checkAuthorization',
21 | arg: {
22 | access_token: accessToken,
23 | client_id: config.server.github.client
24 | },
25 | basicAuth: {
26 | user: config.server.github.client,
27 | pass: config.server.github.secret
28 | }
29 | }
30 | const res = await github.call(args)
31 | if (!res.data || (res.data && res.data.scopes && res.data.scopes.indexOf('write:repo_hook') < 0)) {
32 | throw new Error('You have not enough rights to call this API')
33 | }
34 | return res.data
35 | }
36 |
37 | passport.use(new Strategy(
38 | async (token, done) => {
39 | try {
40 | const res = await getGHUser(token)
41 | if (!res || !res.data) {
42 | throw new Error('Could not find GitHub user for given token')
43 | }
44 | const dbUser = await User.findOne({ uuid: res.data.id, name: res.data.login })
45 | if (!dbUser) {
46 | throw new Error(`Could not find user ${res.data.login} in the database`)
47 | }
48 | const authorization = await checkToken(dbUser.token)
49 |
50 | done(null, merge(res.data, {
51 | token: dbUser.token,
52 | scope: authorization.scopes.toString()
53 | }))
54 | } catch (error) {
55 | done(error)
56 | }
57 | }
58 | ))
--------------------------------------------------------------------------------
/src/client/modals/badge.js:
--------------------------------------------------------------------------------
1 | module.controller('BadgeCtrl', function ($scope, $modalInstance, $window, repo) {
2 | $scope.claRepo = repo;
3 | $scope.badgeUrl = $window.location + 'readme/badge/' + repo.owner + '/' + repo.repo;
4 | $scope.linkUrl = $window.location + repo.owner + '/' + repo.repo;
5 | $scope.types = [{
6 | type: 'HTML',
7 | url: ' '
8 | }, {
9 | type: 'Image URL',
10 | url: $scope.badgeUrl
11 | }, {
12 | type: 'Markdown',
13 | url: '[](' + $scope.linkUrl + ')'
14 | }, {
15 | type: 'Textile',
16 | url: '!' + $scope.badgeUrl + '(CLA assistant)!:' + $scope.linkUrl
17 | }, {
18 | type: 'RDOC',
19 | url: '{ }[' + $scope.linkUrl + ']'
20 | }];
21 |
22 | $scope.selectedType = {};
23 | $scope.selectedType.type = $scope.types[0];
24 |
25 | $scope.cancel = function () {
26 | $modalInstance.dismiss('cancel');
27 | };
28 | })
29 | // copied from https://github.com/sachinchoolur/ngclipboard/blob/master/src/ngclipboard.js
30 | .directive('ngClipboard', function () {
31 | return {
32 | restrict: 'A',
33 | scope: {
34 | ngClipboardSuccess: '&',
35 | ngClipboardError: '&'
36 | },
37 | link: function (scope, element) {
38 | var clipboard = new Clipboard(element[0]);
39 |
40 | clipboard.on('success', function (e) {
41 | scope.ngClipboardSuccess({
42 | e: e
43 | });
44 | });
45 |
46 | clipboard.on('error', function (e) {
47 | scope.ngClipboardError({
48 | e: e
49 | });
50 | });
51 | }
52 | };
53 | });
54 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true,
6 | "mocha": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "sourceType": "module",
11 | "ecmaVersion": 8
12 | },
13 | "globals": {
14 | "$": false,
15 | "angular": false,
16 | "chunk": true,
17 | "config": true,
18 | "filters": true,
19 | "ga": false,
20 | "io": false,
21 | "models": true,
22 | "module": true,
23 | "moment": false,
24 | "server": true,
25 | "Papa": true,
26 | "Clipboard": true
27 | },
28 | "rules": {
29 | "dot-notation": "error",
30 | "func-call-spacing": "error",
31 | "func-name-matching": "error",
32 | "handle-callback-err": "warn",
33 | "no-confusing-arrow": "error",
34 | "no-div-regex": "error",
35 | "no-duplicate-imports": "error",
36 | "no-else-return": "error",
37 | "no-empty-function": "error",
38 | "no-multiple-empty-lines": "error",
39 | "no-native-reassign": "error",
40 | "no-path-concat": "warn",
41 | "no-trailing-spaces": "warn",
42 | "no-undef": "error",
43 | "no-unused-expressions": "warn",
44 | "no-unused-vars": "warn",
45 | "no-use-before-define": [
46 | "error",
47 | {
48 | "functions": false,
49 | "variables": false
50 | }
51 | ],
52 | "no-useless-concat": "error",
53 | "no-useless-constructor": "error",
54 | "no-useless-rename": "error",
55 | "no-useless-return": "error",
56 | "quotes": [
57 | 1,
58 | "single"
59 | ],
60 | "valid-typeof": "warn",
61 | "semi-spacing": "warn",
62 | "space-infix-ops": "warn",
63 | "yoda": [
64 | "error",
65 | "never"
66 | ]
67 | }
68 | };
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/claUtils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | linkRepo: function (I, owner, repo) {
3 | I.amOnPage('https://preview.cla-assistant.io/')
4 | I.waitForVisible('//button[contains(., "Configure CLA")]', 5)
5 | I.waitForInvisible('.loading-indicator', 5)
6 | I.click('//button[contains(., "Configure CLA")]')
7 |
8 | I.see('Choose a repository')
9 | I.click('//*[@id="activated_cla"]/div[3]/div[2]/div[2]/div/div[1]')
10 | I.wait(0.5)
11 | I.see(`${owner}/${repo}`)
12 | I.pressKey('Enter')
13 |
14 | I.see('Choose a CLA')
15 | I.click('//*[@id="activated_cla"]/div[3]/div[3]/div[2]/div/div[1]')
16 | I.wait(0.5)
17 | I.see('SAP individual CLA')
18 | I.pressKey('Enter')
19 |
20 | I.waitForVisible('//button[contains(., "LINK")]', 5)
21 | I.click('//button[contains(., "LINK")]')
22 | I.wait(1)
23 | I.waitForVisible('//button[contains(., "Yes")]', 5)
24 | I.click('//button[contains(., "Yes")]')
25 | I.wait(1)
26 | I.waitForVisible('//button[contains(., "Great")]', 5)
27 | I.click('//button[contains(., "Great")]')
28 |
29 | I.waitForVisible('table.table', 5)
30 | I.see('Linked Repositories')
31 | I.see(`${owner} / ${repo}`)
32 | },
33 |
34 | removeLinkedRepo: function (I, owner, repo) {
35 | I.amOnPage('https://preview.cla-assistant.io/')
36 | I.waitForVisible('table.table', 5)
37 | I.see('Linked Repositories')
38 | I.waitForInvisible('.loading-indicator', 5)
39 | I.waitForVisible('.fa-trash-o', 5)
40 | I.see(`${owner} / ${repo}`)
41 |
42 | I.moveCursorTo('.fa-trash-o')
43 | I.click('.fa-trash-o')
44 | I.wait(3)
45 | I.see('Unlinking will...')
46 | I.waitForVisible('//button[contains(., "Unlink")]')
47 | I.click('//button[contains(., "Unlink")]')
48 | I.wait(2)
49 | I.see('Linked Repositories')
50 | I.dontSee(`${owner} / ${repo}`)
51 | },
52 | };
--------------------------------------------------------------------------------
/src/tests/server/documents/org.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert')
2 | const Org = require('../../../server/documents/org').Org
3 |
4 | // test data
5 | const testData = require('../testData').data
6 |
7 | describe('org document', () => {
8 | it('should properly work with legacy organisations without excludePattern', async () => {
9 | const testOrg = testData.org_from_db
10 | const org = new Org(testOrg)
11 |
12 | assert.equal(org.isRepoExcluded('foo'), false)
13 | assert.equal(org.isRepoExcluded('qux'), false)
14 | })
15 |
16 | it('should properly parse excluded repositories', async () => {
17 | const testOrg = testData.org_from_db_with_excluded_patterns
18 | const org = new Org(testOrg)
19 |
20 | assert.equal(org.isRepoExcluded('foo'), true)
21 | assert.equal(org.isRepoExcluded('qux'), false)
22 | })
23 |
24 | it('should properly parse empty exclusion pattern', async () => {
25 | const testOrg = testData.org_from_db_with_empty_excluded_patterns
26 | const org = new Org(testOrg)
27 |
28 | assert.equal(org.isRepoExcluded('foo'), false)
29 | assert.equal(org.isRepoExcluded('qux'), false)
30 | })
31 |
32 | it('should properly parse whitelisted users', async () => {
33 | const testOrg = testData.org_from_db_with_excluded_patterns
34 | testOrg.whiteListPattern = 'login0,*1,*[bot]'
35 | const org = new Org(testOrg)
36 |
37 | assert.equal(org.isUserWhitelisted('login0'), true)
38 | assert.equal(org.isUserWhitelisted('login1'), true)
39 | assert.equal(org.isUserWhitelisted('user[bot]'), true)
40 | assert.equal(org.isUserWhitelisted('login2'), false)
41 | })
42 |
43 | it('should properly parse empty whitelist pattern', async () => {
44 | const testOrg = testData.org_from_db_with_empty_excluded_patterns
45 | const org = new Org(testOrg)
46 |
47 | assert.equal(org.isUserWhitelisted('login0'), false)
48 | assert.equal(org.isUserWhitelisted('login1'), false)
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/whitelist_user_on_repo2.spec.js:
--------------------------------------------------------------------------------
1 | const github = require(`./githubUtils`)
2 | const cla = require(`./claUtils`)
3 |
4 | let testUserName = process.env.TEST_USER_NAME
5 | let testUserPass = process.env.TEST_USER_PASS
6 | let testContributorName = process.env.TEST_CONTRIBUTOR_NAME
7 | let testContributorPass = process.env.TEST_CONTRIBUTOR_PASS
8 |
9 |
10 | Feature(`Create and link repo2`)
11 |
12 | Scenario(`create repo2`, (I) => {
13 | github.createRepo(I, testUserName, `repo2`)
14 | })
15 |
16 | Scenario(`link repo2`, (I) => {
17 | cla.linkRepo(I, testUserName, `repo2`)
18 | })
19 |
20 | Scenario(`Add whitelisted user for repo2`, (I) => {
21 | I.amOnPage(`https://preview.cla-assistant.io/`)
22 | I.waitForVisible(`//button[contains(., "Configure CLA")]`, 5)
23 | I.waitForInvisible(`.loading-indicator`, 5)
24 | I.waitForVisible(`table.table`, 5)
25 | I.see(`Linked Repositories`)
26 | I.see(`${testUserName} / repo2`)
27 |
28 | I.moveCursorTo(`//tr[contains(., "${testUserName} / repo2")]//i[contains(@class,"fa-ellipsis-h")]`)
29 | I.click(`//tr[contains(., "${testUserName} / repo2")]//i[contains(@class,"fa-ellipsis-h")]`)
30 | I.click(`Edit`)
31 | I.waitForEnabled(`#whiteListPattern`, 2)
32 | I.fillField(`#whiteListPattern`, testContributorName)
33 | I.wait(2)
34 | I.waitForElement(`//button[contains(.,"Save")]`)
35 | I.moveCursorTo(`//button[contains(.,"Save")]`)
36 | I.click(`//button[contains(.,"Save")]`)
37 | I.waitForInvisible(`#whiteListPattern`, 5)
38 | })
39 |
40 | Feature(`Pull Request`)
41 |
42 | Scenario(`create a PR`, (I) => {
43 | session(`contributor`, () => {
44 | github.login(I, testContributorName, testContributorPass)
45 | github.createPR(I, testUserName, `repo2`)
46 |
47 | I.wait(3)
48 | I.waitForElement(`//button[contains(.,"Show all checks")]`, 20)
49 | I.click(`//button[contains(.,"Show all checks")]`)
50 | I.wait(1)
51 | I.see(`successful check`)
52 | I.see(`All CLA requirements met`)
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/src/client/modals/templates/versionView.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 | Sorry, your actual signed CLA is outdated...
12 |
13 |
14 | A new CLA has been linked to this Repository.
15 |
16 |
17 | Take me to the new CLA
18 |
19 |
20 |
21 |
22 |
23 | Sorry, your actual signed CLA is outdated...
24 |
25 |
26 | A new version of the CLA is available.
27 | Please consider to view the changes and to resign the CLA.
28 |
29 |
30 | Show me the changes
31 | Let me resign!
32 |
33 |
34 |
35 |
36 | There is currently no CLA linked to this repository.
37 |
38 |
39 | Please wait, until the owner has linked a CLA.
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/client/modals/templates/whitelist_info.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
How does whitelisting work?
6 |
7 | If a GitHub username is included in the whitelist, they will not be
8 | required to sign a CLA. This also applies to organization usernames.
9 |
10 |
Why whitelist usernames?
11 |
12 | Since there's no way for bot users (such as Dependabot or Greenkeeper) to
13 | sign a CLA, you may want to whitelist them. You can do so by adding their
14 | names (in this case dependabot-preview[bot] and
15 | greenkeeper[bot] separated by a comma) to the whitelist. You can
16 | also use wildcard symbol in case you want to whitelist all bot users
17 | *[bot] .
18 |
19 |
Why whitelist organizations?
20 |
21 | We see at least two use cases you may want to do so:
22 |
23 |
24 | You develop in a team and commit your changes via feature branches. As
25 | soon you create a new pull request CLA assistant would ask you and
26 | your team fellows to sign a CLA even if you are part of the
27 | organization and doesn't need to do so. Now if you whitelist your own
28 | organization name all pull requests coming from the same repository
29 | will pass the CLA check.
30 |
31 |
32 | You get contributions from another company which has already signed
33 | your corporate CLA. You may want to let CLA assistant accept all PRs
34 | coming from this company. Now you can whitelist the GitHub
35 | organization name of this company and all pull requests coming from
36 | this GitHub organization (i.e., from that organization's fork) will pass the check.
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/tests/client/services/utils.spec.js:
--------------------------------------------------------------------------------
1 | /*jshint expr:true*/
2 | /*sinon, describe, xit, it, beforeEach, afterEach*/
3 | var utils, testData;
4 |
5 | describe('Utils service', function () {
6 | beforeEach(angular.mock.module('app'));
7 | beforeEach(angular.mock.module('ngAnimateMock'));
8 |
9 |
10 | beforeEach(angular.mock.inject(function ($injector, _utils_) {
11 | utils = _utils_;
12 |
13 | testData = {};
14 | testData.gist = {
15 | 'url': 'https://api.github.com/gists/aa5a315d61ae9438b18d',
16 | 'id': 'aa5a315d61ae9438b18d',
17 | 'description': 'description of gist',
18 | 'owner': {
19 | 'login': 'octocat',
20 | 'id': 1
21 | },
22 | 'user': null,
23 | 'files': {
24 | 'ring.erl': {
25 | 'filename': 'Ring',
26 | 'content': 'contents of gist',
27 | 'updated_at': '2010-04-14T02:15:15Z'
28 | }
29 | },
30 | 'html_url': 'https://gist.github.com/aa5a315d61ae9438b18d',
31 | 'history': [{
32 | 'url': 'https://api.github.com/gists/aa5a315d61ae9438b18d/57a7f021a713b1c5a6a199b54cc514735d2d462f',
33 | 'version': '57a7f021a713b1c5a6a199b54cc514735d2d462f',
34 | 'user': {
35 | 'login': 'octocat',
36 | 'id': 1
37 | },
38 | 'committed_at': '2010-04-14T02:15:15Z'
39 | }]
40 | };
41 | }));
42 |
43 | it('should return name of the given gist', function () {
44 | var name = utils.getGistAttribute(testData.gist, 'filename');
45 |
46 | name.should.be.equal('Ring');
47 | });
48 |
49 | it('should return file name of the given gist', function () {
50 | testData.gist.files['ring.erl'].filename = undefined;
51 | var name = utils.getGistAttribute(testData.gist, 'fileName');
52 |
53 | name.should.be.equal('ring.erl');
54 | });
55 |
56 | it('should return "updated_at" of the given gist', function () {
57 | var name = utils.getGistAttribute(testData.gist, 'updated_at');
58 |
59 | name.should.be.equal('2010-04-14T02:15:15Z');
60 | });
61 |
62 | it('should not fail if gist obj has no files', function () {
63 | testData.gist.files = undefined;
64 |
65 | var name = utils.getGistAttribute(testData.gist, 'filename');
66 |
67 | (!name).should.be.equal(true);
68 | });
69 | });
--------------------------------------------------------------------------------
/src/client/services/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var reservedGistFileNames = ['metadata'];
3 |
4 | module.factory('utils', ['$q', '$RPCService',
5 | function ($q, $RPCService) {
6 | return {
7 | getGistAttribute: function (gist, attribute) {
8 | var attr;
9 | if (gist && gist.files) {
10 | Object.keys(gist.files).some(function (file) {
11 | if (reservedGistFileNames.indexOf(file) < 0) {
12 | attr = file;
13 |
14 | return true;
15 | }
16 | });
17 | attr = gist.files[attr][attribute] ? gist.files[attr][attribute] : attr;
18 | }
19 |
20 | return attr;
21 | },
22 | getGistContent: function (repoId, orgId, gist_url, gist_version) {
23 | var deferred = $q.defer();
24 | var gistContent = {};
25 | var args = {
26 | repoId: repoId,
27 | orgId: orgId
28 | };
29 | if (gist_url) {
30 | args.gist = {
31 | gist_url: gist_url,
32 | gist_version: gist_version
33 | };
34 | }
35 | $RPCService.call('cla', 'get', args, function (err, cla) {
36 | if (!err) {
37 | gistContent.claText = cla.value.raw;
38 | gistContent.updatedAt = cla.value.updatedAt;
39 | if (cla.value.meta) {
40 | try {
41 | var metaString = cla.value.meta.replace(/|<\/p>|\n|\t/g, '');
42 | gistContent.customFields = JSON.parse(metaString);
43 | gistContent.customKeys = Object.keys(gistContent.customFields);
44 | gistContent.hasCustomFields = true;
45 | deferred.resolve(gistContent);
46 | } catch (ex) {
47 | deferred.reject(ex);
48 | }
49 | } else {
50 | deferred.resolve(gistContent);
51 | }
52 | } else {
53 | deferred.reject(err);
54 | }
55 | });
56 |
57 | return deferred.promise;
58 | }
59 | };
60 | }
61 | ]);
--------------------------------------------------------------------------------
/src/client/assets/styles/_navbar.scss:
--------------------------------------------------------------------------------
1 | $navbar-default-bg: $body-bg;
2 | /*$navbar-default-color: $gray-darkest;
3 | $navbar-default-link-color: $gray-darkest;
4 | $navbar-default-link-hover-color: $gray-darker;
5 | $navbar-default-brand-hover-color: $gray;
6 | $navbar-default-link-active-color: $gray;*/
7 | $navbar-margin-bottom: 0;
8 | $nav-link-padding: 15px 20px 15px 0px;
9 |
10 | $grid-float-breakpoint: 100px;
11 |
12 | $navbar-height: 60px;
13 | $footer-height: $navbar-height;
14 |
15 | .navbar a{
16 | text-decoration: none;
17 | }
18 |
19 |
20 | .navbar-default {
21 | border-color: white;
22 |
23 | .container-fluid{
24 | height: $navbar-height;
25 | margin: 0px;
26 | padding-top: 15px;
27 | }
28 | }
29 |
30 | .navbar-default .navbar-text {
31 | color: $gray-darkest;
32 | margin: 0;
33 | }
34 |
35 | .navbar-home-right{
36 | float: right;
37 | }
38 |
39 | .navbar-home-left{
40 | float: left;
41 | margin-left: 0px
42 | }
43 |
44 | .navbar-fixed-bottom {
45 | position: absolute;
46 | bottom: 0;
47 | width: 100%;
48 | border: transparent;
49 | color: $navbar-default-color;
50 | }
51 |
52 | .navbar-top-border {
53 | height: 1px;
54 | margin: 0px 15px;
55 | background: $brand-primary-lightest;
56 | }
57 |
58 | .navbar-left:first-child {
59 | margin-left: 0px;
60 | }
61 | .navbar{
62 | min-height: $navbar-height;
63 | }
64 |
65 | .navbar-center {
66 | position: absolute;
67 | left: 0;
68 | margin-left: auto;
69 | margin-right: auto;
70 | width: 100%;
71 | text-align: center;
72 | z-index:-1
73 | }
74 |
75 | @media(max-width: 767px) {
76 | .navbar-text {
77 | margin: 0px;
78 | }
79 | .navbar-default .container-fluid{
80 | padding-top: 0px;
81 | }
82 |
83 | .navbar-fixed-top .container-fluid{
84 | padding: 15px;
85 | }
86 |
87 |
88 | .navbar-text-hide{
89 | visibility: hidden;
90 | }
91 |
92 | .navbar-left {
93 | text-align: center;
94 | margin: 0px;
95 | }
96 | }
97 |
98 | @media (min-width: 992px) {
99 | .navbar-top-space {
100 | margin-top: 55px;
101 | }
102 | }
103 |
104 | .nav-justified>li, .nav-justified>.nav-tabs.nav-justified {
105 | bottom: -1px;
106 | }
107 |
108 | nav {
109 | margin-top: 10px;
110 | }
111 |
112 | nav .nav-justified>li>a {
113 | color: $gray;
114 | }
115 |
116 | .nav-justified>.active>a,
117 | .nav>li>a:hover,
118 | .nav>li>a:focus {
119 | background-color: transparent;
120 | }
121 |
122 | .nav-justified>.active>a,
123 | .nav>li>a:hover {
124 | color: $gray-darkest;
125 | border-bottom: solid;
126 | }
127 |
--------------------------------------------------------------------------------
/src/client/modals/editLinkedItem.js:
--------------------------------------------------------------------------------
1 | module.controller('EditLinkedItemCtrl', function ($scope, $modalInstance, $window, $modal, linkItemService, item, gist, gists) {
2 | $scope.linkedItem = item;
3 | $scope.gists = gists;
4 | $scope.selected = {};
5 | $scope.errorMsg = [];
6 |
7 | $scope.selected.item = angular.copy(item);
8 |
9 | function initGist() {
10 | $scope.selected.gist = { name: gist.description || gist.fileName, url: gist.html_url } || gists.find(function (g) {
11 | return g.url === item.gist;
12 | });
13 | // $scope.selected.gist = gists.find(function (g) {
14 | // return g.url === item.gist;
15 | // }) || { name: gist.description || gist.fileName, url: gist.html_url };
16 | }
17 |
18 | function getGistId(url) {
19 | var regexLastSlash = new RegExp('(.*)/$', 'g');
20 | var result = regexLastSlash.exec(url);
21 | var newUrl = result ? result[1] : url;
22 | var regexGistId = new RegExp('([a-zA-Z0-9_-]*)$', 'g');
23 |
24 | return regexGistId.exec(newUrl)[1];
25 | }
26 |
27 | $scope.clear = function ($event) {
28 | $event.stopPropagation();
29 | $scope.selected.gist = undefined;
30 | };
31 |
32 | $scope.gistShareInfo = function () {
33 | $modal.open({
34 | templateUrl: '/modals/templates/info_share_gist.html',
35 | controller: 'InfoCtrl',
36 | windowClass: 'howto'
37 | });
38 | };
39 |
40 | $scope.whitelistInfo = function () {
41 | $modal.open({
42 | templateUrl: '/modals/templates/whitelist_info.html',
43 | controller: 'InfoCtrl',
44 | windowClass: 'howto'
45 | });
46 | };
47 |
48 | $scope.ok = function (itemToSave) {
49 | $scope.selectedGistId = getGistId($scope.selected.gist.url);
50 | $scope.itemsGistId = getGistId(itemToSave.gist);
51 |
52 | if ($scope.selectedGistId !== $scope.itemsGistId) {
53 | itemToSave.gist = $scope.selected.gist;
54 | } else if (!itemToSave.gist.url) {
55 | itemToSave.gist = { url: itemToSave.gist };
56 | }
57 | linkItemService.updateLink(itemToSave).then(function success(data) {
58 | if (data.value) {
59 | $modalInstance.close(data.value);
60 | }
61 | }, function failure(err) {
62 | $scope.errorMsg.push(err);
63 | });
64 | };
65 |
66 | $scope.cancel = function () {
67 | $modalInstance.dismiss('cancel');
68 | };
69 |
70 | initGist();
71 |
72 | });
73 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: CI/CDPipeline
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | tags:
8 | - '*'
9 | pull_request:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | matrix:
18 | node_version:
19 | - 12
20 | os:
21 | - ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v2
25 | - name: setup gcloud CLI
26 | uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
27 | with:
28 | version: '275.0.0'
29 | service_account_key: ${{ secrets.GCP_base64 }}
30 | # Configure docker to use the gcloud command-line tool as a credential helper
31 | - run: gcloud auth configure-docker
32 | - name: Use Node version 12.6
33 | uses: actions/setup-node@v1
34 | with:
35 | version: ${{ matrix.node_version }}
36 | - name: Npm Install
37 | run: |
38 | npm install
39 |
40 | - name: grunt build and test
41 | run: |
42 | grunt build
43 | grunt test
44 | grunt coverage
45 | - name: Test Coverage
46 | uses: coverallsapp/github-action@master
47 | with:
48 | github-token: ${{ secrets.github_token }}
49 | path-to-lcov: ./output/coverage/lcov.info
50 | - name: build the docker image with committ SHA for staging
51 | if: github.ref == 'refs/heads/master'
52 | run: docker build -t eu.gcr.io/sap-cla-assistant/cla-assistant:${GITHUB_SHA} .
53 | - name: build the docker images with tag name and latest for production
54 | if: startsWith(github.ref, 'refs/tags')
55 | run: docker build -t eu.gcr.io/sap-cla-assistant/cla-assistant:${GITHUB_REF:10} -t eu.gcr.io/sap-cla-assistant/cla-assistant:latest -t sapclaassistant/claassistant:latest -t sapclaassistant/claassistant:${GITHUB_REF:10} .
56 | - name: push the latest and release images to dockerhub only for releases
57 | if: startsWith(github.ref, 'refs/tags')
58 | run: |
59 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
60 | docker push sapclaassistant/claassistant
61 | - name: push images to gcp
62 | if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
63 | run: docker push eu.gcr.io/sap-cla-assistant/cla-assistant
64 | - name: deploy to staging cloud run service
65 | if: github.ref == 'refs/heads/master'
66 | run: gcloud --quiet --project sap-cla-assistant beta run deploy cla-assistant-staging --platform managed --region europe-west1 --image eu.gcr.io/sap-cla-assistant/cla-assistant:${GITHUB_SHA}
67 |
--------------------------------------------------------------------------------
/src/client/modals/templates/upload.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Import
5 |
6 |
7 |
8 | Import a CSV file containing all the GitHub usernames of contributors who have already signed your CLA.
9 |
10 |
11 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{$select.selected}}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {{ datum }}
43 |
44 |
45 |
46 |
47 |
50 |
51 |
52 | Cancel
53 | Import
54 |
55 |
--------------------------------------------------------------------------------
/src/tests/acceptance_tests/githubUtils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | login: function (I, name, password) {
3 | I.amOnPage('https://github.com/login')
4 | I.fillField('login', name)
5 | I.fillField('password', password)
6 | I.click('commit')
7 | I.seeInCurrentUrl('https://github.com')
8 | I.wait(1)
9 | },
10 |
11 | createRepo: function (I, owner, repo) {
12 | I.amOnPage('https://github.com/new')
13 | I.fillField('repository[name]', repo)
14 | I.checkOption('repository[auto_init]')
15 | I.scrollPageToBottom()
16 | I.waitForEnabled('//*[@id="new_repository"]/div[3]/button', 5)
17 | I.click('Create repository')
18 | I.wait(1)
19 | I.seeInCurrentUrl(`https://github.com/${owner}/${repo}`)
20 | },
21 |
22 | deleteRepo: function (I, owner, repo) {
23 | I.amOnPage(`https://github.com/${owner}/${repo}`)
24 | I.seeInCurrentUrl(`/${owner}/${repo}`)
25 | I.amOnPage(`https://github.com/${owner}/${repo}/settings`)
26 | I.waitForEnabled('//summary[contains(., "Delete this repository")]', 5)
27 | I.click('//summary[contains(., "Delete this repository")]')
28 | I.waitForVisible('//input[contains(@aria-label, "delete this repository")]', 5)
29 | I.fillField('//input[contains(@aria-label, "delete this repository")]', repo)
30 | I.click('I understand the consequences, delete this repository')
31 | I.seeInCurrentUrl('https://github.com')
32 | },
33 |
34 | revokePermissions: function (I) {
35 | I.amOnPage('https://github.com/settings/applications')
36 | I.click('Revoke')
37 | I.waitForEnabled('//*[@id="facebox"]/div/div/div/form/button', 5)
38 | I.click('//*[@id="facebox"]/div/div/div/form/button')
39 | I.wait(1)
40 | I.refreshPage()
41 | I.see('No authorized applications')
42 | },
43 |
44 | createPR: function (I, owner, repo) {
45 | I.amOnPage(`https://github.com/${owner}/${repo}/blob/master/README.md`)
46 | I.waitForEnabled('//button[starts-with(@aria-label, "Edit the file") or starts-with(@aria-label, "Fork this project")]', 5)
47 | I.click('//button[starts-with(@aria-label, "Edit the file") or starts-with(@aria-label, "Fork this project")]') //click ~Fork this project and edit the file
48 |
49 | I.waitForVisible('.CodeMirror-line', 5)
50 | I.wait(1)
51 | I.click('.CodeMirror-line')
52 | I.wait(1)
53 | I.pressKey(['Space', 't', 'e', 's', 't', 'Enter']);
54 | I.waitForEnabled('//button[contains(., "Propose file change")]', 5)
55 | I.click('Propose file change')
56 | I.waitForEnabled('//button[contains(., "Create pull request")]', 5)
57 | I.click('Create pull request')
58 | I.click('//*[@id="new_pull_request"]/div[1]/div/div/div[3]/button')
59 | }
60 | };
--------------------------------------------------------------------------------
/src/client/templates/cla.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Error
6 |
There is no CLA to sign for {{params.user}}/{{params.repo}}
7 |
({{noLinkedItemError}})
8 |
9 |
10 |
31 |
32 | I agree
34 | Sign in with GitHub to agree
35 |
36 |
37 |
38 |
39 |
Thank you {{user.value.login}}
40 |
for signing the CLA!
41 |
42 |
we are now taking you back to GitHub
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/server/middleware/utils.js:
--------------------------------------------------------------------------------
1 | const githubService = require('../services/github')
2 | const log = require('../services/logger')
3 | const Joi = require('joi')
4 |
5 | class Utils {
6 | couldBeAdmin(username) {
7 | return config.server.github.admin_users.length === 0 || config.server.github.admin_users.indexOf(username) >= 0
8 | }
9 |
10 | async checkRepoPushPermissionByName(repo, owner, token) {
11 | try {
12 | const res = await githubService.call({
13 | obj: 'repos',
14 | fun: 'get',
15 | arg: {
16 | repo: repo,
17 | owner: owner
18 | },
19 | token: token
20 | })
21 | if (!res.data) {
22 | throw new Error('No data returned')
23 | }
24 | if (!res.data.permissions || !res.data.permissions.push) {
25 | throw new Error('User has no push permission for this repo')
26 | }
27 | return res.data
28 | } catch (error) {
29 | if (error.status === 404) {
30 | log.info('User has no authorization for ' + repo + ' repository.')
31 | log.warn(error.stack)
32 | }
33 | throw error
34 | }
35 | }
36 |
37 | async checkRepoPushPermissionById(repoId, token) {
38 | const res = await githubService.call({
39 | obj: 'repos',
40 | fun: 'getById',
41 | arg: {
42 | id: repoId
43 | },
44 | token: token
45 | })
46 | return res.data.permissions.push
47 | }
48 |
49 | async checkOrgAdminPermission(org, username, token) {
50 | try {
51 | const res = await githubService.call({
52 | obj: 'orgs',
53 | fun: 'getMembership',
54 | arg: {
55 | org: org,
56 | username: username
57 | },
58 | token: token
59 | })
60 | if (!res.data) {
61 | throw new Error('No data returned')
62 | }
63 | if (res.data.role !== 'admin') {
64 | throw new Error('User is not an admin of this org')
65 | }
66 |
67 | return res.data
68 | } catch (error) {
69 | if (error.status === 404) {
70 | log.info('User has no authorization for ' + org + ' repository.')
71 | }
72 | throw error
73 | }
74 |
75 | }
76 |
77 | validateArgs(args, schema, allowUnknown = false, convert = true) {
78 | const joiRes = Joi.validate(args, schema, { abortEarly: false, allowUnknown, convert })
79 | if (joiRes.error) {
80 | joiRes.error.code = 400
81 | throw joiRes.error
82 | }
83 | }
84 | }
85 |
86 | module.exports = new Utils()
--------------------------------------------------------------------------------
/src/server/middleware/authenticated.js:
--------------------------------------------------------------------------------
1 | let passport = require('passport')
2 | let q = require('q')
3 | let utils = require('./utils')
4 |
5 | function authenticateForExternalApi(req, res, next) {
6 | passport.authenticate('token', { session: false }, async function (err, user) {
7 | if (err) {
8 | return next(err)
9 | }
10 |
11 | if (!user) {
12 | res.status(401).json({ message: 'Incorrect token credentials' })
13 |
14 | return
15 | }
16 | let hasPermission = false
17 | try {
18 | if (req.args.repoId) {
19 | hasPermission = await utils.checkRepoPushPermissionById(req.args.repoId, user.token)
20 | } else if (req.args.org) {
21 | hasPermission = await utils.checkOrgAdminPermission(req.args.org, user.login, user.token)
22 | }
23 | } catch (e) {
24 | const message = e || 'You have no push permission for this org or repo'
25 | return res.status(403).json({ message })
26 | }
27 | // utils.checkRepoPushPermissionById(req.args.repoId, user.token, function (err, hasPermission) {
28 | if (hasPermission) {
29 | req.user = user
30 | next()
31 | } else {
32 | res.status(403).json({ message: err || 'You have no push permission for this org or repo' })
33 | }
34 | // })
35 | })(req, res)
36 | }
37 |
38 | function authenticateForAdminOnlyApi(req, res, next) {
39 | passport.authenticate('token', { session: false }, function (err, user) {
40 | if (err) {
41 | return next(err)
42 | }
43 | if (!user) {
44 | return res.status(401).json({ message: 'Incorrect token credentials' })
45 | }
46 | if (!utils.couldBeAdmin(user.login) || (req.args.org && user.scope.indexOf('admin:org_hook') < 0)) {
47 | return res.status(403).json({ message: 'Must have admin:org_hook permission scope' })
48 | }
49 | let promises = []
50 | if (req.args.owner && req.args.repo) {
51 | promises.push(utils.checkRepoPushPermissionByName(req.args.repo, req.args.owner, user.token))
52 | }
53 | if (req.args.org) {
54 | promises.push(utils.checkOrgAdminPermission(req.args.org, user.login, user.token))
55 | }
56 |
57 | return q.all(promises).then(function () {
58 | req.user = user
59 | next()
60 | }).catch(function (error) {
61 | return res.status(403).json({ message: error.message || error })
62 | })
63 |
64 | })(req, res)
65 | }
66 |
67 | module.exports = function (req, res, next) {
68 | if (config.server.api_access.free.indexOf(req.originalUrl) > -1 || req.isAuthenticated()) {
69 | return next()
70 | } else if (config.server.api_access.external.indexOf(req.originalUrl) > -1) {
71 | return authenticateForExternalApi(req, res, next)
72 | } else if (config.server.api_access.admin_only.indexOf(req.originalUrl) > -1) {
73 | return authenticateForAdminOnlyApi(req, res, next)
74 | }
75 | res.status(401).send('Authentication required')
76 | }
77 |
--------------------------------------------------------------------------------
/src/server/graphQueries/github.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | getPRCommitters: (owner, repo, number, cursor) => {
4 | number = typeof number === 'string' ? parseInt(number) : number
5 | let query = `
6 | query($owner:String! $name:String! $number:Int! $cursor:String!){
7 | repository(owner: $owner, name: $name) {
8 | pullRequest(number: $number) {
9 | commits(first: 100, after: $cursor) {
10 | totalCount
11 | edges {
12 | node {
13 | commit {
14 | author {
15 | email
16 | name
17 | user {
18 | id
19 | databaseId
20 | login
21 | }
22 | }
23 | committer {
24 | name
25 | user {
26 | id
27 | databaseId
28 | login
29 | }
30 | }
31 | }
32 | }
33 | cursor
34 | }
35 | pageInfo {
36 | endCursor
37 | hasNextPage
38 | }
39 | }
40 | }
41 | }
42 | }`.replace(/ /g, '')
43 | let variables = {
44 | owner: owner,
45 | name: repo,
46 | number: number,
47 | cursor: cursor
48 | }
49 |
50 | return JSON.stringify({ query, variables })
51 | },
52 |
53 | getUserOrgs: (owner, cursor) => {
54 | let query = `query ($owner: String! ${cursor ? '$cursor: String!)' : ')'} {`
55 | query = query + `
56 | user(login: $owner) {
57 | organizations(first: 100${cursor ? ', after: $cursor' : ''}) {
58 | edges {
59 | cursor
60 | node {
61 | login
62 | name
63 | id
64 | databaseId
65 | avatarUrl
66 | viewerCanAdminister
67 | }
68 | }
69 | pageInfo {
70 | endCursor
71 | hasNextPage
72 | }
73 | }
74 | }
75 | }`
76 | let variables = { owner }
77 | if (cursor) {
78 | variables.cursor = cursor
79 | }
80 |
81 | return JSON.stringify({ query, variables }).replace(/ /g, '')
82 | }
83 | }
--------------------------------------------------------------------------------
/src/client/assets/styles/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal.fade .modal-dialog {
2 | -webkit-transform: scale(0.5);
3 | -moz-transform: scale(0.5);
4 | -ms-transform: scale(0.5);
5 | transform: scale(0.5);
6 | opacity: 0;
7 | -webkit-transition: all 0.3s linear;
8 | -moz-transition: all 0.3s linear;
9 | transition: all 0.3s linear;
10 | }
11 |
12 | .modal.fade.in .modal-dialog {
13 | -webkit-transform: scale(1);
14 | -moz-transform: scale(1);
15 | -ms-transform: scale(1);
16 | transform: scale(1);
17 | opacity: 1;
18 | -webkit-transition: all 0.3s linear;
19 | -moz-transition: all 0.3s linear;
20 | transition: all 0.3s linear;
21 | }
22 |
23 | .modal-header {
24 | padding: 15px 35px;
25 | }
26 |
27 | .modal-body {
28 | padding: 15px 15px;
29 | }
30 |
31 | .modal-primary {
32 | background-color: $gray-lighter;
33 | color: $gray-darkest;
34 | }
35 |
36 | .modal-primary-2nd {
37 | background-color: $brand-primary-2nd;
38 | color: $gray-lighter;
39 | }
40 |
41 | .modal-title {
42 | text-align: center;
43 | font-size: 26px;
44 | margin-top: 20px;
45 | }
46 |
47 | .modal-dialog {
48 | .icon {
49 | width: 300px;
50 | border: none;
51 | }
52 |
53 | p {
54 | font-size: 16px;
55 | }
56 |
57 | ul > li {
58 | padding-top: 15px;
59 | }
60 |
61 | button {
62 | margin-left: 10px;
63 | font-size: 16px;
64 | }
65 |
66 | .btn-cancel {
67 | font-weight: 200;
68 | }
69 | }
70 |
71 | .confirm-add .modal-dialog {
72 | width: 550px;
73 |
74 | p {
75 | color: $green;
76 | }
77 | }
78 |
79 | .link-success .modal-dialog {
80 | width: 455px;
81 |
82 | button {
83 | margin-left: 0px;
84 | }
85 | }
86 |
87 | .howto .modal-body .free-space {
88 | padding: 35px 15px 0px 15px;
89 | }
90 |
91 | .howto .modal-body .free-space:last-child {
92 | padding-bottom: 20px;
93 | }
94 |
95 | .howto .modal-body .btn,
96 | .validatePr .modal-body .btn {
97 | margin: 20px 10px 0px 0px;
98 | }
99 |
100 | .validatePr .modal-body .btn-info {
101 | height: 45px;
102 | width: 100%;
103 | }
104 |
105 | .edit-linked-item .modal-body.container {
106 | width: initial;
107 | }
108 |
109 | .edit-linked-item .modal-body {
110 | .side-note {
111 | font-size: 12px;
112 | }
113 |
114 | .center-text {
115 | margin-bottom: 10px;
116 | text-align: center;
117 | }
118 |
119 | .wildcard {
120 | vertical-align: text-top;
121 | font-size: medium;
122 | }
123 | }
124 |
125 | .report .modal-body,
126 | .get-badge .modal-body,
127 | .upload .modal-body,
128 | .edit-linked-item .modal-body,
129 | .validatePr .modal-body {
130 | padding: 15px 35px;
131 |
132 | .close-button {
133 | margin-right: -15px;
134 | }
135 |
136 | .export-button {
137 | margin: 30px 0px;
138 | }
139 |
140 | .form-group {
141 | margin-bottom: 10px;
142 | padding-left: 0px;
143 | }
144 | }
145 |
146 | .report .modal-body,
147 | .get-badge .modal-body {
148 | button {
149 | margin: 0;
150 | }
151 |
152 | .selectize-input {
153 | height: 100%;
154 | }
155 | }
156 |
157 | .validatePr .title {
158 | text-align: center;
159 | font-size: 26px;
160 | margin: 20px;
161 | }
162 |
--------------------------------------------------------------------------------
/custom-fields-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "JSON Schema for CLA assistant custom fields",
3 | "$schema": "http://json-schema.org/draft-04/schema#",
4 | "type": "object",
5 | "patternProperties": {
6 | "^[A-Za-z0-9]*$": {
7 | "type": "object",
8 | "description": "Each property describes additional data which should be collected from a CLA signer and generates a form field",
9 | "properties": {
10 | "title": {
11 | "type": "string",
12 | "description": "Title is used to create a label for the form field"
13 | },
14 | "type": {
15 | "oneOf": [{
16 | "enum": ["hidden", "string", "textarea", "number", "boolean"],
17 | "description": "Field of type boolean generates a checkbox"
18 | },
19 | {
20 | "type": "object",
21 | "description": "Field of type enum generates radio buttons",
22 | "properties": {
23 | "enum": {
24 | "type": "array",
25 | "minItems": 2,
26 | "items": {
27 | "type": "string"
28 | },
29 | "uniqueItems": true
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | "required": {
36 | "type": "boolean",
37 | "description": "Required fields are marked with asterisk and must be filled by signer"
38 | },
39 | "description": {
40 | "type": "string",
41 | "description": "Field description generates small text behind the label"
42 | },
43 | "maximum": {
44 | "type": "number",
45 | "description": "Can be used for fields of type number"
46 | },
47 | "minimum": {
48 | "type": "number",
49 | "description": "Can be used for fields of type number"
50 | },
51 | "githubKey": {
52 | "type": "string",
53 | "description": "Data of github user profile can be used to prefill the form. This refers to the response of the github api https://developer.github.com/v3/users/#get-a-single-user"
54 | }
55 | },
56 | "anyOf": [{
57 | "properties": {
58 | "type": {
59 | "enum": ["hidden"]
60 | },
61 | "required": {
62 | "enum": [false]
63 | }
64 | }
65 | },
66 | {
67 | "properties": {
68 | "type": {
69 | "enum": ["string", "textarea", "number", "boolean"]
70 | }
71 | }
72 | },
73 | {
74 | "properties": {
75 | "type": {
76 | "type": "object",
77 | "properties": {
78 | "enum": {
79 | "type": "array"
80 | }
81 | }
82 |
83 | }
84 | }
85 | }
86 | ],
87 | "additionalProperties": false
88 | }
89 | },
90 | "dependencies": {
91 | "type": {
92 | "properties": {
93 | "required": {
94 | "enum": [false]
95 | }
96 | }
97 | }
98 | },
99 | "additionalProperties": false
100 | }
--------------------------------------------------------------------------------
/src/server/controller/default.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const path = require('path')
3 | const cla = require('./../api/cla')
4 | const logger = require('./../services/logger')
5 | //////////////////////////////////////////////////////////////////////////////////////////////
6 | // Default router
7 | //////////////////////////////////////////////////////////////////////////////////////////////
8 |
9 | const router = express.Router()
10 |
11 | // router.use('/accept', function(req, res) {
12 | router.use('/accept/:owner/:repo', async (req, res) => {
13 | res.set({ 'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' })
14 |
15 | req.args = {
16 | owner: req.params.owner,
17 | repo: req.params.repo
18 | }
19 |
20 | if (req.isAuthenticated()) {
21 | try {
22 | await cla.sign(req)
23 | } catch (e) {
24 | if (e && (!e.code || e.code != 200)) {
25 | logger.error(e)
26 |
27 | return
28 | }
29 | }
30 |
31 | let redirectUrl = `/${req.args.owner}/${req.args.repo}?redirect=true`
32 | redirectUrl = req.query.pullRequest ? `${redirectUrl}&pullRequest=${req.query.pullRequest}` : redirectUrl
33 | res.redirect(redirectUrl)
34 | } else {
35 | req.session.next = req.originalUrl
36 | return res.redirect('/auth/github?public=true')
37 | }
38 | })
39 |
40 | router.use('/signin/:owner/:repo', (req, res) => {
41 | let redirectUrl = `/${req.params.owner}/${req.params.repo}`
42 | req.session.next = req.query.pullRequest ? `${redirectUrl}?pullRequest=${req.query.pullRequest}` : redirectUrl
43 |
44 | return res.redirect('/auth/github?public=true')
45 | })
46 |
47 | router.all('/static/*', (req, res) => {
48 | let filePath
49 | if (req.user && req.path === '/static/cla-assistant.json') {
50 | filePath = path.join(__dirname, '..', '..', '..', 'cla-assistant.json')
51 | } else {
52 | filePath = config.server.templates.login
53 | }
54 | res.setHeader('Last-Modified', (new Date()).toUTCString())
55 | res.status(200).sendFile(filePath)
56 | })
57 |
58 | router.get('/check/:owner/:repo', (req, res) => {
59 | let referer = req.header('Referer')
60 | let back = referer && referer.includes('github.com') ? referer : 'https://github.com'
61 | logger.info('Recheck PR requested for ', `https://github.com/${req.params.owner}/${req.params.repo}/pull/${req.query.pullRequest}`, `referer was ${referer}`)
62 | cla.validatePullRequest({
63 | owner: req.params.owner,
64 | repo: req.params.repo,
65 | number: req.query.pullRequest
66 | })
67 | res.redirect(back)
68 | })
69 |
70 | router.all('/*', (req, res) => {
71 | let filePath
72 | if (req.path === '/robots.txt') {
73 | filePath = path.join(__dirname, '..', '..', 'client', 'assets', 'robots.txt')
74 | }
75 | else if ((req.user && req.user.scope && req.user.scope.indexOf('write:repo_hook') > -1) || req.path !== '/') {
76 | filePath = path.join(__dirname, '..', '..', 'client', 'home.html')
77 | } else {
78 | filePath = config.server.templates.login
79 | }
80 | res.setHeader('Last-Modified', (new Date()).toUTCString())
81 | res.status(200).sendFile(filePath)
82 | })
83 |
84 | module.exports = router
--------------------------------------------------------------------------------
/src/server/services/url.js:
--------------------------------------------------------------------------------
1 | const url = require('url')
2 |
3 | module.exports = function () {
4 | let baseUrl = url.format({
5 | protocol: config.server.http.protocol,
6 | hostname: config.server.http.host,
7 | port: config.server.http.port
8 | });
9 |
10 | let githubBase = url.format({
11 | protocol: config.server.github.protocol,
12 | host: config.server.github.host
13 | });
14 |
15 | let githubApiBase = url.format({
16 | protocol: config.server.github.protocol,
17 | host: config.server.github.api
18 | });
19 |
20 | return {
21 | // socket: localSocket,
22 | baseUrl: baseUrl,
23 | baseWebhook: url.resolve(baseUrl, '/github/webhook/'),
24 | claURL: (user, repo, number) => {
25 | let claUrl = url.resolve(baseUrl, '/' + user + '/' + repo)
26 | claUrl = number ? claUrl + '?pullRequest=' + number : claUrl
27 |
28 | return claUrl
29 | },
30 | githubBase: githubBase,
31 | githubApiBase: githubApiBase,
32 | githubCallback: url.resolve(baseUrl, '/auth/github/callback'),
33 | githubAuthorization: url.resolve(githubBase, '/login/oauth/authorize'),
34 | githubToken: url.resolve(githubBase, '/login/oauth/access_token'),
35 | githubProfile: () => url.resolve(githubApiBase, config.server.github.enterprise ? '/api/v3/user' : '/user'),
36 | githubCommits: (owner, repo, sha, since) => {
37 | let _url = url.resolve(githubApiBase, `/repos/${owner}/${repo}/commits`)
38 | _url = sha ? `${_url}?sha=${sha}` : _url
39 | _url += sha && since ? '&' : since ? '?' : ''
40 | _url = since ? `${_url}since=${since}` : _url
41 |
42 | return _url
43 | },
44 | githubFileReference: (user, repo, fileReference) => url.resolve(githubBase, `/${user}/${repo}/blob/${fileReference}`),
45 | githubOrgWebhook: (org) => url.resolve(githubApiBase, `/orgs/${org}/hooks`),
46 | githubPullRequests: (owner, repo, state) => {
47 | let _url = `${module.exports.githubRepository(owner, repo)}/pulls`
48 | _url = state ? `${_url}?state=${state}` : _url
49 |
50 | return _url
51 | },
52 | githubPullRequest: (owner, repo, number) => url.resolve(githubApiBase, '/repos/' + owner + '/' + repo + '/pulls/' + number),
53 | githubPullRequestCommits: (owner, repo, number) => {
54 | return module.exports.githubPullRequest(owner, repo, number) + '/commits'
55 | },
56 | githubPullRequestComments: (owner, repo, number) => url.resolve(githubApiBase, `/repos/${owner}/${repo}/issues/${number}/comments`),
57 | githubRepository: (owner, repo) => {
58 | let _url = url.resolve(githubApiBase, `/repos/${owner}/${repo}`)
59 |
60 | return _url
61 | },
62 | pullRequestBadge: (signed) => {
63 | let signed_str = signed ? 'signed' : 'not_signed'
64 | return url.resolve(baseUrl, `/pull/badge/${signed_str}`)
65 | },
66 | recheckPrUrl: (owner, repo, number) => {
67 | let checkUrl = url.resolve(baseUrl, `/check/${owner}/${repo}`)
68 | checkUrl = number ? `${checkUrl}?pullRequest=${number}` : checkUrl
69 |
70 | return checkUrl
71 | },
72 | webhook: (repo) => url.resolve(baseUrl, `/github/webhook/${repo}`)
73 | }
74 | }()
--------------------------------------------------------------------------------
/src/client/app.js:
--------------------------------------------------------------------------------
1 | var module = angular.module('app', ['cla.filters',
2 | 'ui.utils',
3 | 'ui.router',
4 | 'ui.bootstrap',
5 | 'ui.bootstrap.popover',
6 | 'ui.select',
7 | 'ngSanitize',
8 | 'ngAnimate',
9 | 'ngCsv'
10 | ]);
11 | // eslint-disable-next-line no-unused-vars
12 | var filters = angular.module('cla.filters', []);
13 |
14 | // *************************************************************
15 | // Delay start
16 | // *************************************************************
17 |
18 | angular.element(document).ready(function () {
19 | angular.bootstrap(document, ['app']);
20 | angular.element(document.querySelectorAll('.needs-javascript')).removeClass('needs-javascript');
21 | });
22 |
23 | // *************************************************************
24 | // States
25 | // *************************************************************
26 |
27 | module.config(['$stateProvider', '$urlRouterProvider', '$locationProvider',
28 | function ($stateProvider, $urlRouterProvider, $locationProvider) {
29 |
30 | $stateProvider
31 | //
32 | // Home state
33 | //
34 | .state('home', {
35 | url: '/',
36 | templateUrl: '/templates/home.html',
37 | controller: 'HomeCtrl'
38 | })
39 |
40 | //
41 | // Settings view
42 | //
43 | .state('home.settings', {
44 | // url: '/detail/:user/:repo',
45 | templateUrl: '/templates/settings.html',
46 | controller: 'SettingsCtrl',
47 | params: {
48 | 'user': {},
49 | 'owner': {},
50 | 'repo': {},
51 | 'gist': {}
52 | }
53 | // params: ['user', 'owner', 'repo', 'gist'] <-- was in older angular version
54 | })
55 |
56 | //
57 | // My-CLA state
58 | //
59 | .state('cla', {
60 | abstract: true,
61 | url: '/my-cla',
62 | template: ''
63 | })
64 |
65 |
66 | .state('cla.myCla', {
67 | url: '',
68 | templateUrl: '/templates/my-cla.html',
69 | controller: 'MyClaCtrl'
70 | })
71 |
72 | //
73 | // Repo state (abstract)
74 | //
75 | .state('repo', {
76 | abstract: true,
77 | url: '/:user/:repo?pullRequest&redirect',
78 | template: ''
79 | })
80 |
81 | //
82 | // Repo cla
83 | //
84 | .state('repo.cla', {
85 | url: '',
86 | templateUrl: '/templates/cla.html',
87 | controller: 'ClaController'
88 | })
89 |
90 | //
91 | // 404 Error
92 | //
93 | .state('404', {
94 | url: '/404',
95 | templateUrl: '/templates/404.html'
96 | });
97 |
98 | $urlRouterProvider.otherwise('/404');
99 |
100 | $locationProvider.html5Mode({
101 | enabled: true,
102 | requireBase: false
103 | });
104 | }
105 | ]);
106 |
--------------------------------------------------------------------------------
/src/server/api/repo.js:
--------------------------------------------------------------------------------
1 | // module
2 | const repo = require('../services/repo')
3 | const logger = require('../services/logger')
4 | const Joi = require('joi')
5 | const webhook = require('./webhook')
6 | const utils = require('../middleware/utils')
7 |
8 | const REPOCREATESCHEMA = Joi.object().keys({
9 | owner: Joi.string().required(),
10 | repo: Joi.string().required(),
11 | repoId: Joi.number().required(),
12 | token: Joi.string().required(),
13 | gist: Joi.alternatives().try(Joi.string().uri(), Joi.any().allow([null])), // Null CLA
14 | sharedGist: Joi.boolean(),
15 | minFileChanges: Joi.number(),
16 | minCodeChanges: Joi.number(),
17 | whiteListPattern: Joi.string().allow(''),
18 | privacyPolicy: Joi.string().allow('')
19 | })
20 |
21 | const REPOREMOVESCHEMA = Joi.alternatives().try(Joi.object().keys({
22 | owner: Joi.string().required(),
23 | repo: Joi.string().required(),
24 | }), Joi.object().keys({
25 | repoId: Joi.number().required()
26 | }))
27 |
28 | module.exports = {
29 | check: (req) => repo.check(req.args),
30 | create: async (req) => {
31 | req.args.token = req.args.token || req.user.token
32 | utils.validateArgs(req.args, REPOCREATESCHEMA, true)
33 |
34 | const repoArgs = {
35 | repo: req.args.repo,
36 | owner: req.args.owner,
37 | token: req.args.token
38 | }
39 | let dbRepo
40 | try {
41 | dbRepo = await repo.get(repoArgs)
42 | if (!dbRepo) {
43 | throw 'New repo should be created'
44 | }
45 | try {
46 | const ghRepo = await repo.getGHRepo(repoArgs)
47 | if (ghRepo && ghRepo.id != dbRepo.repoId) {
48 | throw 'Repo id has changed'
49 | }
50 | } catch (error) {
51 | return repo.update(req.args)
52 | }
53 | } catch (error) {
54 | dbRepo = await repo.create(req.args)
55 |
56 | if (dbRepo.gist) {
57 | try {
58 | webhook.create(req)
59 | } catch (error) {
60 | logger.error(`Could not create a webhook for the new repo ${new Error(error)}`)
61 | }
62 | }
63 | return dbRepo
64 | }
65 | throw 'This repository is already linked.'
66 | },
67 | // get: function(req, done){
68 | // repo.get(req.args, function(err, found_repo){
69 | // if (!found_repo || err || found_repo.owner !== req.user.login) {
70 | // log.warn(err)
71 | // done(err)
72 | // return
73 | // }
74 | // done(err, found_repo)
75 | // })
76 | // },
77 | getAll: (req) => repo.getAll(req.args),
78 | update: (req) => {
79 | req.args.token = req.args.token || req.user.token
80 | utils.validateArgs(req.args, REPOCREATESCHEMA)
81 | return repo.update(req.args)
82 | },
83 | remove: async (req) => {
84 | utils.validateArgs(req.args, REPOREMOVESCHEMA)
85 |
86 | const dbRepo = await repo.remove(req.args)
87 | if (dbRepo && dbRepo.gist) {
88 | req.args.owner = dbRepo.owner
89 | req.args.repo = dbRepo.repo
90 | try {
91 | webhook.remove(req)
92 | } catch (error) {
93 | logger.error(`Could not remove the webhook for the repo ${new Error(error)}`)
94 | }
95 | }
96 | return dbRepo
97 | }
98 | }
--------------------------------------------------------------------------------
/src/tests/server/services/org.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, beforeEach, afterEach*/
2 |
3 | // unit test
4 | const assert = require('assert')
5 | const sinon = require('sinon')
6 |
7 | //model
8 | const Org = require('../../../server/documents/org').Org
9 |
10 | // service under test
11 | const org = require('../../../server/services/org')
12 |
13 | // test data
14 | const testData = require('../testData').data
15 |
16 | describe('org:create', async function () {
17 | afterEach(function () {
18 | Org.create.restore()
19 | })
20 |
21 | it('should create org entry ', async () => {
22 | sinon.stub(Org, 'create').callsFake(async (args) => {
23 | assert(args.orgId)
24 | assert(args.org)
25 | assert(args.gist)
26 | return { org: args.org }
27 | })
28 |
29 | const arg = {
30 | org: testData.orgs[0].login,
31 | orgId: testData.orgs[0].id,
32 | gist: 'url/gistId',
33 | token: 'abc'
34 | }
35 | await org.create(arg)
36 | })
37 |
38 | })
39 | describe('org:update', () => {
40 | afterEach(() => Org.findOne.restore())
41 |
42 | it('should create org entry ', async () => {
43 | sinon.stub(Org, 'findOne').callsFake(async (args) => {
44 | assert(args.orgId)
45 | const org_entry = {
46 | org: args.org,
47 | save: () => { /*do nothing*/ }
48 | }
49 | sinon.stub(org_entry, 'save').callsFake(async () => {
50 | assert.equal(org_entry.token, 'abc')
51 | })
52 | return org_entry
53 | })
54 |
55 | const arg = {
56 | org: testData.orgs[0].login,
57 | orgId: testData.orgs[0].id,
58 | gist: 'url/gistId',
59 | token: 'abc'
60 | }
61 | await org.update(arg)
62 | })
63 | })
64 | describe('org:get', () => {
65 | afterEach(() => Org.findOne.restore())
66 |
67 | it('should find org entry ', async () => {
68 | sinon.stub(Org, 'findOne').callsFake(async (args) => {
69 | assert(args.orgId)
70 | return { org: args.org }
71 | })
72 |
73 | let args = {
74 | orgId: testData.orgs[0].id,
75 | org: testData.orgs[0].login,
76 | }
77 |
78 | const org_entry = await org.get(args)
79 | assert(org_entry)
80 | })
81 | })
82 | describe('org:getMultiple', () => {
83 | it('should find multiple entries', async () => {
84 | sinon.stub(Org, 'find').callsFake(async (args) => {
85 | assert(args.orgId.$in.length > 0)
86 | // assert(args.orgId.$in.length > 0)
87 | return [{}, {}]
88 | })
89 |
90 | let args = {
91 | orgId: [1, 2]
92 | }
93 |
94 | const res = await org.getMultiple(args)
95 | assert.equal(res.length, 2)
96 | Org.find.restore()
97 | })
98 | })
99 | describe('org:remove', () => {
100 | beforeEach(() => {
101 | sinon.stub(Org, 'findOneAndRemove').callsFake(async (args) => {
102 | assert(args.orgId);
103 | return {}
104 | })
105 | })
106 |
107 | afterEach(() => Org.findOneAndRemove.restore())
108 |
109 | it('should find org entry ', async () => {
110 | let args = {
111 | orgId: testData.orgs[0].id,
112 | org: testData.orgs[0].login,
113 | }
114 | const org_entry = await org.remove(args)
115 | assert(org_entry)
116 | })
117 | })
118 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | let appJsFiles = [
3 | 'src/client/app.js',
4 | 'src/client/api.js',
5 | 'src/client/controller/**/*.js',
6 | 'src/client/modals/**/*.js',
7 | 'src/client/services/**/*.js'
8 | ];
9 |
10 | let config = {
11 |
12 | pkg: grunt.file.readJSON('package.json'),
13 |
14 | coveralls: {
15 | target: {
16 | src: 'output/coverage/lcov.info'
17 | }
18 | },
19 |
20 | mocha_istanbul: {
21 | coverage: {
22 | src: 'src/tests/server',
23 | options: {
24 | coverage: true,
25 | mask: '**/*.js',
26 | coverageFolder: 'output/coverage'
27 | }
28 | }
29 | },
30 |
31 | // server tests
32 | mochaTest: {
33 | server: {
34 | options: {
35 | timeout: 1000
36 | },
37 | src: ['src/tests/server/**/*.js']
38 | },
39 | debugServer: {
40 | options: {
41 | timeout: 400000
42 | },
43 | src: ['src/tests/server/**/*.js']
44 | }
45 | },
46 |
47 | // client tests
48 | karma: {
49 | unit: {
50 | configFile: 'src/tests/karma.config.js'
51 | }
52 | },
53 |
54 | eslint: {
55 | options: {
56 | configFile: './.eslintrc.js'
57 | },
58 | target: ['*.js', 'src']
59 | },
60 |
61 | scsslint: {
62 | allFiles: [
63 | 'src/client/assets/styles/*.scss'
64 | ],
65 | options: {
66 | config: '.scss-lint.yml',
67 | colorizeOutput: true
68 | }
69 | },
70 |
71 | watch: {
72 | uglify: {
73 | tasks: ['uglify'],
74 | files: appJsFiles
75 | },
76 | eslint: {
77 | tasks: ['eslint'],
78 | files: ['src/**/*.js', '!src/client/app.min.js']
79 | },
80 | mocha: {
81 | tasks: ['mochaTest:server'],
82 | files: ['src/server/**/*.js', 'src/tests/server/**/*.js', '!src/client/app.min.js']
83 | },
84 | karma: {
85 | tasks: ['karma'],
86 | files: ['src/client/**/*.js', 'src/tests/client/**/*.js', '!src/client/app.min.js']
87 | }
88 | },
89 |
90 | uglify: {
91 | options: {
92 | sourceMap: true,
93 | sourceMapIncludeSources: true,
94 | mangle: false
95 | },
96 | target: {
97 | files: {
98 | 'src/client/app.min.js': appJsFiles
99 | }
100 | }
101 | }
102 | };
103 |
104 | // Initialize configuration
105 | grunt.initConfig(config);
106 |
107 | require('load-grunt-tasks')(grunt);
108 |
109 | grunt.registerTask('build', ['uglify']);
110 | grunt.registerTask('lint', ['eslint', 'scsslint']);
111 | grunt.registerTask('coverage', ['mocha_istanbul']);
112 | grunt.registerTask('default', ['uglify', 'eslint', 'mochaTest:server', 'karma', 'watch']);
113 | grunt.registerTask('test', ['eslint', 'mochaTest:server', 'karma']);
114 | grunt.registerTask('debug_test', ['mochaTest:debugServer']);
115 | };
116 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cla-assistant",
3 | "author": "https://github.com/cla-assistant",
4 | "version": "1.8.3",
5 | "homepage": "https://cla-assistant.io",
6 | "description": "Contributor license agreement",
7 | "keywords": [
8 | "license",
9 | "contributor",
10 | "agreement",
11 | "github",
12 | "cla"
13 | ],
14 | "license": "Apache-2.0",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/cla-assistant/cla-assistant"
18 | },
19 | "bugs": "https://github.com/cla-assistant/cla-assistant/issues",
20 | "contributors": [],
21 | "dependencies": {
22 | "@octokit/plugin-retry": "^2.2.0",
23 | "@octokit/plugin-throttling": "^2.4.0",
24 | "@octokit/rest": "^16.37.0",
25 | "angular": "^1.7.9",
26 | "array-sugar": "^1.2.2",
27 | "async": "^2.6.2",
28 | "base-64": "^0.1.0",
29 | "body-parser": "^1.18.3",
30 | "bower": "^1.8.8",
31 | "btoa": "^1.2.1",
32 | "bunyan": "^1.8.12",
33 | "bunyan-sentry-stream": "^1.2.1",
34 | "bunyan-slack": "0.0.10",
35 | "codeceptjs": "^2.0.7",
36 | "colors": "^1.3.3",
37 | "connect-mongo": "^2.0.3",
38 | "cookie-parser": "^1.4.4",
39 | "dont-sniff-mimetype": "^1.0.0",
40 | "ejs": "^2.6.1",
41 | "express": "^4.16.4",
42 | "express-session": "^1.15.6",
43 | "glob": "^7.1.3",
44 | "joi": "^14.3.1",
45 | "json-stable-stringify": "^1.0.1",
46 | "lodash": "^4.17.13",
47 | "memory-cache": "^0.2.0",
48 | "merge": ">=1.2.1",
49 | "mongoose": "^5.4.19",
50 | "node-fetch": "^2.6.0",
51 | "node-sass-middleware": "^0.11.0",
52 | "passport": "^0.4.0",
53 | "passport-accesstoken": "^0.1.0",
54 | "passport-github": "^1.1.0",
55 | "q": "^1.5.1",
56 | "raven": "^2.6.4",
57 | "request": "^2.88.0",
58 | "request-promise-native": "^1.0.7",
59 | "socket.io": "^2.2.0",
60 | "valid-url": "^1.0.9",
61 | "x-frame-options": "^1.0.0"
62 | },
63 | "devDependencies": {
64 | "eslint": "^5.15.1",
65 | "grunt": "^1.0.4",
66 | "grunt-cli": "^1.3.2",
67 | "grunt-contrib-jshint": "^2.0.0",
68 | "grunt-contrib-uglify": "^4.0.0",
69 | "grunt-contrib-watch": "^1.1.0",
70 | "grunt-coveralls": "^2.0.0",
71 | "grunt-eslint": "^21.0.0",
72 | "grunt-http": "^2.3.1",
73 | "grunt-karma": "^3.0.1",
74 | "grunt-mocha-istanbul": "^5.0.2",
75 | "grunt-mocha-test": "^0.13.3",
76 | "grunt-scss-lint": "^0.5.0",
77 | "istanbul": "^0.4.5",
78 | "karma": "^4.0.1",
79 | "karma-chrome-launcher": "^2.2.0",
80 | "karma-mocha": "^1.3.0",
81 | "karma-ng-html2js-preprocessor": "~1.0.0",
82 | "karma-phantomjs-launcher": "1.0.4",
83 | "load-grunt-tasks": "^4.0.0",
84 | "mkdirp": "0.5.1",
85 | "mocha": "^6.1.4",
86 | "phantomjs-prebuilt": "^2.1.16",
87 | "rewire": "^4.0.1",
88 | "should": "^13.2.3",
89 | "sinon": "^7.2.7",
90 | "supertest": "4.0.0",
91 | "webdriverio": "^5.7.2"
92 | },
93 | "engines": {
94 | "node": "12"
95 | },
96 | "scripts": {
97 | "build": "node_modules/grunt/bin/grunt build",
98 | "start": "node app.js",
99 | "postinstall": "bower install",
100 | "test": "node_modules/grunt/bin/grunt test",
101 | "acceptance_test": "source .env && npx codeceptjs run"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/tests/karma.config.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 |
3 | module.exports = function (config) {
4 | config.set({
5 |
6 | basePath: '../../',
7 |
8 | frameworks: ['mocha'],
9 |
10 | // list of files / patterns to load in the browser
11 | files: [
12 | // Testing libs
13 | // CDN
14 | 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js',
15 | 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular.min.js',
16 | 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular-animate.min.js',
17 | 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular-route.js',
18 | 'http://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.17/angular-ui-router.min.js',
19 | 'http://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.4/ui-bootstrap-tpls.min.js',
20 | 'http://cdnjs.cloudflare.com/ajax/libs/angular-ui-select/0.19.8/select.min.js',
21 | 'http://cdnjs.cloudflare.com/ajax/libs/angular-ui-utils/0.1.1/angular-ui-utils.min.js',
22 | 'http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular-sanitize.min.js',
23 | 'http://cdnjs.cloudflare.com/ajax/libs/angular-scroll/0.6.5/angular-scroll.min.js',
24 | 'http://cdnjs.cloudflare.com/ajax/libs/ng-csv/0.3.3/ng-csv.min.js',
25 |
26 | 'http://cdnjs.cloudflare.com/ajax/libs/sinon.js/1.7.3/sinon-min.js',
27 |
28 | 'src/bower/should/should.js',
29 |
30 | // Bower
31 | 'src/bower/bootstrap-sass-official/assets/javascripts/bootstrap.js',
32 | 'src/bower/angular-mocks/angular-mocks.js',
33 |
34 | // Client code
35 | 'src/client/app.js',
36 | 'src/client/api.js',
37 | 'src/client/services/**/*.js',
38 | 'src/client/controller/**/*.js',
39 |
40 | // Client templates
41 | 'src/client/**/*.html',
42 |
43 | // Tests
44 | 'src/tests/client/**/*.js'
45 | ],
46 |
47 |
48 | // list of files to exclude
49 | exclude: [
50 |
51 | ],
52 |
53 |
54 | // preprocess matching files before serving them to the browser
55 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
56 | preprocessors: {
57 | 'src/client/**/*.html': ['ng-html2js']
58 | },
59 |
60 | ngHtml2JsPreprocessor: {
61 | stripPrefix: 'src/client',
62 | moduleName: 'templates'
63 | },
64 |
65 |
66 | // test results reporter to use
67 | // possible values: 'dots', 'progress'
68 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
69 | reporters: ['dots'],
70 |
71 |
72 | // web server port
73 | port: 9876,
74 |
75 | // to avoid DISCONNECTED messages
76 | browserDisconnectTimeout: 10000, // default 2000
77 | browserDisconnectTolerance: 1, // default 0
78 | browserNoActivityTimeout: 60000, //default 10000
79 |
80 | // enable / disable colors in the output (reporters and logs)
81 | colors: true,
82 |
83 |
84 | // level of logging
85 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
86 | logLevel: config.LOG_INFO,
87 |
88 |
89 | // enable / disable watching file and executing tests whenever any file changes
90 | autoWatch: true,
91 |
92 |
93 | // start these browsers
94 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
95 | browsers: ['PhantomJS'],
96 | // browsers: ['Chrome'],
97 | plugins: [
98 | 'karma-ng-html2js-preprocessor',
99 | 'karma-mocha',
100 | // 'karma-chrome-launcher'
101 | 'karma-phantomjs-launcher',
102 | ],
103 |
104 |
105 | // Continuous Integration mode
106 | // if true, Karma captures browsers, runs the tests and exits
107 | singleRun: true
108 | });
109 | };
110 |
--------------------------------------------------------------------------------
/src/client/assets/images/feature1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
19 |
21 |
23 |
25 |
27 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/tests/server/services/github.js:
--------------------------------------------------------------------------------
1 | /*global describe, it, beforeEach*/
2 |
3 | // unit test
4 | const rewire = require('rewire')
5 | const assert = require('assert')
6 | const sinon = require('sinon')
7 |
8 | // config
9 | global.config = require('../../../config')
10 |
11 | // service
12 | const github = rewire('../../../server/services/github')
13 | const cache = require('memory-cache')
14 |
15 | const callStub = sinon.stub()
16 | const authenticateStub = sinon.stub()
17 |
18 | describe('github:call', () => {
19 | let expectedAuth
20 | function OctokitMock(args) {
21 | assert.deepStrictEqual(args.auth, expectedAuth)
22 | assert.strictEqual(args.protocol, 'https')
23 | assert.strictEqual(args.version, '3.0.0')
24 | assert.strictEqual(args.host, 'api.github.com')
25 | assert.strictEqual(args.pathPrefix, null)
26 |
27 | this.obj = {
28 | fun: callStub,
29 | listSomething: {
30 | endpoint: {
31 | merge: function (args) {
32 | return args
33 | }
34 | }
35 | }
36 | }
37 |
38 | this.authenticate = authenticateStub
39 |
40 | this.paginate = callStub
41 | }
42 | OctokitMock.plugin = sinon.stub().returns(OctokitMock)
43 |
44 | github.__set__('Octokit', OctokitMock)
45 |
46 | beforeEach(() => {
47 | github.resetList = {}
48 | callStub.reset()
49 | cache.clear()
50 | expectedAuth = undefined
51 | authenticateStub.reset()
52 | })
53 |
54 | it('should return an error if obj is not set', async () => {
55 | try {
56 | await github.call({})
57 | assert(false, 'Should throw an error')
58 | } catch (error) {
59 | assert.equal(error.message, 'obj required/obj not found')
60 | }
61 | })
62 |
63 | it('should return an error if fun is not set', async () => {
64 | try {
65 | await github.call({ obj: 'obj' })
66 | assert(false, 'Should throw an error')
67 | } catch (error) {
68 | assert.equal(error.message, 'fun required/fun not found')
69 | }
70 | })
71 |
72 | it('should authenticate when token is set', async () => {
73 | expectedAuth = 'token token'
74 | callStub.resolves({ data: {}, meta: {} })
75 |
76 | await github.call({
77 | obj: 'obj',
78 | fun: 'fun',
79 | token: 'token'
80 | })
81 | })
82 |
83 | it('should authenticate when basic authentication is required', async () => {
84 | expectedAuth = {
85 | username: 'user',
86 | password: 'pass'
87 | }
88 | callStub.resolves({ data: {}, meta: {} })
89 | await github.call({
90 | obj: 'obj',
91 | fun: 'fun',
92 | basicAuth: {
93 | user: 'user',
94 | pass: 'pass'
95 | }
96 | })
97 | })
98 |
99 | it('should not authenticate when neither token nor basicAuth are provided', async () => {
100 | expectedAuth = undefined
101 | callStub.resolves({ data: {}, meta: {} })
102 | await github.call({
103 | obj: 'obj',
104 | fun: 'fun'
105 | })
106 | })
107 |
108 | it('should call the appropriate function on the github api', async () => {
109 | const testHeaders = {
110 | link: null,
111 | 'x-oauth-scopes': []
112 | }
113 | callStub.resolves({ data: {}, headers: testHeaders })
114 | const res = await github.call({
115 | obj: 'obj',
116 | fun: 'fun'
117 | })
118 | assert.deepStrictEqual(res, { data: {}, headers: testHeaders })
119 | })
120 |
121 | it('should return github error', async () => {
122 | callStub.rejects('github error')
123 | try {
124 | await github.call({
125 | obj: 'obj',
126 | fun: 'fun'
127 | })
128 | assert(false, 'Should throw an error')
129 | } catch (error) {
130 | assert.equal(error, 'github error')
131 | }
132 | })
133 | })
--------------------------------------------------------------------------------
/src/client/modals/upload.js:
--------------------------------------------------------------------------------
1 | module.controller('UploadCtrl',
2 | function ($scope, $modalInstance, customFields) {
3 |
4 | $scope.json = {};
5 | $scope.availableFieldKeys = ['user', 'created_at'];
6 | $scope.selectedKeys = {};
7 |
8 | $scope.availableFieldKeys = $scope.availableFieldKeys.concat(customFields);
9 |
10 | $scope.selectedKeyList = function () {
11 | return Object.keys($scope.selectedKeys).map(function (i) { return $scope.selectedKeys[i]; });
12 | };
13 |
14 | $scope.canBeUploaded = function () {
15 | return $scope.selectedKeyList().indexOf('user') != -1;
16 | };
17 |
18 | $scope.validateAttribute = function (data, index) {
19 | if ($scope.selectedKeys[index] === 'created_at') {
20 | return isNaN(Date.parse(data)) ? 'danger' : 'success';
21 | } else if ($scope.selectedKeys[index] === 'user') {
22 | return typeof data === 'string' ? 'success' : 'danger';
23 | }
24 | };
25 |
26 | $scope.$watch('file', function () {
27 | if ($scope.file) {
28 | Papa.parse($scope.file, {
29 | complete: function (data) {
30 | $scope.json = data;
31 | }
32 | });
33 | }
34 | });
35 |
36 | $scope.upload = function () {
37 |
38 | var data = [];
39 | if ($scope.header) {
40 | $scope.json.data.splice(0, 1);
41 | }
42 | data = $scope.json.data.map(function (line) {
43 | var argsToUpload = {};
44 | try {
45 | Object.keys($scope.selectedKeys).forEach(function (index) {
46 | var attributeName = $scope.selectedKeys[index];
47 | if (attributeName === 'user') {
48 | argsToUpload.user = line[index];
49 | } else if (attributeName === 'created_at') {
50 | if (isNaN(Date.parse(line[index]))) {
51 | throw new Error('unsupported date format');
52 | }
53 | argsToUpload.created_at = new Date(line[index]).toUTCString();
54 | } else {
55 | argsToUpload.custom_fields = argsToUpload.custom_fields ? argsToUpload.custom_fields : {};
56 | argsToUpload.custom_fields[attributeName] = line[index];
57 | }
58 | });
59 | } catch (e) {
60 | // eslint-disable-next-line no-console
61 | console.log(e);
62 |
63 | return;
64 | }
65 | argsToUpload.custom_fields = JSON.stringify(argsToUpload.custom_fields);
66 |
67 | return argsToUpload;
68 | }).filter(function (line) { return line; });
69 |
70 | $modalInstance.close(data);
71 | };
72 |
73 | $scope.cancel = function () {
74 | $modalInstance.dismiss('cancel');
75 | };
76 | }
77 | )
78 | .directive('fileModel', function () {
79 | return {
80 | scope: {
81 | fileModel: '='
82 | },
83 | link: function (scope, elem) {
84 | elem.bind('change drop', function (change) {
85 | var reader = new FileReader();
86 | reader.onload = function (load) {
87 | scope.$apply(function () {
88 | scope.fileModel = load.target.result;
89 | });
90 | };
91 | reader.readAsText(change.target.files[0]);
92 | });
93 | }
94 | };
95 | });
96 |
97 | filters.filter('notSelected', function () {
98 | return function (items, arr) {
99 |
100 | if (!arr || arr.length === 0) {
101 | return items;
102 | }
103 |
104 | var notMatched = [];
105 |
106 | items.forEach(function (item) {
107 | if (arr.indexOf(item) < 0) {
108 | notMatched.push(item);
109 | }
110 | });
111 |
112 | return notMatched;
113 | };
114 | });
--------------------------------------------------------------------------------
/src/client/assets/images/nervous_remove/nervous-36.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
41 |
43 |
46 |
47 |
48 |
49 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/client/modals/templates/report.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Contributors who signed {{getGistName()}}
12 |
13 |
14 |
29 |
30 |
31 |
32 |
33 |
34 |
{{getGistName()}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
{{ claItem.owner }} / {{ claItem.repo }}
42 |
{{ claItem.org }}
43 |
44 |
45 |
46 |
Contributor
47 |
48 |
49 |
Date
50 |
51 |
52 |
53 |
54 |
55 |
56 |
{{contributor.user_name}}
57 |
{{contributor.signed_at | date: 'medium'}}
58 |
59 |
60 |
61 |
62 |
63 |
64 | {{contributors.length}} contributor signed this CLA
65 |
66 | {{contributors.length}} contributors signed this CLA
67 |
--------------------------------------------------------------------------------
/src/client/assets/images/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
41 |
43 |
46 |
47 |
48 |
49 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/client/templates/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
27 |
28 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 | {{ item.sharedGist ? 'Yes' : 'No' }}
47 |
48 |
49 |
53 |
54 |
63 |
64 |
67 |
68 |
69 |
76 |
85 |
--------------------------------------------------------------------------------
/src/server/services/github.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise-native')
2 |
3 | const cache = require('memory-cache')
4 | const config = require('../../config')
5 | let Octokit = require('@octokit/rest')
6 | .plugin(require('@octokit/plugin-throttling'))
7 | .plugin(require('@octokit/plugin-retry'))
8 | const stringify = require('json-stable-stringify')
9 | const logger = require('../services/logger')
10 |
11 | async function callGithub(octokit, obj, fun, arg, cacheKey) {
12 | const cachedRes = arg.noCache ? null : cache.get(cacheKey)
13 | delete arg.noCache
14 | if (cachedRes && config.server.cache_time > 0) {
15 | return cachedRes
16 | }
17 | let res
18 | if (fun.match(/list.*/g)) {
19 | const options = octokit[obj][fun].endpoint.merge(arg)
20 | res = {
21 | data: await octokit.paginate(options)
22 | }
23 | } else {
24 | res = await octokit[obj][fun](arg)
25 | }
26 |
27 | if (res && config.server.cache_time > 0) {
28 | cache.put(cacheKey, res, 60000 * config.server.cache_time)
29 | }
30 |
31 | return res
32 | }
33 |
34 | function newOctokit(auth) {
35 | Octokit = addReposGetByIdEndpoint()
36 | return new Octokit({
37 | auth,
38 | protocol: config.server.github.protocol,
39 | version: config.server.github.version,
40 | host: config.server.github.api,
41 | pathPrefix: config.server.github.enterprise ? '/api/v3' : null,
42 | throttle: {
43 | onRateLimit: (retryAfter, options) => {
44 | // eslint-disable-next-line no-console
45 | console.warn(`Request quota exhausted for request ${options.method} ${options.url}`)
46 |
47 | if (options.request.retryCount === 0) { // only retries once
48 | // eslint-disable-next-line no-console
49 | console.log(`Retrying after ${retryAfter} seconds!`)
50 | return true
51 | }
52 | },
53 | onAbuseLimit: (retryAfter, options) => {
54 | // does not retry, only logs a warning
55 | // eslint-disable-next-line no-console
56 | console.warn(`Abuse detected for request ${options.method} ${options.url}`)
57 | }
58 | }
59 | })
60 | }
61 |
62 | function addReposGetByIdEndpoint() {
63 | return Octokit.plugin((octokit) => {
64 | octokit.registerEndpoints({
65 | repos: {
66 | getById: {
67 | method: 'GET',
68 | url: '/repositories/:id',
69 | params: {
70 | id: {
71 | type: 'string',
72 | required: true
73 | }
74 | }
75 | }
76 | }
77 | })
78 | })
79 | }
80 |
81 | const githubService = {
82 | resetList: {},
83 |
84 | call: async (call) => {
85 | const arg = call.arg || {}
86 | const basicAuth = call.basicAuth
87 | const fun = call.fun
88 | const obj = call.obj
89 | const token = call.token
90 |
91 | const argWithoutNoCache = Object.assign({}, arg)
92 | delete argWithoutNoCache.noCache
93 |
94 | const stringArgs = stringify({
95 | obj: call.obj,
96 | fun: call.fun,
97 | arg: argWithoutNoCache,
98 | token: call.token
99 | })
100 |
101 | let auth
102 | if (token) {
103 | auth = `token ${token}`
104 | }
105 | if (basicAuth) {
106 | auth = {
107 | username: basicAuth.user,
108 | password: basicAuth.pass
109 | }
110 | }
111 | const octokit = newOctokit(auth)
112 |
113 | if (!obj || !octokit[obj]) {
114 | throw new Error('obj required/obj not found')
115 | }
116 |
117 | if (!fun || !octokit[obj][fun]) {
118 | throw new Error('fun required/fun not found')
119 | }
120 | try {
121 | return callGithub(octokit, obj, fun, arg, stringArgs)
122 | } catch (error) {
123 | logger.info(`${error} - Error on callGithub.${obj}.${fun} with args ${arg}.`)
124 | throw new Error(error)
125 | }
126 | },
127 |
128 | callGraphql: async (query, token) => {
129 | return request.post({
130 | headers: {
131 | 'Authorization': `bearer ${token}`,
132 | 'User-Agent': 'CLA assistant'
133 | },
134 | url: config.server.github.graphqlEndpoint,
135 | body: query
136 | })
137 | }
138 | }
139 |
140 | module.exports = githubService
--------------------------------------------------------------------------------
/src/server/api/org.js:
--------------------------------------------------------------------------------
1 | const org = require('../services/org')
2 | const github = require('../services/github')
3 | const log = require('../services/logger')
4 | const Joi = require('joi')
5 | const webhook = require('./webhook')
6 | const logger = require('../services/logger')
7 | const utils = require('../middleware/utils')
8 | //queries
9 | const queries = require('../graphQueries/github')
10 | const newOrgSchema = Joi.object().keys({
11 | orgId: Joi.number().required(),
12 | org: Joi.string().required(),
13 | gist: Joi.string().required(),
14 | token: Joi.string().required(),
15 | excludePattern: Joi.string(),
16 | sharedGist: Joi.boolean(),
17 | minFileChanges: Joi.number(),
18 | minCodeChanges: Joi.number(),
19 | whiteListPattern: Joi.string().allow(''),
20 | privacyPolicy: Joi.string().allow('')
21 | })
22 | const removeOrgSchema = Joi.object().keys({
23 | org: Joi.string(),
24 | orgId: Joi.number()
25 | }).or('org', 'orgId')
26 |
27 | class OrgAPI {
28 | async create(req) {
29 | req.args.token = req.args.token || req.user.token
30 | utils.validateArgs(req.args, newOrgSchema, true)
31 |
32 | const query = {
33 | orgId: req.args.orgId,
34 | org: req.args.org,
35 | }
36 | let dbOrg
37 | try {
38 | dbOrg = await org.get(query)
39 | } catch (error) {
40 | logger.info(new Error(error).stack)
41 | }
42 |
43 | if (dbOrg) {
44 | throw new Error('This org is already linked.')
45 | }
46 | dbOrg = await org.create(req.args)
47 |
48 | try {
49 | await webhook.create(req)
50 | } catch (error) {
51 | logger.error(new Error(error).stack)
52 | }
53 | return dbOrg
54 | }
55 |
56 | async update(req) {
57 | req.args.token = req.args.token || req.user.token
58 | utils.validateArgs(req.args, newOrgSchema, true)
59 |
60 | return org.update(req.args)
61 | }
62 |
63 | async getForUser(req) {
64 | try {
65 | const res = await this.getGHOrgsForUser(req)
66 | const argsForOrg = {
67 | orgId: res.map((org) => org.id)
68 | }
69 | return org.getMultiple(argsForOrg)
70 | } catch (error) {
71 | log.warn(error.stack)
72 | throw error
73 | }
74 | }
75 |
76 | async getGHOrgsForUser(req) {
77 | let organizations = []
78 |
79 | async function callGithub(arg) {
80 | const query = arg.query ? arg.query : queries.getUserOrgs(req.user.login, null)
81 |
82 | try {
83 | let body = await github.callGraphql(query, req.user.token)
84 | body = JSON.parse(body)
85 |
86 | if (body.errors) {
87 | const errorMessage = body.errors[0] && body.errors[0].message ? body.errors[0].message : 'Error occurred by getting users organizations'
88 | logger.info(new Error(errorMessage).stack)
89 | }
90 |
91 | const data = body.data.user.organizations
92 |
93 | organizations = data.edges.reduce((orgs, edge) => {
94 | if (edge.node.viewerCanAdminister) {
95 | edge.node.id = edge.node.databaseId
96 | orgs.push(edge.node)
97 | }
98 |
99 | return orgs
100 | }, organizations)
101 |
102 | if (data.pageInfo.hasNextPage) {
103 | arg.query = queries.getUserOrgs(req.user.login, data.pageInfo.endCursor)
104 | return callGithub(arg)
105 | }
106 | return organizations
107 | } catch (error) {
108 | log.info(new Error(error).stack)
109 | log.warn(`No result on GH call, getting user orgs! For user: ${req.user}`)
110 | throw error
111 | }
112 | }
113 | if (req.user && req.user.login) {
114 | return callGithub({})
115 | }
116 |
117 | throw new Error('User is undefined')
118 | }
119 | // update(req, done){
120 | // org.update(req.args, done)
121 | // }
122 | async remove(req) {
123 | utils.validateArgs(req.args, removeOrgSchema)
124 | const dbOrg = await org.remove(req.args)
125 | if (!dbOrg) {
126 | throw new Error('Organization is not Found')
127 | }
128 | req.args.org = dbOrg.org
129 | try {
130 | await webhook.remove(req)
131 | } catch (error) {
132 | logger.warn(new Error(error))
133 | }
134 | return dbOrg
135 | }
136 | }
137 |
138 | module.exports = new OrgAPI()
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration Module
3 | *
4 | * @title config
5 | * @overview Configuration Module
6 | */
7 | let path = require('path');
8 |
9 | module.exports = {
10 | server: {
11 | github: {
12 | // optional
13 | protocol: process.env.GITHUB_PROTOCOL || 'https',
14 | host: process.env.GITHUB_HOST || 'github.com',
15 | api: process.env.GITHUB_API_HOST || 'api.github.com',
16 | enterprise: !!process.env.GITHUB_HOST, // flag enterprise version
17 | version: process.env.GITHUB_VERSION || '3.0.0',
18 |
19 | graphqlEndpoint: process.env.GITHUB_GRAPHQL || 'https://api.github.com/graphql',
20 |
21 | // required
22 | client: process.env.GITHUB_CLIENT,
23 | secret: process.env.GITHUB_SECRET,
24 |
25 | // required
26 | user: process.env.GITHUB_USER,
27 | pass: process.env.GITHUB_PASS,
28 | admin_users: process.env.GITHUB_ADMIN_USERS ? process.env.GITHUB_ADMIN_USERS.split(/\s*,\s*/) : [],
29 |
30 | // required
31 | token: process.env.GITHUB_TOKEN,
32 |
33 | user_scope: ['user:email'],
34 | admin_scope: ['user:email', 'repo:status', 'read:repo_hook', 'write:repo_hook', 'read:org', 'gist'],
35 |
36 | commit_bots: ['web-flow'],
37 |
38 | //delay reaction on webhook
39 | enforceDelay: parseInt(process.env.GITHUB_DELAY || '5000', 10),
40 |
41 | //slow down API calls in order to avoid abuse rate limit
42 | timeToWait: process.env.GITHUB_TIME_TO_WAIT || 1000
43 | },
44 |
45 | localport: process.env.PORT || 5000,
46 |
47 | always_recompile_sass: process.env.NODE_ENV === 'production' ? false : true,
48 |
49 | cache_time: process.env.CACHE_TIME || 5,
50 |
51 | http: {
52 | protocol: process.env.PROTOCOL || 'http',
53 | host: process.env.HOST || 'cla-assistant.io',
54 | port: process.env.HOST_PORT
55 | },
56 |
57 | security: {
58 | sessionSecret: process.env.SESSION_SECRET || 'cla-assistant',
59 | cookieMaxAge: 60 * 60 * 1000
60 | },
61 |
62 | smtp: {
63 | enabled: !!process.env.SMTP_HOST,
64 | host: process.env.SMTP_HOST,
65 | secure: (!!process.env.SMTP_SSL && process.env.SMTP_SSL === 'true'),
66 | port: process.env.SMTP_PORT || 465,
67 | auth: {
68 | user: process.env.SMTP_USER,
69 | pass: process.env.SMTP_PASS
70 | },
71 | name: 'cla-assistant'
72 | },
73 |
74 | mongodb: {
75 | uri: process.env.MONGODB || process.env.MONGOLAB_URI
76 | },
77 |
78 | slack: {
79 | url: process.env.SLACK_URL,
80 | channel: process.env.SLACK_CHANNEL
81 | },
82 |
83 | templates: {
84 | login: process.env.LOGIN_PAGE_TEMPLATE || path.join(__dirname, 'client', 'login.html')
85 | },
86 |
87 | api_access: {
88 | free: ['/api/cla/get', '/api/cla/getLinkedItem'],
89 | external: ['/api/cla/getAll'],
90 | admin_only: [
91 | '/api/cla/addSignature',
92 | '/api/cla/hasSignature',
93 | '/api/cla/terminateSignature',
94 | '/api/cla/validate',
95 | '/api/cla/getGist',
96 | '/api/org/create',
97 | '/api/org/remove',
98 | '/api/repo/create',
99 | '/api/repo/remove'
100 | ]
101 | },
102 |
103 | feature_flag: {
104 | required_signees: process.env.REQUIRED_SIGNEES || '',
105 | organization_override_enabled: process.env.ORG_OVERRIDE_ENABLED || false,
106 | },
107 |
108 | static: [
109 | path.join(__dirname, 'bower'),
110 | path.join(__dirname, 'client')
111 | ],
112 |
113 | api: [
114 | path.join(__dirname, 'server', 'api', '*.js')
115 | ],
116 |
117 | webhooks: [
118 | path.join(__dirname, 'server', 'webhooks', '*.js')
119 | ],
120 |
121 | documents: [
122 | path.join(__dirname, 'server', 'documents', '*.js')
123 | ],
124 |
125 | controller: [
126 | path.join(__dirname, 'server', 'controller', '!(default).js'),
127 | path.join(__dirname, 'server', 'controller', 'default.js')
128 | ],
129 |
130 | graphQueries: [
131 | path.join(__dirname, 'server', 'graphQueries', '*.js')
132 | ],
133 |
134 | middleware: [
135 | path.join(__dirname, 'server', 'middleware', '*.js')
136 | ],
137 |
138 | passport: [
139 | path.join(__dirname, 'server', 'passports', '*.js')
140 | ],
141 |
142 | }
143 | };
--------------------------------------------------------------------------------
/src/client/assets/images/feature2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 |
70 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/server/passports/github.js:
--------------------------------------------------------------------------------
1 | const url = require('../services/url')
2 | const repoService = require('../services/repo')
3 | const orgApi = require('../api/org')
4 | const logger = require('../services/logger')
5 | const passport = require('passport')
6 | const Strategy = require('passport-github').Strategy
7 | const merge = require('merge')
8 | const User = require('mongoose').model('User')
9 | const fetch = require('node-fetch')
10 | const base64 = require('base-64')
11 |
12 | function updateToken(item, newToken) {
13 | item.token = newToken
14 | item.save()
15 | logger.debug('Update access token for repo / org', item.repo || item.org)
16 | }
17 |
18 | async function checkToken(item, accessToken) {
19 | const baseURL = `https://api.github.com`
20 | const checkAuthApi = `${ baseURL }/applications/${ config.server.github.client }/token`
21 | const newToken = accessToken
22 | const oldToken = item.token
23 | const accesstokenObject = {
24 | access_token: oldToken
25 | }
26 |
27 | try {
28 | const header = {
29 | method: 'POST',
30 | headers: {
31 | 'Authorization': 'Basic ' + base64.encode(config.server.github.client + ":" + config.server.github.secret),
32 | 'User-Agent': 'CLA assistant',
33 | 'Accept': 'application/vnd.github.doctor-strange-preview+json',
34 | 'Content-Type': 'application/json'
35 | },
36 | body: JSON.stringify(accesstokenObject)
37 | }
38 | const res = await fetch(checkAuthApi, header)
39 | const checkTokenResponse = await res.json()
40 | if (checkTokenResponse) {
41 | if (!(checkTokenResponse && checkTokenResponse.scopes && checkTokenResponse.scopes.indexOf('write:repo_hook') >= 0)) {
42 | updateToken(item, newToken)
43 | } else if (item.repo) {
44 | const ghRepo = await repoService.getGHRepo(item)
45 | if (!(ghRepo && ghRepo.permissions && ghRepo.permissions.admin)) {
46 | updateToken(item, newToken)
47 | logger.info('Update access token for repo ', item.repo, ' admin rights have been changed')
48 | }
49 | }
50 | }
51 | } catch (error) {
52 | updateToken(item, newToken)
53 | }
54 |
55 | }
56 |
57 | passport.use(new Strategy({
58 | clientID: config.server.github.client,
59 | clientSecret: config.server.github.secret,
60 | callbackURL: url.githubCallback,
61 | authorizationURL: url.githubAuthorization,
62 | tokenURL: url.githubToken,
63 | userProfileURL: url.githubProfile()
64 | }, async (accessToken, _refreshToken, params, profile, done) => {
65 | let user
66 | try {
67 | user = await User.findOne({
68 | name: profile.username
69 | })
70 |
71 | if (user && !user.uuid) {
72 | user.uuid = profile.id
73 | }
74 | user.token = accessToken
75 | user.save()
76 | } catch (error) {
77 | logger.warn(error.stack)
78 | }
79 |
80 | if (!user) {
81 | try {
82 | await User.create({
83 | uuid: profile.id,
84 | name: profile.username,
85 | token: accessToken
86 | })
87 | } catch (error) {
88 | logger.warn(new Error(`Could not create new user ${error}`).stack)
89 | }
90 | }
91 | // User.update({
92 | // uuid: profile.id
93 | // }, {
94 | // name: profile.username,
95 | // email: '', // needs fix
96 | // token: accessToken
97 | // }, {
98 | // upsert: true
99 | // }, function () {})
100 |
101 | if (params.scope.indexOf('write:repo_hook') >= 0) {
102 | try {
103 | const repoRes = await repoService.getUserRepos({
104 | token: accessToken
105 | })
106 | if (repoRes && repoRes.length > 0) {
107 | repoRes.forEach((repo) => checkToken(repo, accessToken))
108 | }
109 | } catch (error) {
110 | logger.warn(new Error(error).stack)
111 | }
112 | }
113 | if (params.scope.indexOf('admin:org_hook') >= 0) {
114 | try {
115 | const orgRes = await orgApi.getForUser({
116 | user: {
117 | token: accessToken,
118 | login: profile.username
119 | }
120 | })
121 | if (orgRes && orgRes.length > 0) {
122 | orgRes.forEach((org) => checkToken(org, accessToken))
123 | }
124 | } catch (error) {
125 | logger.warn(new Error(error).stack)
126 | }
127 | }
128 | done(null, merge(profile._json, {
129 | token: accessToken,
130 | scope: params.scope
131 | }))
132 | }))
133 |
134 | passport.serializeUser((user, done) => done(null, user))
135 |
136 | passport.deserializeUser((user, done) => done(null, user))
--------------------------------------------------------------------------------