├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── feature_request.yml │ └── general_issue.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── node.js.yml ├── .gitignore ├── .husky └── pre-commit ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Open Certs.postman_collection.json ├── README.md ├── bin └── www ├── docs ├── BITBUCKET_SETUP.md ├── GITHUB_SETUP.md ├── GITLAB_SETUP.md └── MONGO_DB_SETUP.md ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── sample.env ├── sample.test.env ├── src ├── app.js ├── config │ └── constants.js ├── controllers │ ├── auth.controller.js │ ├── certificate.controller.js │ ├── project.controller.js │ └── user.controler.js ├── errors │ ├── authentication.error.js │ ├── custom.error.js │ ├── githubAPI.error.js │ ├── githubAPITimeout.error.js │ ├── notFound.error.js │ ├── passport.error.js │ ├── projectToken.error.js │ └── recaptcha.error.js ├── helpers │ ├── bitbucket.helper.js │ ├── cluster.helper.js │ ├── crypto.helper.js │ ├── errorhandler.helper.js │ ├── github.helper.js │ ├── gitlab.helper.js │ ├── jwt.helper.js │ ├── objectId.helper.js │ ├── passport.helper.js │ ├── project.jwt.helper.js │ ├── recaptcha.helper.js │ └── user.helper.js ├── index.js ├── models │ ├── certificate.model.js │ └── index.model.js ├── routes │ ├── auth.route.js │ ├── certificate.route.js │ ├── index.route.js │ ├── project.route.js │ └── users.route.js ├── validations │ ├── certificate.validation.js │ └── project.validation.js └── views │ ├── certificate.ejs │ ├── certificateHolder.ejs │ ├── error.ejs │ └── index.ejs └── test ├── database.test.js ├── enviroment.test.js ├── integration └── init.test.js └── unit ├── auth.test.js ├── certificate.test.js ├── crypto.helper.test.js ├── errorHandler.test.js ├── github.helper.test.js ├── project.controller.test.js ├── project.jwt.helper.test.js ├── recaptcha.helper.test.js ├── user.helper.test.js └── user.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .* 3 | *.min.js 4 | adminPanel -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2020": true, 5 | "node": true, 6 | "jest/globals": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:node/recommended"], 9 | "plugins": ["prettier","jest"], 10 | "parserOptions": { 11 | "ecmaVersion": 12 12 | }, 13 | "rules": { 14 | "node/shebang": "error", 15 | "indent": 0, 16 | "prefer-const": ["error", { 17 | "destructuring": "any", 18 | "ignoreReadBeforeAssign": false 19 | }], 20 | "quotes": 0, 21 | "semi": ["error", "always"], 22 | "linebreak-style": 0, 23 | "no-use-before-define": 0, 24 | "no-tabs": 0, 25 | "no-underscore-dangle": 0, 26 | "space-before-function-paren": 0, 27 | "quote-props": 0, 28 | "comma-dangle": 0, 29 | "operator-linebreak": 0, 30 | "new-cap": 0, 31 | "arrow-parens": 0, 32 | "function-paren-newline": 0, 33 | "no-param-reassign": 0, 34 | "object-curly-newline": 0, 35 | "no-console": 0, 36 | "import/prefer-default-export": 0, 37 | "consistent-return": 0, 38 | "func-names": 0, 39 | "implicit-arrow-linebreak": 0, 40 | "node/no-extraneous-require": [ 41 | "error", 42 | { 43 | "allowModules": ["chalk"] 44 | } 45 | ], 46 | "prettier/prettier": [ 47 | "error", 48 | { 49 | "singleQuote": true, 50 | "trailingComma": "none", 51 | "bracketSpacing": true, 52 | "jsxBracketSameLine": true, 53 | "parser": "flow", 54 | "semi": true, 55 | "endOfLine":"auto", 56 | "tabWidth":4 57 | } 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug 3 | title: "[Bug]: " 4 | labels: 5 | - "bug" 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Preflight Checklist 10 | description: Please ensure you've completed all of the following. 11 | options: 12 | - label: I have read the Contribution.md for this project. 13 | required: true 14 | - label: I agree to follow the Code of Conduct that this project adheres to. 15 | required: true 16 | - label: I have searched the issue for a feature request that matches the one I want to file, without success. 17 | required: true 18 | - type: input 19 | attributes: 20 | label: Version 21 | description: What version of are you using? 22 | placeholder: 0.0.0 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Current Behavior 28 | description: A clear description of what actually happens. 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: Expected Behavior 34 | description: A clear and concise description of what you expected to happen. 35 | validations: 36 | required: true 37 | - type: textarea 38 | attributes: 39 | label: Additional Information 40 | description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea 3 | title: "[Feature Request]: " 4 | labels: 5 | - "enhancement" 6 | body: 7 | - type: checkboxes 8 | attributes: 9 | label: Preflight Checklist 10 | description: Please ensure you've completed all of the following. 11 | options: 12 | - label: I have read the Contributions.md for this project. 13 | required: true 14 | - label: I agree to follow the Code of Conduct that this project adheres to. 15 | required: true 16 | - label: I have searched the feature for a feature request that matches the one I want to file, without success. 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Problem Description 21 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Proposed Solution 27 | description: Describe the solution you'd like in a clear and concise manner. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Alternatives Considered 33 | description: A clear and concise description of any alternative solutions or features you've considered. 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Additional Information 39 | description: Add any other context about the problem here. 40 | validations: 41 | required: false 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General issue 3 | about: Suggest an issue for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | # Prerequisites: 10 | 11 | Please answer the following questions for yourself before submitting an issue. **YOU MAY DELETE THE PREREQUISITES SECTION.** 12 | 13 | 14 | 15 | - [ ] I checked to make sure that this issue has not already been filed. 16 | 17 | # Expected Behavior: 18 | 19 | Please describe the behavior you are expecting. 20 | 21 | # Current Behavior: 22 | 23 | What is the current behavior? 24 | 25 | # Solution: 26 | 27 | Please describe what will you do to solve this issue or your approach to solve this issue. 28 | 29 | # Screenshots (optional): 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description: 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue no.) 6 | 7 | 8 | 9 | ## Type of change: 10 | 11 | 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | # Checklist: 19 | 20 | 21 | 22 | - [ ] My code follows the style guidelines of this project. 23 | - [ ] I have performed a self-review of my own code. 24 | - [ ] I have commented my code, particularly in hard-to-understand areas. 25 | - [ ] I have made corresponding changes to the documentation. 26 | - [ ] My changes generate no new warnings. 27 | - [ ] I have added tests that prove my fix is effective or that my feature works. 28 | - [ ] New and existing unit tests pass locally with my changes. 29 | - [ ] Any dependent changes have been merged and published in downstream modules. 30 | 31 | # Screenshots / Video: 32 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [main, ci-cd] 9 | pull_request: 10 | branches: [main, ci-cd] 11 | env: 12 | CI: true 13 | MONGO_DB_URI: mongodb://localhost:27017/open-certs 14 | CLUSTER: YES 15 | SHOW_MONGO: YES 16 | FRONTEND_URL: http://localhost:3000 17 | TOKEN_SECRET: XX 18 | GITHUB_CLIENT_ID: XX 19 | PROJECT_TOKEN_SECRET: XX 20 | GITHUB_CLIENT_SECRET: XX 21 | BITBUCKET_CLIENT_ID: XX 22 | BITBUCKET_CLIENT_SECRET: XX 23 | GITLAB_CLIENT_ID: XX 24 | GITLAB_CLIENT_SECRET: XX 25 | RECAPTCHA_SECRECT_KEY: XX 26 | VALIDATE_RECAPTCHA: YES 27 | TEST: YES 28 | ENCRYPTION_SECRET_KEY: jhfuhksjdbhjbajhdghabhbdhsabhbajhdbsjahbdysgyhfahbsdhbjbhad 29 | 30 | jobs: 31 | build: 32 | runs-on: ubuntu-latest 33 | 34 | strategy: 35 | matrix: 36 | node-version: [16.x] 37 | mongodb-version: ["5.0"] 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v2 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | cache: "npm" 46 | - name: Start MongoDB 47 | uses: supercharge/mongodb-github-action@1.7.0 48 | with: 49 | mongodb-version: ${{ matrix.mongodb-version }} 50 | - run: npm ci 51 | - run: npm run build --if-present 52 | - run: npm run quality 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | 64 | .test.env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format && npm test 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Email. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Open Certs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Open Certs.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "465d0b08-8a0c-4346-b8fd-ca034a176f1c", 4 | "name": "Open Certs", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "auth", 10 | "item": [ 11 | { 12 | "name": "Github Login", 13 | "event": [ 14 | { 15 | "listen": "test", 16 | "script": { 17 | "exec": [ 18 | "pm.test(\"Github-Redirected\",()=>{\r", 19 | " pm.expect(pm.response.headers.get('server').toLowerCase()).eq(\"github.com\")\r", 20 | " pm.expect(pm.response.code).eq(200)\r", 21 | "})" 22 | ], 23 | "type": "text/javascript" 24 | } 25 | } 26 | ], 27 | "request": { 28 | "method": "GET", 29 | "header": [], 30 | "url": { 31 | "raw": "{{base_url}}/auth/github", 32 | "host": [ 33 | "{{base_url}}" 34 | ], 35 | "path": [ 36 | "auth", 37 | "github" 38 | ] 39 | } 40 | }, 41 | "response": [] 42 | }, 43 | { 44 | "name": "Gitlab Login", 45 | "event": [ 46 | { 47 | "listen": "test", 48 | "script": { 49 | "exec": [ 50 | "var cheerio = require(\"cheerio\")\r", 51 | "pm.test(\"Gitlab-Redirected\",()=>{\r", 52 | " pm.expect(pm.response.headers.get('server').toLowerCase()).eq(\"cloudflare\")\r", 53 | " pm.expect(pm.response.code).eq(503)\r", 54 | " const $ = cheerio.load(pm.response.text())\r", 55 | " pm.expect($(\"title\").text().includes(\"GitLab\")).eq(true)\r", 56 | "})" 57 | ], 58 | "type": "text/javascript" 59 | } 60 | } 61 | ], 62 | "request": { 63 | "method": "GET", 64 | "header": [], 65 | "url": { 66 | "raw": "{{base_url}}/auth/gitlab", 67 | "host": [ 68 | "{{base_url}}" 69 | ], 70 | "path": [ 71 | "auth", 72 | "gitlab" 73 | ] 74 | } 75 | }, 76 | "response": [] 77 | }, 78 | { 79 | "name": "Bitbucket Login", 80 | "event": [ 81 | { 82 | "listen": "test", 83 | "script": { 84 | "exec": [ 85 | "var cheerio = require(\"cheerio\")\r", 86 | "pm.test(\"Bitbucket-Redirected\",()=>{\r", 87 | " pm.expect(pm.response.headers.get('server').toLowerCase()).eq(\"globaledge-envoy\")\r", 88 | " pm.expect(pm.response.code).eq(200)\r", 89 | " const $ = cheerio.load(pm.response.text())\r", 90 | " pm.expect($(\"title\").text().includes(\"Atlassian\")).eq(true)\r", 91 | "})" 92 | ], 93 | "type": "text/javascript" 94 | } 95 | } 96 | ], 97 | "request": { 98 | "method": "GET", 99 | "header": [], 100 | "url": { 101 | "raw": "{{base_url}}/auth/bitbucket", 102 | "host": [ 103 | "{{base_url}}" 104 | ], 105 | "path": [ 106 | "auth", 107 | "bitbucket" 108 | ] 109 | } 110 | }, 111 | "response": [] 112 | } 113 | ] 114 | }, 115 | { 116 | "name": "Certificate", 117 | "item": [ 118 | { 119 | "name": "Generate Certificate", 120 | "request": { 121 | "method": "POST", 122 | "header": [ 123 | { 124 | "key": "Authorization", 125 | "value": "{{auth_token}}", 126 | "type": "text" 127 | }, 128 | { 129 | "key": "project-token", 130 | "value": "{{project_token}}", 131 | "type": "text" 132 | } 133 | ], 134 | "body": { 135 | "mode": "raw", 136 | "raw": "{\n \"includeUserImage\": true,\n \"includeRepositoryImage\": true \n}", 137 | "options": { 138 | "raw": { 139 | "language": "json" 140 | } 141 | } 142 | }, 143 | "url": { 144 | "raw": "{{base_url}}/certificate/", 145 | "host": [ 146 | "{{base_url}}" 147 | ], 148 | "path": [ 149 | "certificate", 150 | "" 151 | ] 152 | } 153 | }, 154 | "response": [] 155 | }, 156 | { 157 | "name": "Fetch Certificate", 158 | "event": [ 159 | { 160 | "listen": "test", 161 | "script": { 162 | "exec": [ 163 | "const res = pm.response.json()\r", 164 | "pm.test(\"should return Certificate when called with valid certificateID\",()=>{\r", 165 | " pm.expect(res.error).eq(undefined);\r", 166 | " pm.expect(res.certificate).not.eq(undefined);\r", 167 | " pm.expect(res.certificate._id).eq(pm.environment.get(\"certificateId\"));\r", 168 | " pm.expect(pm.response.code).eq(200)\r", 169 | "})" 170 | ], 171 | "type": "text/javascript" 172 | } 173 | } 174 | ], 175 | "protocolProfileBehavior": { 176 | "disableBodyPruning": true 177 | }, 178 | "request": { 179 | "method": "GET", 180 | "header": [ 181 | { 182 | "key": "Authorization", 183 | "value": "{{auth_token}}", 184 | "type": "text" 185 | } 186 | ], 187 | "body": { 188 | "mode": "raw", 189 | "raw": "" 190 | }, 191 | "url": { 192 | "raw": "{{base_url}}/certificate/certDetails/{{certificateId}}", 193 | "host": [ 194 | "{{base_url}}" 195 | ], 196 | "path": [ 197 | "certificate", 198 | "certDetails", 199 | "{{certificateId}}" 200 | ] 201 | } 202 | }, 203 | "response": [] 204 | } 205 | ] 206 | }, 207 | { 208 | "name": "User", 209 | "item": [ 210 | { 211 | "name": "Profile", 212 | "event": [ 213 | { 214 | "listen": "test", 215 | "script": { 216 | "exec": [ 217 | "const res = pm.response.json()\r", 218 | "pm.test(\"should return User Profile when called with auth token\",()=>{\r", 219 | " pm.expect(res.error).eq(undefined);\r", 220 | " pm.expect(res.user).not.eq(undefined);\r", 221 | " pm.expect(pm.response.code).eq(200)\r", 222 | "})" 223 | ], 224 | "type": "text/javascript" 225 | } 226 | } 227 | ], 228 | "request": { 229 | "method": "GET", 230 | "header": [ 231 | { 232 | "key": "Authorization", 233 | "value": "{{auth_token}}", 234 | "type": "text" 235 | } 236 | ], 237 | "url": { 238 | "raw": "{{base_url}}/users/me", 239 | "host": [ 240 | "{{base_url}}" 241 | ], 242 | "path": [ 243 | "users", 244 | "me" 245 | ] 246 | } 247 | }, 248 | "response": [] 249 | } 250 | ] 251 | }, 252 | { 253 | "name": "Project", 254 | "item": [ 255 | { 256 | "name": "GitLab Project Token", 257 | "request": { 258 | "method": "POST", 259 | "header": [ 260 | { 261 | "key": "Authorization", 262 | "value": "{{auth_token}}", 263 | "type": "text" 264 | } 265 | ], 266 | "url": { 267 | "raw": "{{base_url}}/project/gitlab/278964", 268 | "host": [ 269 | "{{base_url}}" 270 | ], 271 | "path": [ 272 | "project", 273 | "gitlab", 274 | "278964" 275 | ] 276 | } 277 | }, 278 | "response": [] 279 | }, 280 | { 281 | "name": "GitHub Project Token", 282 | "request": { 283 | "method": "POST", 284 | "header": [ 285 | { 286 | "key": "Authorization", 287 | "value": "{{auth_token}}", 288 | "type": "text" 289 | } 290 | ], 291 | "url": { 292 | "raw": "{{base_url}}/project/github/open-certs/oc-backend", 293 | "host": [ 294 | "{{base_url}}" 295 | ], 296 | "path": [ 297 | "project", 298 | "github", 299 | "open-certs", 300 | "oc-backend" 301 | ] 302 | } 303 | }, 304 | "response": [] 305 | } 306 | ] 307 | } 308 | ] 309 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open-Certs-Backend 2 | 3 | open-certs 4 | 5 | > This includes the backend server for Open-Certs. 6 | 7 | After seeing so many open-source projects being monetized without giving any recognition to contributors, Open-Certs comes with the vision to certify every open-source contribution. 8 | 9 | Open-Certs backend is based on RESTFULL arch. as of now. It mainly takes adavntage of `Node.js`, `MongoDB`, `ExpressJS`, `EJS` (as tampelate engine), etc. to deliver the required services to [Open-Certs-Frontend](https://github.com/open-certs/oc-frontend) 10 | 11 | 12 | ## Setup 13 | 14 | - To get started, install the required node modules: 15 | 16 | ``` 17 | npm install 18 | ``` 19 | - Then copy the sample.env to .env and configure it. 20 | 21 | - Install Postman from [here](https://www.postman.com/downloads/) 22 | 23 | ## Run 24 | Then issue the following command to run the server: 25 | 26 | ``` 27 | npm start 28 | ``` 29 | 30 | Or use the following command to run the server in development mode: 31 | 32 | ``` 33 | npm run dev 34 | ``` 35 | ## Github setup 36 | - You can find detailed explanation in `docs` folder, file name`GITHUB_SETUP.md` 37 | 38 | ## Documentation 39 | - To get the documention of rest apis import `Open Certs.postman_collection.json` into Postman. 40 | 41 | ## Contributing 42 | 43 | Any contributions you make are **greatly appreciated**. 44 | 45 | 1. Create / Choose an issue [here](https://github.com/open-certs/oc-backend/issues). 46 | 2. Get the issue assigned to yourself by commenting. 47 | 2. Fork the Project 48 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 49 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 50 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 51 | 5. Open a Pull Request 52 | 53 | 54 | 55 | ## Contributors: 56 | 57 | ### Credits goes to these people: ✨ 58 | 59 | 60 | 61 | 66 | 67 |
62 | 63 | 64 | 65 |
68 |

69 |

Visitor's Count Visitor Count

70 |

-------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: '.env' }); 2 | require('../src/models/index.model').connect(); 3 | const cluster = require('../src/helpers/cluster.helper').clusterise(); 4 | 5 | if (cluster.clusterised == true) { 6 | if (cluster.isMaster == false) { 7 | require('../src/index'); 8 | } 9 | } else if (cluster.clusterised == false) { 10 | console.log('Starting app in non-cluster mode'); 11 | require('../src/index'); 12 | } 13 | -------------------------------------------------------------------------------- /docs/BITBUCKET_SETUP.md: -------------------------------------------------------------------------------- 1 | # Bitbucket setup 2 | 1. First thing you need to do is to create a **New Workspace**. 3 | 2. Go to the **Settings**(on the left panel of the window) of the above created workspace. 4 | 3. Scroll down to see **OAuth Consumers**. Click on **Add Consumer** button. 5 | 4. It will take you to **Add OAuth Consumer** 6 | - Name : put any name you want or put ours **oc-backend** :wink: 7 | - Callback URL : http://localhost:4000/auth/bitbucket/callback 8 | - URL : http://localhost:4000 9 | - Application description : you do not need to put or put any description if you wish. 10 | - Privacy policy URL and End user license agreement URL can be left blank. 11 | - Permisions 12 | - Account 13 | - Email 14 | - Read 15 | - Projects 16 | - Read 17 | - Repositories 18 | - Read 19 | - Pull Requests 20 | - Read 21 | - Issue 22 | - Read 23 | 5. Click on **Save** button. 24 | 6. After clicking save you will find a row with the Name you specified in the above form. Click on it to view the **Key** and **Secret**. 25 | 26 | ## Setup your `.env` file 27 | - Replace `XX` in `BITBUCKET_CLIENT_ID=XX` to **Key** we got. 28 | - Replace `XX` in `BITBUCKET_CLIENT_SECRET=XX` to **Secret** we got. 29 | -------------------------------------------------------------------------------- /docs/GITHUB_SETUP.md: -------------------------------------------------------------------------------- 1 | # Github setup 2 | 1. First thing you need to go to [developer settings](https://github.com/settings/apps) directly or you can do it manually by : 3 | - In the upper-right corner of your github page, click your profile photo, then click **Settings**. 4 | - Scroll down until you find in the left bar **Developer Settings** 5 | 2. You will find in the left bar three options choose **OAuth apps** 6 | 3. Create new auth app by click on **New OAuth app** 7 | 4. It will take you to **Register a new OAuth application** 8 | - Application name : put any name you want or put ours **oc-backend** :wink: 9 | - Homepage URL : http://localhost:4000 10 | - Application description : you do not need to put or put any description if you wish 11 | - Authorization callback URL : http://localhost:4000/auth/github/callback 12 | - Click on **Register application** 13 | - It will take you to page where you find **Client ID** and you will click on **Generate a new client secret** 14 | ## Setup your `.env` file 15 | - Replace `XX` in `GITHUB_CLIENT_ID=XX` to **Client ID** we got it 16 | - Replace `XX` in `GITHUB_CLIENT_SECRET=XX` to **Client secrets** we generated it 17 | -------------------------------------------------------------------------------- /docs/GITLAB_SETUP.md: -------------------------------------------------------------------------------- 1 | # Gitlab setup 2 | 3 | 1. In the top-right corner, select your avatar (where your photo/ profile is). 4 | 5 | 2. Select Edit profile or use link- https://gitlab.com/-/profile. 6 | 7 | 3. On the left sidebar, select Applications. 8 | 9 | 4. It'll take you to a page to enter some details. 10 | 11 | - Enter a Name (oc-backend or anything you like :)), 12 | - Enter Redirect URI : http://localhost:4000/auth/gitlab/callback 13 | - The Redirect URI is the URL where users are sent after they authorize with GitLab, 14 | - Check Confidential and Expire access token and 15 | - For OAuth 2 scopes select the `read_api` checkbox. 16 | 17 | 5. Select Save application. GitLab provides: 18 | 19 | - The OAuth 2 Client ID in the Application ID field. 20 | - The OAuth 2 Client Secret, accessible using the Copy button on the Secret field 21 | 22 | ## Setup your `.env` file 23 | 24 | - Replace `XX` in `GITLAB_CLIENT_ID=XX` to **Client ID** we got it 25 | - Replace `XX` in `GITLAB_CLIENT_SECRET=XX` to **Client secrets** we generated it 26 | -------------------------------------------------------------------------------- /docs/MONGO_DB_SETUP.md: -------------------------------------------------------------------------------- 1 | 2 | # Mongo_db setup 3 | 4 | 1. First thing you need to go to this url [mongo_db login](https://account.mongodb.com/account/login). 5 | 6 | 7 | 8 | 2. After login (or register in case you don't have account) there will be a page in top of it **Deploy a cloud database** with 3 options (Serverless - Dedicated - Shared) you will choose **Shared** option 9 | 10 | 11 | 12 | 3. Then you will find at the top of the page **create cluster** 13 | 14 | 15 | 4. After finishing you will wait from (1 - 3) minutes for the cluster to be created 16 | 17 | 18 | 19 | 5. Then he will ask you for **username** and **password** put whatever you want 20 | 21 | 22 | 23 | 6. In the ip address field put **0.0.0.0/0** to allow access from anywhere 24 | 25 | 26 | 27 | 7. Second thing he will ask you about **choose connection method** you will choose **connect you application option** 28 | 29 | 30 | 31 | 8. Then he will ask you about the **driver** choose **nodejs** and the **version** **4 or late** 32 | 33 | 34 | 35 | 9. You will find this header **Add your connection string into your application code** you will copy the link in this header and paste it into our **MONGO_DB_URL** variable in your **.env** file 36 | 37 | 38 | 39 | 10. You should replace ~~ in the url you got with the password you have chosen in the creation when you created the user (**password you choose in step 4**) 40 | 41 | 42 | 43 | 11. You will also change the **myFirstDatabase** in the same url with name of the app you have set it (in case you don't set name for the app it will be by default **Project 0**) 44 | 45 | 46 | 47 | 12. You have finished it :clap::clap::clap: 48 | 49 | 50 | 51 | ## Access collections 52 | 53 | - Last thing if you want to see the collections of the database, after opening the mongodb website and choose the project name you are currently working on from the list in the top left corner, you will find the cluster opens, then choose **Browse Collections** -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oc-backend", 3 | "version": "0.0.1", 4 | "private": true, 5 | "main": "./bin/www", 6 | "engines": { 7 | "node": ">=10.0.0 <17.0.0" 8 | }, 9 | "scripts": { 10 | "start": "node ./bin/www", 11 | "dev": "nodemon ./bin/www", 12 | "test": "jest test/", 13 | "format": "eslint . --fix", 14 | "checkFormat": "eslint .", 15 | "quality": "npm run checkFormat && npm run test -- --silent", 16 | "prepare": "husky install" 17 | }, 18 | "dependencies": { 19 | "@gitbeaker/node": "^35.6.0", 20 | "@octokit/rest": "^18.12.0", 21 | "axios": "^0.26.1", 22 | "bitbucket": "^2.7.0", 23 | "cookie-parser": "~1.4.4", 24 | "cors": "^2.8.5", 25 | "debug": "~2.6.9", 26 | "dotenv": "^16.0.0", 27 | "ejs": "~3.1.7", 28 | "express": "~4.16.1", 29 | "express-validation": "^3.0.8", 30 | "http-errors": "~1.6.3", 31 | "jsonwebtoken": "^8.5.1", 32 | "mongoose": "^6.2.1", 33 | "morgan": "~1.9.1", 34 | "passport": "^0.6.0", 35 | "passport-bitbucket-oauth2": "^0.1.2", 36 | "passport-github2": "^0.1.12", 37 | "passport-gitlab2": "^5.0.0" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^27.4.1", 41 | "eslint": "^8.8.0", 42 | "eslint-config-prettier": "^8.3.0", 43 | "eslint-plugin-jest": "^26.1.1", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-prettier": "^4.0.0", 46 | "husky": "^7.0.0", 47 | "jest": "^27.5.1", 48 | "newman": "^5.3.2", 49 | "nodemon": "^2.0.15", 50 | "prettier": "^2.5.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | 2 | 3 | MONGO_DB_URI=XX 4 | 5 | PORT=4000 6 | 7 | CLUSTER=YES 8 | SHOW_MONGO=YES 9 | 10 | BASE_URL=http://localhost:4000 11 | FRONTEND_URL=http://localhost:3000 12 | TOKEN_SECRET=XX 13 | 14 | GITHUB_CLIENT_ID=XX 15 | GITHUB_CLIENT_SECRET=XX 16 | 17 | PROJECT_TOKEN_SECRET=XX 18 | 19 | BITBUCKET_CLIENT_ID=XX 20 | BITBUCKET_CLIENT_SECRET=XX 21 | 22 | GITLAB_CLIENT_ID=XX 23 | GITLAB_CLIENT_SECRET=XX 24 | 25 | RECAPTCHA_SECRECT_KEY=XX 26 | VALIDATE_RECAPTCHA=XX 27 | 28 | ENCRYPTION_SECRET_KEY=XX 29 | -------------------------------------------------------------------------------- /sample.test.env: -------------------------------------------------------------------------------- 1 | TEST=YES 2 | 3 | SHOW_MONGO=NO -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const cookieParser = require('cookie-parser'); 4 | const logger = require('morgan'); 5 | const cors = require('cors'); 6 | const router = require('./routes/index.route'); 7 | const { errorHandler } = require('./helpers/errorhandler.helper'); 8 | const NotFoundError = require('./errors/notFound.error'); 9 | 10 | const app = express(); 11 | app.use(cors()); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'ejs'); 16 | 17 | app.use(logger('dev')); 18 | app.use(express.json()); 19 | app.use(express.urlencoded({ extended: false })); 20 | app.use(cookieParser()); 21 | app.use(express.static(path.join(__dirname, '../public'))); 22 | 23 | router(app); 24 | 25 | // catch 404 and forward to error handler 26 | app.use(function (req, res, next) { 27 | next(new NotFoundError('API not found')); 28 | }); 29 | 30 | // error handler 31 | // eslint-disable-next-line no-unused-vars 32 | app.use(errorHandler); 33 | 34 | module.exports = app; 35 | -------------------------------------------------------------------------------- /src/config/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AUTH_TOKEN_EXPIRY_HOURS: '1', 3 | 4 | PROJECT_TOKEN_EXPIRY_HOURS: '1', 5 | 6 | GITHUB_LOGO: { 7 | src: 'https://cdn-icons-png.flaticon.com/512/25/25231.png', 8 | url: 'https://github.com' 9 | }, 10 | 11 | GITLAB_LOGO: { 12 | src: 'https://cdn-icons-png.flaticon.com/512/5968/5968853.png', 13 | url: 'https://gitlab.com' 14 | }, 15 | 16 | BITBUCKET_LOGO: { 17 | src: 'https://cdn-icons-png.flaticon.com/512/6125/6125001.png', 18 | url: 'https://bitbucket.com' 19 | }, 20 | 21 | GITHUB_SCOPES: [ 22 | 'public_repo', 23 | 'read:user', 24 | 'user:email', 25 | 'read:org', 26 | 'repo' 27 | ], 28 | 29 | BITBUCKET_SCOPES: [ 30 | 'repository', 31 | 'account', 32 | 'email', 33 | 'issue', 34 | 'pullrequest' 35 | ], 36 | 37 | GITLAB_SCOPES: ['read_api'], 38 | 39 | reputationWeight: { 40 | closedIssues: 6, 41 | stars: 9, 42 | forks: 7, 43 | openIssues: 4, 44 | license: 10, 45 | pullRequests: 6, 46 | contributors: 10, 47 | subscribers: 0 48 | }, 49 | 50 | thresholdWeight: 250, 51 | 52 | categories: { 53 | bronze: 100, 54 | silver: 750, 55 | gold: 5000 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const { encrypt } = require('../helpers/crypto.helper'); 2 | const { sign } = require('../helpers/jwt.helper'); 3 | 4 | exports.login = (req, res, next) => { 5 | try { 6 | const user = req.user; 7 | user.accessToken = encrypt(user.accessToken); 8 | const token = sign(user); 9 | delete user.accessToken; 10 | return res.redirect( 11 | `${process.env.FRONTEND_URL}/recievedToken.html?token=${token}` 12 | ); 13 | // return res.status(200).json({ 14 | // user, 15 | // token 16 | // }); 17 | } catch (e) { 18 | next(e); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/controllers/certificate.controller.js: -------------------------------------------------------------------------------- 1 | const github = require('../helpers/github.helper'); 2 | const gitlab = require('../helpers/gitlab.helper'); 3 | const bitbuket = require('../helpers/bitbucket.helper'); 4 | const ejs = require('ejs'); 5 | const path = require('path'); 6 | const Certificate = require('../models/certificate.model'); 7 | const NotFoundError = require('../errors/notFound.error'); 8 | const CustomError = require('../errors/custom.error'); 9 | const getLastContributionDate = (latestCommit, latestPullRequest) => { 10 | if (!latestCommit) { 11 | return latestPullRequest; 12 | } 13 | if (!latestPullRequest) { 14 | return latestCommit; 15 | } 16 | latestCommit = new Date(latestCommit); 17 | latestPullRequest = new Date(latestPullRequest); 18 | return latestCommit > latestPullRequest ? latestCommit : latestPullRequest; 19 | }; 20 | 21 | const getGitHubData = async (user, project) => { 22 | const [commits, pullRequests] = await Promise.all([ 23 | github.getMyCommits(user.accessToken, project.owner, project.name), 24 | github.getMyPullRequests(user.accessToken, project.owner, project.name) 25 | ]); 26 | const commitCount = commits.data.total_count; 27 | const pullRequestCount = pullRequests.data.total_count; 28 | if (commitCount === 0 && pullRequestCount === 0) { 29 | throw new CustomError('No commits found by user'); 30 | } 31 | const lastContributionDate = getLastContributionDate( 32 | commits.data.items[0]?.commit?.committer?.date, 33 | pullRequests.data.items[0]?.created_at 34 | ); 35 | return { commitCount, pullRequestCount, lastContributionDate }; 36 | }; 37 | 38 | const getGitLabData = async (user, project) => { 39 | const [commits, pullRequests] = await Promise.all([ 40 | gitlab.getMyCommits(user.accessToken, project.id, user.email), 41 | gitlab.getMyMergeRequests(user.accessToken, project.id) 42 | ]); 43 | const commitCount = commits.length; 44 | const pullRequestCount = pullRequests.length; 45 | if (commitCount === 0 && pullRequestCount === 0) { 46 | throw new CustomError('No commits found by user'); 47 | } 48 | const lastContributionDate = getLastContributionDate( 49 | commits[0]?.created_at, 50 | pullRequests[0]?.created_at 51 | ); 52 | return { commitCount, pullRequestCount, lastContributionDate }; 53 | }; 54 | 55 | const getBitBucketData = async (user, project) => { 56 | let pullRequests = await bitbuket.getAllMergedPullRequests( 57 | user.accessToken, 58 | 'MERGED', 59 | project.repoSlug, 60 | project.workspaceSlug 61 | ); 62 | pullRequests = pullRequests.data.values; 63 | const pullRequestCount = pullRequests.length; 64 | if (pullRequestCount === 0) { 65 | throw new CustomError('No commits found by user'); 66 | } 67 | const lastContributionDate = pullRequests[0]?.created_on; 68 | return { pullRequestCount, lastContributionDate }; 69 | }; 70 | 71 | const getData = async (user, project) => { 72 | if (user.kind === 'github') return await getGitHubData(user, project); 73 | if (user.kind === 'bitbucket') return await getBitBucketData(user, project); 74 | return await getGitLabData(user, project); 75 | }; 76 | 77 | exports.generateCertificate = async (req, res, next) => { 78 | try { 79 | const user = req.user; 80 | const project = req.project; 81 | if (user.kind !== project.service) { 82 | throw new CustomError( 83 | 'Login Credentials not found for current service!' 84 | ); 85 | } 86 | 87 | const { commitCount, pullRequestCount, lastContributionDate } = 88 | await getData(user, project); 89 | 90 | const images = [project.provider]; 91 | 92 | if (req.body.includeRepositoryImage) { 93 | images.push({ 94 | src: project.ownerAvatar, 95 | url: project.repoLink 96 | }); 97 | } 98 | if (req.body.includeUserImage) { 99 | images.push({ 100 | src: user.avatar, 101 | url: user.profileUrl 102 | }); 103 | } 104 | const certificate = await Certificate.create({ 105 | userGithubId: user.username, 106 | userName: user.name, 107 | projectRepo: project.name, 108 | projectOwner: project.owner, 109 | commitCount, 110 | pullRequestCount, 111 | lastContributionDate, 112 | images 113 | }); 114 | return res.status(200).json({ 115 | certificate, 116 | url: process.env.BASE_URL + '/certificate/' + certificate._id 117 | }); 118 | } catch (e) { 119 | next(e); 120 | } 121 | }; 122 | 123 | exports.getCert = async (req, res, next) => { 124 | try { 125 | const certificate = await Certificate.getById(req.params.id).lean(); 126 | if (!certificate) { 127 | throw new NotFoundError('Invalid Certificate Id'); 128 | } 129 | // console.log({ 130 | // ...certificate, 131 | // verifyAt: `${process.env.BASE_URL}/certificate/${certificate._id}` 132 | // }); 133 | return res.render('certificateHolder', { 134 | html: await ejs.renderFile( 135 | path.join(__dirname, '../views/certificate.ejs'), 136 | { 137 | data: { 138 | ...certificate, 139 | verifyAt: `${process.env.BASE_URL}/certificate/${certificate._id}` 140 | } 141 | } 142 | ) 143 | }); 144 | } catch (e) { 145 | next(e); 146 | } 147 | }; 148 | 149 | exports.getCertDetails = async (req, res, next) => { 150 | try { 151 | const cert_id = req.params.id; 152 | 153 | const certificate = await Certificate.getById(cert_id); 154 | if (!certificate) { 155 | throw new NotFoundError('Invalid Certificate Id'); 156 | } 157 | 158 | res.status(200).json({ 159 | certificate 160 | }); 161 | } catch (e) { 162 | next(e); 163 | } 164 | }; 165 | -------------------------------------------------------------------------------- /src/controllers/project.controller.js: -------------------------------------------------------------------------------- 1 | const github = require('../helpers/github.helper'); 2 | 3 | const gitlab = require('../helpers/gitlab.helper'); 4 | 5 | const bitbuket = require('../helpers/bitbucket.helper'); 6 | 7 | const { sign } = require('../helpers/project.jwt.helper'); 8 | const constants = require('../config/constants'); 9 | const CustomError = require('../errors/custom.error'); 10 | 11 | const calculateReputation = ({ 12 | closedIssues, 13 | stars, 14 | forks, 15 | openIssues, 16 | license, 17 | pullRequests, 18 | contributors, 19 | subscribers 20 | }) => { 21 | let reputation = 0; 22 | const consideredData = { 23 | closedIssues, 24 | stars, 25 | forks, 26 | openIssues, 27 | license: license ? 1 : 0, 28 | pullRequests, 29 | contributors, 30 | subscribers 31 | }; 32 | 33 | Object.keys(consideredData).forEach((key) => { 34 | reputation += 35 | constants.reputationWeight[key] * 36 | Math.min(consideredData[key], 999) || 0; 37 | }); 38 | 39 | return reputation; 40 | }; 41 | 42 | exports.getBitBucketProjectToken = async (req, res, next) => { 43 | try { 44 | const user = req.user; 45 | let repo = await bitbuket.getRepo( 46 | user.accessToken, 47 | req.params.workspace, 48 | req.params.repo 49 | ); 50 | repo = repo.data; 51 | if (!repo) { 52 | throw new CustomError('Repository not found'); 53 | } 54 | 55 | if (repo.is_private) { 56 | throw new CustomError('Private repositories not allowed.'); 57 | } 58 | 59 | if (repo.parent) { 60 | throw new CustomError('Forked repositories not allowed.'); 61 | } 62 | const [ 63 | pullRequests, 64 | issues, 65 | contributors, 66 | permission, 67 | forkList, 68 | watchers 69 | ] = await Promise.all([ 70 | bitbuket.getAllMergedPullRequests( 71 | user.accessToken, 72 | 'MERGED', 73 | req.params.repo, 74 | req.params.workspace 75 | ), 76 | bitbuket.getAllClosedIssues( 77 | user.accessToken, 78 | req.params.repo, 79 | req.params.workspace 80 | ), 81 | bitbuket.getAllContributors(user.accessToken, req.params.workspace), 82 | bitbuket.getRepoPermission( 83 | user.accessToken, 84 | repo.workspace.slug, 85 | req.user.serviceId 86 | ), 87 | bitbuket.getForkList( 88 | user.accessToken, 89 | req.params.repo, 90 | req.params.workspace 91 | ), 92 | bitbuket.getRepoWatchers( 93 | user.accessToken, 94 | req.params.repo, 95 | req.params.workspace 96 | ) 97 | ]); 98 | const accumulatedData = { 99 | name: repo.name, 100 | owner: repo.workspace.name, 101 | ownerAvatar: repo.links.avatar.href, 102 | forks: forkList.data.size, 103 | watchers: watchers.data.size, 104 | ownerLink: repo.workspace.links.html.href, 105 | repoLink: repo.links.html.href, 106 | repoSlug: repo.slug, 107 | workspaceSlug: repo.workspace.slug, 108 | permissions: { maintain: permission }, 109 | pullRequests: pullRequests.data.size, 110 | closedIssues: issues.data.size, 111 | contributors: contributors.data.size, 112 | provider: constants.BITBUCKET_LOGO, 113 | service: 'bitbucket' 114 | }; 115 | 116 | const reputation = calculateReputation(accumulatedData); 117 | // if (reputation < constants.thresholdWeight) 118 | // throw new Error('Repository is not appropriate'); 119 | 120 | let category = null; 121 | Object.keys(constants.categories).forEach((key) => { 122 | if (constants.categories[key] < reputation) { 123 | category = key; 124 | } 125 | }); 126 | 127 | accumulatedData['category'] = category; 128 | accumulatedData['reputation'] = reputation; 129 | accumulatedData['thresholdWeight'] = constants.thresholdWeight; 130 | 131 | const token = sign(accumulatedData); 132 | res.status(200).json({ 133 | accumulatedData, 134 | projectToken: token, 135 | levels: constants.categories 136 | }); 137 | } catch (err) { 138 | console.log(err); 139 | next(err); 140 | } 141 | }; 142 | 143 | exports.getGitLabProjectToken = async (req, res, next) => { 144 | try { 145 | const user = req.user; 146 | const repo = await gitlab.getRepo(user.accessToken, req.params.id); 147 | if (!repo) { 148 | throw new CustomError('Repository not found'); 149 | } 150 | 151 | if (repo.private || repo.visibility !== 'public') { 152 | throw new CustomError('Private repositories not allowed.'); 153 | } 154 | if (repo.archived) { 155 | throw new CustomError('Archived repositories not allowed.'); 156 | } 157 | if (repo.source) { 158 | throw new CustomError('Forked repositories not allowed.'); 159 | } 160 | const [pullRequests, issues, contributors] = await Promise.all([ 161 | gitlab.getAllMergedPullRequests( 162 | user.accessToken, 163 | req.params.id, 164 | req.user.username 165 | ), 166 | gitlab.getAllClosedIssues(user.accessToken, req.params.id), 167 | gitlab.getAllContributors(user.accessToken, req.params.id) 168 | ]); 169 | const accumulatedData = { 170 | name: repo.name, 171 | owner: repo.namespace.name, 172 | id: req.params.id, 173 | ownerAvatar: repo.avatar_url || repo.owner.avatar_url, 174 | ownerLink: repo.namespace.web_url, 175 | repoLink: repo.web_url, 176 | stars: repo.star_count, 177 | forks: repo.forks_count, 178 | openIssues: repo.open_issues_count, 179 | permissions: repo.permissions, 180 | pullRequests: pullRequests ? pullRequests.length : 0, 181 | closedIssues: issues, 182 | contributors: contributors.length, 183 | provider: constants.GITLAB_LOGO, 184 | service: 'gitlab' 185 | }; 186 | 187 | const reputation = calculateReputation(accumulatedData); 188 | // if (reputation < constants.thresholdWeight) 189 | // throw new Error('Repository is not appropriate'); 190 | 191 | let category = null; 192 | Object.keys(constants.categories).forEach((key) => { 193 | if (constants.categories[key] < reputation) { 194 | category = key; 195 | } 196 | }); 197 | 198 | accumulatedData['category'] = category; 199 | accumulatedData['reputation'] = reputation; 200 | accumulatedData['thresholdWeight'] = constants.thresholdWeight; 201 | 202 | const token = sign(accumulatedData); 203 | res.status(200).json({ 204 | accumulatedData, 205 | projectToken: token, 206 | levels: constants.categories 207 | }); 208 | } catch (err) { 209 | next(err); 210 | } 211 | }; 212 | 213 | exports.getGitHubProjectToken = async (req, res, next) => { 214 | try { 215 | const user = req.user; 216 | let repo = await github.getRepo( 217 | user.accessToken, 218 | req.params.owner, 219 | req.params.repo 220 | ); 221 | if (!repo) { 222 | throw new CustomError('Repository not found'); 223 | } 224 | repo = repo.data; 225 | 226 | if (repo.private || repo.visibility !== 'public') { 227 | throw new CustomError('Private repositories not allowed.'); 228 | } 229 | if (repo.archived) { 230 | throw new CustomError('Archived repositories not allowed.'); 231 | } 232 | if (repo.source) { 233 | throw new CustomError('Forked repositories not allowed.'); 234 | } 235 | 236 | const [pullRequests, issues, contributors] = await Promise.all([ 237 | github.getAllMergedPullRequests( 238 | user.accessToken, 239 | req.params.owner, 240 | req.params.repo 241 | ), 242 | github.getAllClosedIssues( 243 | user.accessToken, 244 | req.params.owner, 245 | req.params.repo 246 | ), 247 | github.getAllContributors( 248 | user.accessToken, 249 | req.params.owner, 250 | req.params.repo 251 | ) 252 | ]); 253 | const accumulatedData = { 254 | name: repo.name, 255 | owner: req.params.owner, 256 | ownerAvatar: repo.owner.avatar_url, 257 | ownerLink: repo.owner.html_url, 258 | repoLink: repo.homepage, 259 | stars: repo.stargazers_count, 260 | forks: repo.network_count, 261 | openIssues: repo.open_issues_count, 262 | license: repo.license, 263 | permissions: repo.permissions, 264 | pullRequests: pullRequests.data.total_count, 265 | closedIssues: issues.data.total_count, 266 | contributors: contributors.data.length, 267 | subscribers: repo.subscribers_count, 268 | provider: constants.GITHUB_LOGO, 269 | service: 'github' 270 | }; 271 | const reputation = calculateReputation(accumulatedData); 272 | // if (reputation < constants.thresholdWeight) 273 | // throw new Error('Repository is not appropriate'); 274 | 275 | let category = null; 276 | Object.keys(constants.categories).forEach((key) => { 277 | if (constants.categories[key] < reputation) { 278 | category = key; 279 | } 280 | }); 281 | 282 | accumulatedData['category'] = category; 283 | accumulatedData['reputation'] = reputation; 284 | accumulatedData['thresholdWeight'] = constants.thresholdWeight; 285 | 286 | const token = sign(accumulatedData); 287 | res.status(200).json({ 288 | accumulatedData, 289 | projectToken: token, 290 | levels: constants.categories 291 | }); 292 | } catch (err) { 293 | next(err); 294 | } 295 | }; 296 | -------------------------------------------------------------------------------- /src/controllers/user.controler.js: -------------------------------------------------------------------------------- 1 | exports.profile = (req, res, next) => { 2 | try { 3 | const user = req.user; 4 | delete user._json; 5 | delete user.accessToken; 6 | return res.status(200).json({ 7 | user 8 | }); 9 | } catch (e) { 10 | next(e); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/errors/authentication.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class AuthenticationError extends CustomError { 4 | constructor(message, status = 401) { 5 | super(message, status); 6 | this.name = 'Authentication Error'; 7 | this.type = 'Authentication Error'; 8 | } 9 | getResponse() { 10 | return { ...super.getResponse(), logout: true }; 11 | } 12 | } 13 | module.exports = AuthenticationError; 14 | -------------------------------------------------------------------------------- /src/errors/custom.error.js: -------------------------------------------------------------------------------- 1 | class CustomError extends Error { 2 | constructor(message, status = 200) { 3 | super(message); 4 | this.name = 'Expected error'; 5 | this.status = status; 6 | this.type = 'CustomError'; 7 | } 8 | getResponse() { 9 | return { message: this.message, type: this.type, name: this.name }; 10 | } 11 | handleError(res) { 12 | return res.status(this.status).json({ 13 | error: this.getResponse() 14 | }); 15 | } 16 | } 17 | module.exports = CustomError; 18 | -------------------------------------------------------------------------------- /src/errors/githubAPI.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class GithubAPIError extends CustomError { 4 | constructor(message, status) { 5 | super(message, status); 6 | this.name = 'Github API Error'; 7 | this.type = 'GithubAPIError'; 8 | } 9 | } 10 | module.exports = GithubAPIError; 11 | -------------------------------------------------------------------------------- /src/errors/githubAPITimeout.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class GithubAPITimeoutError extends CustomError { 4 | constructor(message) { 5 | super(message, 503); 6 | this.name = 'Github API Timeout Error'; 7 | this.type = 'GithubAPITimeoutError'; 8 | } 9 | } 10 | module.exports = GithubAPITimeoutError; 11 | -------------------------------------------------------------------------------- /src/errors/notFound.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class NotFoundError extends CustomError { 4 | constructor(message) { 5 | super(message, 404); 6 | this.name = 'Not Found Error'; 7 | this.type = 'NotFoundError'; 8 | } 9 | } 10 | module.exports = NotFoundError; 11 | -------------------------------------------------------------------------------- /src/errors/passport.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class PassportError extends CustomError { 4 | constructor(message, status) { 5 | super(message, status); 6 | this.name = 'Passport Error'; 7 | this.type = 'PassportError'; 8 | } 9 | 10 | handleError(res) { 11 | return res.redirect( 12 | `${process.env.FRONTEND_URL}/recievedToken.html?errorType=${this.type}&errorMessage=${this.message}` 13 | ); 14 | } 15 | } 16 | module.exports = PassportError; 17 | -------------------------------------------------------------------------------- /src/errors/projectToken.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class ProjectTokenError extends CustomError { 4 | constructor(message) { 5 | super(message, 403); 6 | this.name = 'Project token error'; 7 | this.type = 'ProjectTokenError'; 8 | } 9 | } 10 | module.exports = ProjectTokenError; 11 | -------------------------------------------------------------------------------- /src/errors/recaptcha.error.js: -------------------------------------------------------------------------------- 1 | const CustomError = require('./custom.error'); 2 | 3 | class RecaptchaError extends CustomError { 4 | constructor(message) { 5 | super(message, 403); 6 | this.name = 'Recaptcha error'; 7 | this.type = 'RecaptchaError'; 8 | } 9 | } 10 | module.exports = RecaptchaError; 11 | -------------------------------------------------------------------------------- /src/helpers/bitbucket.helper.js: -------------------------------------------------------------------------------- 1 | const { Bitbucket } = require('bitbucket'); 2 | 3 | const getAuth = (token) => { 4 | return new Bitbucket({ 5 | auth: { token } 6 | }); 7 | }; 8 | 9 | const getRepo = async (token, workspace, repo_slug) => { 10 | const bitbucket = getAuth(token); 11 | const data = await bitbucket.repositories.get({ repo_slug, workspace }); 12 | return data; 13 | }; 14 | 15 | const getMyPullRequests = async (token, repo_slug, workspace) => { 16 | const bitbucket = getAuth(token); 17 | const data = await bitbucket.pullrequests.get({ repo_slug, workspace }); 18 | return data; 19 | }; 20 | 21 | const getAllMergedPullRequests = async (token, state, repo_slug, workspace) => { 22 | const bitbucket = getAuth(token); 23 | const data = await bitbucket.pullrequests.list({ 24 | repo_slug, 25 | state, 26 | workspace 27 | }); 28 | return data; 29 | }; 30 | 31 | const getAllClosedIssues = async (token, repo_slug, workspace) => { 32 | const bitbucket = getAuth(token); 33 | const q = `state = "closed"`; 34 | const data = await bitbucket.repositories.listIssues({ 35 | repo_slug, 36 | workspace, 37 | q 38 | }); 39 | return data; 40 | }; 41 | // TODO: Fetch Contributors for public repos 42 | const getAllContributors = async (token, workspace) => { 43 | const bitbucket = getAuth(token); 44 | const data = await bitbucket.workspaces 45 | .getMembersForWorkspace({ 46 | workspace 47 | }) 48 | .catch(() => ({ data: { size: 0 } })); 49 | return data; 50 | }; 51 | 52 | const getForkList = async (token, repo_slug, workspace) => { 53 | const bitbucket = getAuth(token); 54 | const data = await bitbucket.repositories.listForks({ 55 | repo_slug, 56 | workspace 57 | }); 58 | return data; 59 | }; 60 | 61 | const getRepoWatchers = async (token, repo_slug, workspace) => { 62 | const bitbucket = getAuth(token); 63 | const data = await bitbucket.repositories.listWatchers({ 64 | repo_slug, 65 | workspace 66 | }); 67 | return data; 68 | }; 69 | 70 | const getRepoPermission = async (token, workspace, member) => { 71 | const bitbucket = getAuth(token); 72 | const data = await bitbucket.workspaces 73 | .getMemberForWorkspace({ 74 | member, 75 | workspace 76 | }) 77 | .then(() => true) 78 | .catch(() => false); 79 | return data; 80 | }; 81 | 82 | module.exports = { 83 | getRepo, 84 | getMyPullRequests, 85 | getAllMergedPullRequests, 86 | getAllClosedIssues, 87 | getAllContributors, 88 | getForkList, 89 | getRepoWatchers, 90 | getRepoPermission 91 | }; 92 | -------------------------------------------------------------------------------- /src/helpers/cluster.helper.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster'); 2 | const numCPUs = require('os').cpus().length; 3 | 4 | exports.clusterise = () => { 5 | const status = { clusterised: false, isMaster: cluster.isMaster }; 6 | if (process.env.CLUSTER == 'YES') { 7 | status.clusterised = true; 8 | if (cluster.isMaster) { 9 | console.log( 10 | 'Starting app in cluster mode with ' + numCPUs + ' workers' 11 | ); 12 | console.log(`Master is running with pid ${process.pid}`); 13 | for (let i = 0; i < numCPUs; i++) { 14 | cluster.fork(); 15 | } 16 | 17 | cluster.on('exit', (worker, code, signal) => { 18 | console.log( 19 | `Worker ${process.pid} died with code: ${code} and signal: ${signal}` 20 | ); 21 | console.log('Starting new worker'); 22 | cluster.fork(); 23 | }); 24 | } 25 | } 26 | return status; 27 | }; 28 | -------------------------------------------------------------------------------- /src/helpers/crypto.helper.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const AuthenticationError = require('../errors/authentication.error'); 3 | 4 | const hash = (plainText) => { 5 | const hashedText = crypto 6 | .createHash('sha256') 7 | .update(String(plainText)) 8 | .digest('base64'); 9 | return hashedText; 10 | }; 11 | 12 | const algorithm = 'aes-256-ctr'; 13 | const secretKey = hash(process.env.ENCRYPTION_SECRET_KEY).substring(0, 32); 14 | 15 | const encrypt = (text) => { 16 | const hashedText = hash(text); 17 | const dataWord = text + '|' + hashedText; 18 | const iv = crypto.randomBytes(16); 19 | 20 | const cipher = crypto.createCipheriv(algorithm, secretKey, iv); 21 | 22 | const encrypted = Buffer.concat([cipher.update(dataWord), cipher.final()]); 23 | 24 | return encrypted.toString('hex') + '|' + iv.toString('hex'); 25 | }; 26 | 27 | const decrypt = (cipherText) => { 28 | const data = cipherText.split('|'); 29 | try { 30 | if (data.length !== 2) { 31 | throw new AuthenticationError('Invalid token'); 32 | } 33 | const decipher = crypto.createDecipheriv( 34 | algorithm, 35 | secretKey, 36 | Buffer.from(data[1], 'hex') 37 | ); 38 | 39 | const decrpyted = Buffer.concat([ 40 | decipher.update(Buffer.from(data[0], 'hex')), 41 | decipher.final() 42 | ]); 43 | 44 | const decryptedString = decrpyted.toString(); 45 | 46 | const dataWord = decryptedString.split('|'); 47 | const newHash = hash(dataWord[0]); 48 | if (newHash === dataWord[1]) { 49 | return dataWord[0]; 50 | } else { 51 | throw new AuthenticationError('Decryption of access token failed'); 52 | } 53 | } catch (e) { 54 | if (e instanceof AuthenticationError) { 55 | throw e; 56 | } 57 | console.log(e); 58 | throw new AuthenticationError('Invalid token'); 59 | } 60 | }; 61 | 62 | module.exports = { 63 | encrypt, 64 | decrypt 65 | }; 66 | -------------------------------------------------------------------------------- /src/helpers/errorhandler.helper.js: -------------------------------------------------------------------------------- 1 | const { ValidationError } = require('express-validation'); 2 | const CustomError = require('../errors/custom.error'); 3 | 4 | // eslint-disable-next-line prettier/prettier, no-unused-vars 5 | exports.errorHandler = (err, req, res, _) => { 6 | if (err instanceof CustomError) { 7 | return err.handleError(res); 8 | } 9 | if (err instanceof ValidationError) { 10 | return res.status(400).json({ 11 | error: { 12 | message: 'Bad request', 13 | type: 'ValidationError', 14 | name: 'Validation Error', 15 | details: err.details 16 | } 17 | }); 18 | } 19 | const isDevelopment = req.app.get('env') === 'development'; 20 | return res.status(500).json({ 21 | error: { 22 | message: isDevelopment ? String(err) : 'Something went wrong', 23 | type: 'Error', 24 | name: 'Unexpected error', 25 | trace: isDevelopment ? err.stack : undefined 26 | } 27 | }); 28 | // set locals, only providing error in development 29 | // res.locals.message = err.message; 30 | // res.locals.error = req.app.get('env') === 'development' ? err : {}; 31 | 32 | // // render the error page 33 | // res.status(err.status || 500); 34 | // res.render('error'); 35 | }; 36 | -------------------------------------------------------------------------------- /src/helpers/github.helper.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | const GithubAPIError = require('../errors/githubAPI.error'); 3 | const GithubAPITimeoutError = require('../errors/githubAPITimeout.error'); 4 | 5 | const getKit = (auth) => { 6 | return new Octokit({ 7 | auth 8 | }); 9 | }; 10 | 11 | const formatGithubError = (error) => { 12 | throw new GithubAPIError(error.response.data.message, error.status); 13 | }; 14 | 15 | const checkStatus202 = (response) => { 16 | if (response?.status === 202) { 17 | throw new GithubAPITimeoutError( 18 | 'This Operation is Currently Unavailable, Please try after a few seconds.' 19 | ); 20 | } 21 | return response; 22 | }; 23 | 24 | const getRepo = (auth, owner, repo) => { 25 | const kit = getKit(auth); 26 | return kit.rest.repos 27 | .get({ 28 | owner, 29 | repo 30 | }) 31 | .catch(formatGithubError) 32 | .then(checkStatus202); 33 | }; 34 | 35 | const getMyCommits = (auth, owner, repo) => { 36 | const kit = getKit(auth); 37 | const q = `author:@me repo:${owner}/${repo} sort:committer-date-desc`; 38 | // console.log(q); 39 | return kit.rest.search 40 | .commits({ 41 | q 42 | }) 43 | .catch(formatGithubError) 44 | .then(checkStatus202); 45 | }; 46 | 47 | const getMyPullRequests = (auth, owner, repo) => { 48 | const kit = getKit(auth); 49 | const q = `is:merged is:pr author:@me repo:${owner}/${repo} sort:created-desc`; 50 | // console.log(q); 51 | return kit.rest.search 52 | .issuesAndPullRequests({ 53 | q 54 | }) 55 | .catch(formatGithubError) 56 | .then(checkStatus202); 57 | }; 58 | 59 | const getAllMergedPullRequests = (auth, owner, repo) => { 60 | const kit = getKit(auth); 61 | const q = `is:merged is:pr -author:@me repo:${owner}/${repo} is:closed`; 62 | // console.log(q); 63 | return kit.rest.search 64 | .issuesAndPullRequests({ 65 | q 66 | }) 67 | .catch(formatGithubError) 68 | .then(checkStatus202); 69 | }; 70 | 71 | const getAllClosedIssues = (auth, owner, repo) => { 72 | const kit = getKit(auth); 73 | const q = `is:closed is:issue repo:${owner}/${repo} linked:pr`; 74 | return kit.rest.search 75 | .issuesAndPullRequests({ 76 | q 77 | }) 78 | .catch(formatGithubError) 79 | .then(checkStatus202); 80 | }; 81 | 82 | const getAllContributors = (auth, owner, repo) => { 83 | const kit = getKit(auth); 84 | return kit.rest.repos 85 | .getContributorsStats({ 86 | owner, 87 | repo 88 | }) 89 | .catch(formatGithubError) 90 | .then(checkStatus202); 91 | }; 92 | 93 | module.exports = { 94 | getRepo, 95 | getMyCommits, 96 | getMyPullRequests, 97 | getAllMergedPullRequests, 98 | getAllClosedIssues, 99 | getAllContributors 100 | }; 101 | -------------------------------------------------------------------------------- /src/helpers/gitlab.helper.js: -------------------------------------------------------------------------------- 1 | const { Gitlab } = require('@gitbeaker/node'); 2 | 3 | const getGitlabApi = (token) => { 4 | return new Gitlab({ 5 | oauthToken: token 6 | }); 7 | }; 8 | 9 | const getRepo = (token, id) => { 10 | const api = getGitlabApi(token); 11 | return api.Projects.show(id); 12 | }; 13 | 14 | const getMyCommits = async (token, id, userEmail) => { 15 | const api = getGitlabApi(token); 16 | const commits = await api.Commits.all(id); 17 | return commits 18 | .filter((commit) => commit.author_email == userEmail) 19 | .sort((a, b) => b.created_at - a.created_at); 20 | }; 21 | 22 | const getMyMergeRequests = (token, id) => { 23 | const api = getGitlabApi(token); 24 | return api.MergeRequests.all({ 25 | projectId: id, 26 | scope: 'created_by_me', 27 | orderBy: 'created_at', 28 | sort: 'desc', 29 | state: 'merged', 30 | page: 1, 31 | perPage: 1000 32 | }); 33 | }; 34 | 35 | const getAllMergedPullRequests = (token, id, userName) => { 36 | const api = getGitlabApi(token); 37 | return api.MergeRequests.all({ 38 | projectId: id, 39 | not: { authorUsername: userName }, 40 | scope: 'all', 41 | state: 'merged', 42 | page: 1, 43 | perPage: 1000 44 | }); 45 | }; 46 | 47 | const getAllClosedIssues = async (token, id) => { 48 | const api = getGitlabApi(token); 49 | const issueStatistics = await api.IssuesStatistics.all({ 50 | projectId: id, 51 | page: 1, 52 | perPage: 1000 53 | }); 54 | return issueStatistics.statistics.counts.closed; 55 | }; 56 | 57 | const getAllContributors = async (token, id) => { 58 | const api = getGitlabApi(token); 59 | return api.Repositories.contributors(id); 60 | }; 61 | 62 | module.exports = { 63 | getRepo, 64 | getMyCommits, 65 | getMyMergeRequests, 66 | getAllMergedPullRequests, 67 | getAllClosedIssues, 68 | getAllContributors 69 | }; 70 | -------------------------------------------------------------------------------- /src/helpers/jwt.helper.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const configConsts = require('../config/constants'); 3 | const AuthenticationError = require('../errors/authentication.error'); 4 | const { decrypt } = require('./crypto.helper'); 5 | 6 | exports.sign = (payload) => { 7 | return jwt.sign(payload, process.env.TOKEN_SECRET, { 8 | expiresIn: configConsts.AUTH_TOKEN_EXPIRY_HOURS + 'h' 9 | }); 10 | }; 11 | 12 | exports.verify = (token) => 13 | new Promise((resolve, reject) => { 14 | jwt.verify(token, process.env.TOKEN_SECRET, function (err, decoded) { 15 | if (err) { 16 | return reject(err.message); 17 | } else { 18 | resolve(decoded); 19 | } 20 | }); 21 | }); 22 | 23 | exports.validate = (req, res, next) => { 24 | const token = req.headers['authorization']; 25 | // console.log(token); 26 | 27 | if (token) { 28 | exports 29 | .verify(token) 30 | .then((user) => { 31 | user.accessToken = decrypt(user.accessToken); 32 | req.user = user; 33 | next(); 34 | }) 35 | .catch((err) => { 36 | // console.log(err); 37 | // return res.status(200).json({ 38 | // error: err, 39 | // logout: true 40 | // }); 41 | //next(new CustomError(String(err))); 42 | next(new AuthenticationError(String(err))); 43 | }); 44 | } else { 45 | // return res.status(200).json({ 46 | // error: 'No token supplied', 47 | // logout: true 48 | // }); 49 | //next(new CustomError('No token supplied')); 50 | next(new AuthenticationError('No Token Supplied')); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/helpers/objectId.helper.js: -------------------------------------------------------------------------------- 1 | const { Types } = require('mongoose'); 2 | 3 | exports.validObjectId = (id) => { 4 | try { 5 | const objId = new Types.ObjectId(id); 6 | return Types.ObjectId.isValid(id) && String(id) == String(objId); 7 | } catch (e) { 8 | return false; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/helpers/passport.helper.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const GitHubStrategy = require('passport-github2'); 3 | const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy; 4 | const GitlabStrategy = require('passport-gitlab2'); 5 | const PassportError = require('../errors/passport.error'); 6 | 7 | passport.serializeUser(function (user, done) { 8 | done(null, user); 9 | }); 10 | 11 | passport.deserializeUser(function (user, done) { 12 | done(null, user); 13 | }); 14 | 15 | passport.use( 16 | new GitHubStrategy( 17 | { 18 | clientID: process.env.GITHUB_CLIENT_ID, 19 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 20 | callbackURL: `${process.env.BASE_URL}/auth/github/callback` 21 | }, 22 | function (accessToken, refreshToken, profile, done) { 23 | done(null, { 24 | accessToken, 25 | email: profile.email, 26 | name: profile.displayName, 27 | username: profile.username, 28 | profileUrl: profile.profileUrl, 29 | avatar: profile.photos[0].value, 30 | serviceId: profile.id, 31 | kind: 'github' 32 | }); 33 | } 34 | ) 35 | ); 36 | 37 | passport.use( 38 | new BitbucketStrategy( 39 | { 40 | callbackURL: `${process.env.BASE_URL}/auth/bitbucket/callback`, 41 | clientID: process.env.BITBUCKET_CLIENT_ID, 42 | clientSecret: process.env.BITBUCKET_CLIENT_SECRET, 43 | profileWithEmail: true, 44 | apiVersion: '2.0' 45 | }, 46 | function (accessToken, refreshToken, profile, done) { 47 | // console.log(profile); 48 | done(null, { 49 | accessToken, 50 | email: '', 51 | name: profile.displayName, 52 | username: profile.username, 53 | profileUrl: profile.profileUrl, 54 | avatar: profile._json.links.avatar.href, 55 | serviceId: profile.id, 56 | kind: 'bitbucket' 57 | }); 58 | } 59 | ) 60 | ); 61 | 62 | passport.use( 63 | new GitlabStrategy( 64 | { 65 | clientID: process.env.GITLAB_CLIENT_ID, 66 | clientSecret: process.env.GITLAB_CLIENT_SECRET, 67 | callbackURL: `${process.env.BASE_URL}/auth/gitlab/callback` 68 | }, 69 | function (accessToken, refreshToken, profile, done) { 70 | done(null, { 71 | accessToken, 72 | email: profile.emails[0].value, 73 | name: profile.displayName, 74 | username: profile.username, 75 | profileUrl: profile.profileUrl, 76 | avatar: profile.avatarUrl, 77 | serviceId: profile.id, 78 | kind: 'gitlab' 79 | }); 80 | } 81 | ) 82 | ); 83 | 84 | passport.errorFormatter = (err, _req, _res, next) => { 85 | next(new PassportError(err.message, err.status)); 86 | }; 87 | 88 | module.exports = passport; 89 | -------------------------------------------------------------------------------- /src/helpers/project.jwt.helper.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const configConsts = require('../config/constants'); 3 | const ProjectTokenError = require('../errors/projectToken.error'); 4 | 5 | exports.sign = (payload) => { 6 | return jwt.sign(payload, process.env.PROJECT_TOKEN_SECRET, { 7 | expiresIn: configConsts.PROJECT_TOKEN_EXPIRY_HOURS + 'h' 8 | }); 9 | }; 10 | 11 | exports.verify = (token) => 12 | new Promise((resolve, reject) => { 13 | jwt.verify( 14 | token, 15 | process.env.PROJECT_TOKEN_SECRET, 16 | function (err, decoded) { 17 | if (err) { 18 | return reject(err.message); 19 | } else { 20 | resolve(decoded); 21 | } 22 | } 23 | ); 24 | }); 25 | 26 | exports.validateProject = (req, res, next) => { 27 | const token = req.headers['project-token']; 28 | // console.log(token); 29 | 30 | if (token) { 31 | exports 32 | .verify(token) 33 | .then((project) => { 34 | req.project = project; 35 | next(); 36 | }) 37 | .catch((err) => { 38 | next(new ProjectTokenError(String(err))); 39 | }); 40 | } else { 41 | next(new ProjectTokenError('No project token supplied')); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/helpers/recaptcha.helper.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const CustomError = require('../errors/custom.error'); 3 | const RecaptchaError = require('../errors/recaptcha.error'); 4 | 5 | exports.validateReCaptcha = (req, res, next) => { 6 | const token = req.headers['recaptcha']; 7 | if (process.env.VALIDATE_RECAPTCHA == 'NO') { 8 | return next(); 9 | } 10 | if (!token) { 11 | // return res.json({ error: 'recaptcha token not found!' }); 12 | return next(new RecaptchaError('Recaptcha token not provided!')); 13 | } 14 | 15 | // verify url 16 | const verifyUrl = `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA_SECRECT_KEY}&response=${token}`; 17 | 18 | // making request to verify url 19 | axios 20 | .post(verifyUrl) 21 | .then((body) => { 22 | // err 23 | if (!body.data.success) { 24 | // return res.json({ 25 | // error: 'captcha verification failed!' 26 | // }); 27 | return next( 28 | new RecaptchaError('Recaptcha verification failed!') 29 | ); 30 | } 31 | // success 32 | next(); 33 | }) 34 | .catch((err) => { 35 | // return res.json({ 36 | // error: 'captcha verification failed!' 37 | // }); 38 | console.log(err); 39 | return next(new CustomError('Captcha verification failed!')); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/helpers/user.helper.js: -------------------------------------------------------------------------------- 1 | const AuthenticationError = require('../errors/authentication.error'); 2 | 3 | exports.checkUser = (type) => { 4 | return (req, res, next) => { 5 | if (req.user.kind !== type) { 6 | return next( 7 | new AuthenticationError( 8 | 'Login Credentials not found for current service!' 9 | ) 10 | ); 11 | } 12 | return next(); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const http = require('http'); 3 | 4 | /** 5 | * Get port from environment and store in Express. 6 | */ 7 | 8 | const port = normalizePort(process.env.PORT || '4000'); 9 | app.set('port', port); 10 | 11 | /** 12 | * Create HTTP Server 13 | */ 14 | 15 | const server = http.createServer(app); 16 | 17 | /** 18 | * Listen on provided port , on all network devices 19 | */ 20 | 21 | server.listen(port); 22 | server.on('error', onError); 23 | server.on('listening', onListening); 24 | 25 | /** 26 | * Normalize port into a number, string or false 27 | */ 28 | 29 | function normalizePort(val) { 30 | const port = parseInt(val, 10); 31 | 32 | if (isNaN(port)) { 33 | // named pipe 34 | return val; 35 | } 36 | 37 | if (port >= 0) { 38 | // port number 39 | return port; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | /** 46 | * Event listener for HTTP server "error" event 47 | */ 48 | 49 | function onError(error) { 50 | if (error.syscall !== 'listen') { 51 | throw error; 52 | } 53 | 54 | const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 55 | 56 | // handle specific listen errors with friendly messages 57 | switch (error.code) { 58 | case 'EACCES': 59 | console.error(bind + ' requires elevated privileges'); 60 | throw new Error(bind + ' requires elevated privileges'); 61 | case 'EADDRINUSE': 62 | console.error(bind + ' is already in use'); 63 | throw new Error(bind + ' is already in use'); 64 | default: 65 | throw error; 66 | } 67 | } 68 | 69 | /** 70 | * Event listener for HTTP server "listening" event 71 | */ 72 | 73 | function onListening() { 74 | const addr = server.address(); 75 | const bind = 76 | typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 77 | console.log('Listening on ' + bind); 78 | } 79 | -------------------------------------------------------------------------------- /src/models/certificate.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const CertificateImageSchema = new mongoose.Schema({ 3 | src: { type: String }, 4 | url: { type: String } 5 | }); 6 | 7 | const CertificateSchema = new mongoose.Schema( 8 | { 9 | userGithubId: { type: String }, 10 | userName: { type: String }, 11 | projectRepo: { type: String }, 12 | projectOwner: { type: String }, 13 | commitCount: { type: Number, default: 0 }, 14 | pullRequestCount: { type: Number, default: 0 }, 15 | lastContributionDate: { type: Date }, 16 | images: { 17 | type: [CertificateImageSchema], 18 | default: [] 19 | } 20 | }, 21 | { timestamps: true }, 22 | { collection: 'certificates' } 23 | ); 24 | 25 | CertificateSchema.statics.getById = (_id) => { 26 | return Certificate.findOne({ _id }); 27 | }; 28 | 29 | const Certificate = mongoose.model('certificates', CertificateSchema); 30 | module.exports = Certificate; 31 | -------------------------------------------------------------------------------- /src/models/index.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | mongoose.Promise = Promise; 3 | exports.connect = function () { 4 | if (process.env.SHOW_MONGO == 'YES') mongoose.set('debug', true); 5 | mongoose.connection.on('error', (e) => { 6 | console.log( 7 | 'MongoDB connection error. Make sure MongoDB is up and running' 8 | ); 9 | throw e; 10 | }); 11 | 12 | return mongoose 13 | .connect(process.env.MONGO_DB_URI, { 14 | useNewUrlParser: true, 15 | useUnifiedTopology: true 16 | }) 17 | .then(() => { 18 | console.log('Connected to MongoDB'); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/routes/auth.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { GITHUB_SCOPES } = require('../config/constants'); 3 | const { GITLAB_SCOPES } = require('../config/constants'); 4 | const { BITBUCKET_SCOPES } = require('../config/constants'); 5 | const { login } = require('../controllers/auth.controller'); 6 | const router = express.Router(); 7 | const passport = require('../helpers/passport.helper'); 8 | 9 | /* GET users github. */ 10 | router.get( 11 | '/github/', 12 | passport.authenticate('github', { 13 | scope: GITHUB_SCOPES, 14 | session: false 15 | }) 16 | ); 17 | 18 | router.get( 19 | '/github/callback', 20 | passport.authenticate('github', { 21 | scope: GITHUB_SCOPES, 22 | failureRedirect: 'http://localhost:3000/', 23 | session: false 24 | }), 25 | login 26 | ); 27 | 28 | /* GET users bitbucket */ 29 | router.get( 30 | '/bitbucket', 31 | passport.authenticate('bitbucket', { 32 | scope: BITBUCKET_SCOPES, 33 | session: false 34 | }) 35 | ); 36 | 37 | router.get( 38 | '/bitbucket/callback', 39 | passport.authenticate('bitbucket', { 40 | scope: BITBUCKET_SCOPES, 41 | session: false 42 | }), 43 | login 44 | ); 45 | 46 | /* GET users Gitlab */ 47 | router.get( 48 | '/gitlab', 49 | passport.authenticate('gitlab', { 50 | scope: GITLAB_SCOPES.join(' '), 51 | session: false 52 | }) 53 | ); 54 | 55 | router.get( 56 | '/gitlab/callback', 57 | passport.authenticate('gitlab', { 58 | scope: GITLAB_SCOPES.join(' '), 59 | session: false 60 | }), 61 | login 62 | ); 63 | 64 | router.use(passport.errorFormatter); 65 | 66 | const authRouter = (app) => { 67 | app.use('/auth', router); 68 | }; 69 | module.exports = authRouter; 70 | -------------------------------------------------------------------------------- /src/routes/certificate.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | generateCertificate, 4 | getCert, 5 | getCertDetails 6 | } = require('../controllers/certificate.controller'); 7 | const certificateValidation = require('../validations/certificate.validation'); 8 | const { validate } = require('../helpers/jwt.helper'); 9 | const { validateReCaptcha } = require('../helpers/recaptcha.helper'); 10 | const router = express.Router(); 11 | const { validateProject } = require('../helpers/project.jwt.helper'); 12 | 13 | const { certIdValidate } = require('../validations/certificate.validation'); 14 | 15 | router.post( 16 | '/', 17 | validate, 18 | validateProject, 19 | validateReCaptcha, 20 | certificateValidation.create, 21 | generateCertificate 22 | ); 23 | router.get('/:id', getCert); 24 | 25 | router.get('/certDetails/:id', certIdValidate, getCertDetails); 26 | 27 | const certificateRouter = (app) => { 28 | app.use('/certificate', router); 29 | }; 30 | 31 | module.exports = certificateRouter; 32 | -------------------------------------------------------------------------------- /src/routes/index.route.js: -------------------------------------------------------------------------------- 1 | const authRouter = require('./auth.route'); 2 | const certificateRouter = require('./certificate.route'); 3 | const projectRouter = require('./project.route'); 4 | const userRouter = require('./users.route'); 5 | const router = (app) => { 6 | userRouter(app); 7 | authRouter(app); 8 | certificateRouter(app); 9 | projectRouter(app); 10 | }; 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /src/routes/project.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { 4 | getGitHubProjectToken, 5 | getGitLabProjectToken, 6 | getBitBucketProjectToken 7 | } = require('./../controllers/project.controller'); 8 | const { validate } = require('../helpers/jwt.helper'); 9 | const { 10 | githubProjectValidation, 11 | gitlabProjectValidation, 12 | bitBucketProjectValidation 13 | } = require('../validations/project.validation'); 14 | const { checkUser } = require('../helpers/user.helper'); 15 | 16 | router.post( 17 | '/github/:owner/:repo', 18 | validate, 19 | githubProjectValidation, 20 | checkUser('github'), 21 | getGitHubProjectToken 22 | ); 23 | 24 | router.post( 25 | '/gitlab/:id', 26 | validate, 27 | gitlabProjectValidation, 28 | checkUser('gitlab'), 29 | getGitLabProjectToken 30 | ); 31 | 32 | router.post( 33 | '/bitbucket/:workspace/:repo', 34 | validate, 35 | bitBucketProjectValidation, 36 | checkUser('bitbucket'), 37 | getBitBucketProjectToken 38 | ); 39 | 40 | const projectRouter = (app) => { 41 | app.use('/project', router); 42 | }; 43 | 44 | module.exports = projectRouter; 45 | -------------------------------------------------------------------------------- /src/routes/users.route.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { profile } = require('../controllers/user.controler'); 3 | const { validate } = require('../helpers/jwt.helper'); 4 | const router = express.Router(); 5 | 6 | router.get('/me', validate, profile); 7 | 8 | const userRouter = (app) => { 9 | app.use('/users', router); 10 | }; 11 | 12 | module.exports = userRouter; 13 | -------------------------------------------------------------------------------- /src/validations/certificate.validation.js: -------------------------------------------------------------------------------- 1 | const { validate, Joi } = require('express-validation'); 2 | const { validObjectId } = require('../helpers/objectId.helper'); 3 | 4 | const create = { 5 | body: Joi.object({ 6 | includeRepositoryImage: Joi.boolean().strict(), 7 | includeUserImage: Joi.boolean().strict() 8 | }) 9 | }; 10 | 11 | const objectId = Joi.string().custom((value, helper) => { 12 | if (!validObjectId(value)) { 13 | return helper.message('Object id is invalid'); 14 | } else { 15 | return true; 16 | } 17 | }); 18 | 19 | exports.create = validate(create, {}, {}); 20 | 21 | exports.certIdValidate = validate( 22 | { 23 | params: Joi.object({ 24 | id: objectId.required() 25 | }) 26 | }, 27 | {}, 28 | {} 29 | ); 30 | -------------------------------------------------------------------------------- /src/validations/project.validation.js: -------------------------------------------------------------------------------- 1 | const { validate, Joi } = require('express-validation'); 2 | 3 | const githubProjectValidation = { 4 | params: Joi.object({ 5 | owner: Joi.string().strict().required(), 6 | repo: Joi.string().strict().required() 7 | }) 8 | }; 9 | 10 | const gitlabProjectValidation = { 11 | params: Joi.object({ 12 | id: Joi.number().required() 13 | }) 14 | }; 15 | 16 | const bitBucketProjectValidation = { 17 | params: Joi.object({ 18 | workspace: Joi.string().strict().required(), 19 | repo: Joi.string().strict().required() 20 | }) 21 | }; 22 | 23 | exports.githubProjectValidation = validate(githubProjectValidation, {}, {}); 24 | exports.gitlabProjectValidation = validate(gitlabProjectValidation, {}, {}); 25 | exports.bitBucketProjectValidation = validate( 26 | bitBucketProjectValidation, 27 | {}, 28 | {} 29 | ); 30 | -------------------------------------------------------------------------------- /src/views/certificate.ejs: -------------------------------------------------------------------------------- 1 |
2 | 37 |
38 | 40 | 41 |
42 |
43 | Certified On: <%= data.createdAt.toISOString().split('T').join(' ').slice(0,19) %>
44 |
45 |

Certificate

46 |

of Contribution

47 |
48 |
49 | This is to certify that
50 | <%= data.userName%>
51 | has actively contibuted to open source project <%= data.projectRepo %> of <%= data.projectOwner %>. 52 | 53 |
54 |

Last contributed at : <%= data.lastContributionDate.toISOString().split('T').join(' ').slice(0,19) %>

55 |
56 |
57 |
58 |
59 |
60 |

Scan to verify

61 |
62 |
63 | <% for(var loop=0; loop < data.images.length; loop++) { %> 64 | 65 | <% } %> 66 |
67 |
68 |
69 | Certificate Id: <%= (data._id).toString().match(new RegExp('.{1,5}', 'g')).join("-"); %> 70 |
71 |
72 |
73 |
-------------------------------------------------------------------------------- /src/views/certificateHolder.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Certificate 5 | 6 | 7 | 8 | <%- html %> 9 | 14 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 | -------------------------------------------------------------------------------- /src/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 |

<%= title %>

9 |

Welcome to <%= title %>

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/database.test.js: -------------------------------------------------------------------------------- 1 | require('./enviroment.test'); 2 | 3 | const mongoose = require('mongoose'); 4 | beforeAll(() => require('../src/models/index.model').connect()); 5 | 6 | afterAll(() => { 7 | mongoose.connection.close(); 8 | }); 9 | 10 | test('should connect to mongoDB when loaded', () => { 11 | expect(mongoose.connection.readyState).toBe(1); 12 | }); 13 | -------------------------------------------------------------------------------- /test/enviroment.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: '.test.env' }); 2 | require('dotenv').config({ path: '.env' }); 3 | jest.setTimeout(10000); 4 | 5 | test('should be in test enviroment when loaded', () => { 6 | expect(process.env.TEST).toBe('YES'); 7 | }); 8 | -------------------------------------------------------------------------------- /test/integration/init.test.js: -------------------------------------------------------------------------------- 1 | require('../database.test'); 2 | require('../enviroment.test'); 3 | const app = require('../../src/app'); 4 | let server = null; 5 | 6 | beforeAll(() => { 7 | server = app.listen(); 8 | 9 | return new Promise((resolve, reject) => { 10 | server.on('error', reject); 11 | server.on('listening', resolve); 12 | }); 13 | }); 14 | 15 | afterAll(() => { 16 | server.close(); 17 | }); 18 | 19 | test('Should start the server on given port when loaded', () => { 20 | expect(server.listening).toBe(true); 21 | }); 22 | 23 | exports.getLivePort = () => { 24 | if (!server) 25 | throw new Error( 26 | 'Server not started.\n\n Call this method only inside a test.' 27 | ); 28 | return server.address().port; 29 | }; 30 | -------------------------------------------------------------------------------- /test/unit/auth.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | 3 | const auth = require('../../src/controllers/auth.controller'); 4 | 5 | test('should return valid jwt token when recieved verified github user', async () => { 6 | const mReq = { 7 | user: { 8 | name: 'Test User', 9 | username: 'Test-User', 10 | profileUrl: 'https://github.com/Test-User', 11 | avatar: 'https://avatars.githubusercontent.com/u/99425406?s=200&v=4', 12 | kind: 'github', 13 | accessToken: 'accessToken' 14 | } 15 | }; 16 | const mRes = { 17 | status: jest.fn().mockReturnThis(), 18 | redirect: jest.fn((x) => { 19 | const url = new URL(x); 20 | const frontEndUrl = new URL(process.env.FRONTEND_URL); 21 | expect(url.hostname).toBe(frontEndUrl.hostname); 22 | expect(url.pathname).toBe('/recievedToken.html'); 23 | expect(url.searchParams.get('token')).toBeTruthy(); 24 | }), 25 | json: jest.fn((x) => { 26 | expect(x).toBeTruthy(); 27 | expect(x.error).toBeUndefined(); 28 | }) 29 | }; 30 | const mNext = jest.fn(); 31 | 32 | await auth.login(mReq, mRes, mNext); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/certificate.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | require('../database.test'); 3 | const { Types } = require('mongoose'); 4 | const certificateController = require('../../src/controllers/certificate.controller'); 5 | const Certificate = require('../../src/models/certificate.model'); 6 | const NotFoundError = require('../../src/errors/notFound.error'); 7 | test('should return certificate details when valid certificate id is provided', async () => { 8 | const certificate = new Certificate({ 9 | userGithubId: 'test-open-certs-userId', 10 | userName: 'test-open-certs-userId', 11 | projectRepo: 'open-certs', 12 | projectOwner: 'open-certs', 13 | commitCount: 0, 14 | pullRequestCount: 0, 15 | lastContributionDate: new Date(), 16 | images: [] 17 | }); 18 | await certificate.save(); 19 | const mReq = { 20 | params: { 21 | id: String(certificate._id) 22 | } 23 | }; 24 | const mRes = { 25 | status: jest.fn().mockReturnThis(), 26 | json: jest.fn((x) => { 27 | expect(x).toBeTruthy(); 28 | expect(x.error).toBeUndefined(); 29 | expect(String(x.certificate._id)).toBe(String(certificate._id)); 30 | }) 31 | }; 32 | 33 | await certificateController.getCertDetails(mReq, mRes); 34 | }); 35 | 36 | test('should return no certificate details when non-existing certificate id is provided', async () => { 37 | const mReq = { 38 | params: { 39 | id: new Types.ObjectId() 40 | } 41 | }; 42 | 43 | const mRes = { 44 | status: jest.fn().mockReturnThis(), 45 | json: jest.fn() 46 | }; 47 | const mNext = jest.fn((x) => { 48 | expect(x).toBeTruthy(); 49 | expect(x).toBeInstanceOf(NotFoundError); 50 | }); 51 | 52 | await certificateController.getCertDetails(mReq, mRes, mNext); 53 | }); 54 | -------------------------------------------------------------------------------- /test/unit/crypto.helper.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | 3 | const AuthenticationError = require('../../src/errors/authentication.error'); 4 | const crypto = require('../../src/helpers/crypto.helper'); 5 | 6 | test('should return the correct plainText when decrypted using valid encrypted text', () => { 7 | const plainText = 'openCerts'; 8 | const encryptedText = crypto.encrypt(plainText); 9 | 10 | const decryptedText = crypto.decrypt(encryptedText); 11 | 12 | expect(decryptedText).toBe(plainText); 13 | }); 14 | 15 | test('should throw error while decrypting invalid encrypted text', () => { 16 | const randomText = 'openCerts'; 17 | let thrownError = null; 18 | 19 | try { 20 | crypto.decrypt(randomText); 21 | } catch (e) { 22 | thrownError = e; 23 | } 24 | 25 | expect(thrownError).toBeTruthy(); 26 | expect(thrownError instanceof AuthenticationError).toBeTruthy(); 27 | }); 28 | 29 | test('should throw error while decrypting invalid formatted encrypted text', () => { 30 | const randomText = 'open|1234123412341234'; 31 | let thrownError = null; 32 | 33 | try { 34 | crypto.decrypt(randomText); 35 | } catch (e) { 36 | thrownError = e; 37 | } 38 | 39 | expect(thrownError).toBeTruthy(); 40 | expect(thrownError instanceof AuthenticationError).toBeTruthy(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/errorHandler.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | const { errorHandler } = require('../../src/helpers/errorhandler.helper'); 3 | const NotFoundError = require('../../src/errors/notFound.error'); 4 | const CustomError = require('../../src/errors/custom.error'); 5 | const { ValidationError } = require('express-validation'); 6 | const AuthenticationError = require('../../src/errors/authentication.error'); 7 | const GithubAPIUnavailableError = require('../../src/errors/githubAPITimeout.error'); 8 | 9 | test('should return customError when a customError is found', () => { 10 | const mReq = {}; 11 | const mError = new CustomError('testing', 401); 12 | const mRes = { 13 | status: jest.fn((x) => { 14 | expect(x).toBe(mError.status); 15 | return mRes; 16 | }), 17 | json: jest.fn((x) => { 18 | expect(x).toBeTruthy(); 19 | expect(x.error).toBeTruthy(); 20 | expect(x.error.type).toBe(mError.type); 21 | }) 22 | }; 23 | const mNext = jest.fn(); 24 | 25 | errorHandler(mError, mReq, mRes, mNext); 26 | }); 27 | 28 | test('should return NotFoundError when a NotFoundError is found', () => { 29 | const mReq = {}; 30 | const mError = new NotFoundError('testing'); 31 | const mRes = { 32 | status: jest.fn((x) => { 33 | expect(x).toBe(404); 34 | return mRes; 35 | }), 36 | json: jest.fn((x) => { 37 | expect(x).toBeTruthy(); 38 | expect(x.error).toBeTruthy(); 39 | expect(x.error.type).toBe(mError.type); 40 | }) 41 | }; 42 | const mNext = jest.fn(); 43 | 44 | errorHandler(mError, mReq, mRes, mNext); 45 | }); 46 | 47 | test('should return AuthenticationError when a NotFoundError is found', () => { 48 | const mReq = {}; 49 | const mError = new AuthenticationError('testing'); 50 | const mRes = { 51 | status: jest.fn((x) => { 52 | expect(x).toBe(401); 53 | return mRes; 54 | }), 55 | json: jest.fn((x) => { 56 | expect(x).toBeTruthy(); 57 | expect(x.error).toBeTruthy(); 58 | expect(x.error.type).toBe(mError.type); 59 | expect(x.error.logout).toBeTruthy(); 60 | }) 61 | }; 62 | const mNext = jest.fn(); 63 | 64 | errorHandler(mError, mReq, mRes, mNext); 65 | }); 66 | 67 | test('should return GithubAPIUnavailableError when a 202 status code is found', () => { 68 | const mReq = {}; 69 | const mError = new GithubAPIUnavailableError('testing'); 70 | const mRes = { 71 | status: jest.fn((x) => { 72 | expect(x).toBe(503); 73 | return mRes; 74 | }), 75 | json: jest.fn((x) => { 76 | expect(x).toBeTruthy(); 77 | expect(x.error).toBeTruthy(); 78 | expect(x.error.type).toBe(mError.type); 79 | }) 80 | }; 81 | const mNext = jest.fn(); 82 | 83 | errorHandler(mError, mReq, mRes, mNext); 84 | }); 85 | 86 | test('should return validationError with details when a validationError is found', () => { 87 | const mReq = {}; 88 | const mError = new ValidationError({}, {}); 89 | const mRes = { 90 | status: jest.fn((x) => { 91 | expect(x).toBe(400); 92 | return mRes; 93 | }), 94 | json: jest.fn((x) => { 95 | expect(x).toBeTruthy(); 96 | expect(x.error).toBeTruthy(); 97 | expect(x.error.type).toBe('ValidationError'); 98 | expect(x.error.details).toBeTruthy(); 99 | }) 100 | }; 101 | const mNext = jest.fn(); 102 | 103 | errorHandler(mError, mReq, mRes, mNext); 104 | }); 105 | 106 | test('should return Error with trace when a Error is found in development', () => { 107 | const mReq = { 108 | app: { 109 | get: jest.fn(() => 'development') 110 | } 111 | }; 112 | const mError = new Error('testing error'); 113 | const mRes = { 114 | status: jest.fn((x) => { 115 | expect(x).toBe(500); 116 | return mRes; 117 | }), 118 | json: jest.fn((x) => { 119 | expect(x).toBeTruthy(); 120 | expect(x.error).toBeTruthy(); 121 | expect(x.error.type).toBe('Error'); 122 | expect(x.error.trace).toBeTruthy(); 123 | }) 124 | }; 125 | const mNext = jest.fn(); 126 | 127 | errorHandler(mError, mReq, mRes, mNext); 128 | }); 129 | 130 | test('should return Error without trace when a error is found in production', () => { 131 | const mReq = { 132 | app: { 133 | get: jest.fn(() => 'production') 134 | } 135 | }; 136 | const mError = new Error('testing error'); 137 | const mRes = { 138 | status: jest.fn((x) => { 139 | expect(x).toBe(500); 140 | return mRes; 141 | }), 142 | json: jest.fn((x) => { 143 | expect(x).toBeTruthy(); 144 | expect(x.error).toBeTruthy(); 145 | expect(x.error.type).toBe('Error'); 146 | expect(x.error.trace).toBeUndefined(); 147 | expect(x.error.message).toBe('Something went wrong'); 148 | }) 149 | }; 150 | const mNext = jest.fn(); 151 | 152 | errorHandler(mError, mReq, mRes, mNext); 153 | }); 154 | 155 | test('should call handleError with getResponse when a custom error occurs', () => { 156 | const mReq = {}; 157 | const mError = new CustomError('testing error'); 158 | const handleError = jest.spyOn(mError, 'handleError'); 159 | const getResponse = jest.spyOn(mError, 'getResponse'); 160 | const mRes = { 161 | status: jest.fn((x) => { 162 | expect(x).toBe(mError.status); 163 | return mRes; 164 | }), 165 | json: jest.fn((x) => { 166 | expect(x).toBeTruthy(); 167 | expect(x.error).toBeTruthy(); 168 | expect(x.error.type).toBe('CustomError'); 169 | }) 170 | }; 171 | const mNext = jest.fn(); 172 | errorHandler(mError, mReq, mRes, mNext); 173 | expect(handleError).toBeCalledTimes(1); 174 | expect(getResponse).toBeCalledTimes(1); 175 | }); 176 | -------------------------------------------------------------------------------- /test/unit/github.helper.test.js: -------------------------------------------------------------------------------- 1 | const GithubAPIError = require('../../src/errors/githubAPI.error'); 2 | const GithubAPITimeoutError = require('../../src/errors/githubAPITimeout.error'); 3 | const { 4 | getRepo, 5 | getMyCommits, 6 | getMyPullRequests 7 | } = require('../../src/helpers/github.helper'); 8 | 9 | const data = {}; 10 | jest.mock('@octokit/rest', () => { 11 | const mf = async () => { 12 | if (data.error) throw data.error; 13 | return data.response; 14 | }; 15 | return { 16 | Octokit: function () { 17 | this.rest = { 18 | search: { 19 | commits: mf, 20 | issuesAndPullRequests: mf 21 | }, 22 | repos: { 23 | get: mf, 24 | getContributorsStats: mf 25 | } 26 | }; 27 | } 28 | }; 29 | }); 30 | 31 | afterEach(() => { 32 | data.error = undefined; 33 | data.response = undefined; 34 | }); 35 | 36 | test('should return GithubAPIUnavailableError when response status is 202', () => { 37 | data.response = { 38 | status: 202 39 | }; 40 | const mAuth = {}; 41 | const mOwner = 'owner'; 42 | const mRepo = 'repo'; 43 | const mCatch = jest.fn((err) => { 44 | expect(err).toBeTruthy(); 45 | expect(err).toBeInstanceOf(GithubAPITimeoutError); 46 | }); 47 | getRepo(mAuth, mOwner, mRepo) 48 | .catch(mCatch) 49 | .finally(() => { 50 | expect(mCatch).toBeCalledTimes(1); 51 | }); 52 | }); 53 | 54 | test('should return valid response when response is valid', () => { 55 | data.response = { 56 | status: 200 57 | }; 58 | const mAuth = {}; 59 | const mOwner = 'owner'; 60 | const mRepo = 'repo'; 61 | const mCatch = jest.fn(); 62 | const mThen = jest.fn((res) => { 63 | expect(res).toBe(data.response); 64 | }); 65 | getMyCommits(mAuth, mOwner, mRepo) 66 | .catch(mCatch) 67 | .then(mThen) 68 | .finally(() => { 69 | expect(mCatch).toBeCalledTimes(0); 70 | expect(mThen).toBeCalledTimes(1); 71 | }); 72 | }); 73 | 74 | test('should return GithubAPIError when response is invalid', () => { 75 | data.error = { 76 | response: { 77 | data: { 78 | message: 'message' 79 | } 80 | } 81 | }; 82 | const mAuth = {}; 83 | const mOwner = 'owner'; 84 | const mRepo = 'repo'; 85 | const mCatch = jest.fn((err) => { 86 | expect(err).toBeTruthy(); 87 | expect(err).toBeInstanceOf(GithubAPIError); 88 | expect(err.message).toBe(data.error.response.data.message); 89 | }); 90 | getMyPullRequests(mAuth, mOwner, mRepo) 91 | .catch(mCatch) 92 | .finally(() => { 93 | expect(mCatch).toBeCalledTimes(1); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/unit/project.controller.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | 3 | test('getting accumulated github repo', async () => { 4 | console.log('Not implemented yet.'); 5 | // TODO 6 | 7 | // const mReq = { 8 | // user: { 9 | // accessToken: 'random' 10 | // }, 11 | // params: { 12 | // owner: 'random-owner', 13 | // repo: 'random-repo' 14 | // } 15 | // }; 16 | // const mRes = {}; 17 | 18 | // const mNext = jest.fn((err) => {}); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/project.jwt.helper.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | const ProjectTokenError = require('../../src/errors/projectToken.error'); 3 | const projectHelper = require('../../src/helpers/project.jwt.helper'); 4 | 5 | test('should return original project data after verifying signed token', async () => { 6 | const sampleData = { 7 | projectId: '123' 8 | }; 9 | 10 | const token = projectHelper.sign(sampleData); 11 | const decryptedData = await projectHelper.verify(token); 12 | 13 | expect(decryptedData).toBeTruthy(); 14 | expect(decryptedData.projectId).toBe(sampleData.projectId); 15 | }); 16 | 17 | test('should validate project token and call the next middleware', async () => { 18 | const sampleData = { 19 | projectId: '123' 20 | }; 21 | 22 | const token = projectHelper.sign(sampleData); 23 | const mReq = { 24 | headers: { 25 | 'project-token': token 26 | } 27 | }; 28 | 29 | const mRes = {}; 30 | 31 | const mNext = jest.fn((err) => { 32 | expect(err).not.toBeTruthy(); 33 | expect(mReq.project).toBeTruthy(); 34 | expect(mReq.project.projectId).toBe(sampleData.projectId); 35 | }); 36 | 37 | projectHelper.validateProject(mReq, mRes, mNext); 38 | }); 39 | 40 | test('should call mNext with error when no token is supplied', async () => { 41 | const mReq = { 42 | headers: {} 43 | }; 44 | 45 | const mRes = {}; 46 | 47 | const mNext = jest.fn((err) => { 48 | expect(err).toBeTruthy(); 49 | expect(mReq.project).not.toBeTruthy(); 50 | expect(err instanceof ProjectTokenError).toBeTruthy(); 51 | }); 52 | 53 | projectHelper.validateProject(mReq, mRes, mNext); 54 | }); 55 | 56 | test('should call mNext with error when invalid token is supplied', async () => { 57 | const mReq = { 58 | headers: { 59 | 'project-token': 'random-text' 60 | } 61 | }; 62 | 63 | const mRes = {}; 64 | 65 | const mNext = jest.fn((err) => { 66 | expect(err).toBeTruthy(); 67 | expect(mReq.project).not.toBeTruthy(); 68 | expect(err instanceof ProjectTokenError).toBeTruthy(); 69 | }); 70 | 71 | projectHelper.validateProject(mReq, mRes, mNext); 72 | }); 73 | -------------------------------------------------------------------------------- /test/unit/recaptcha.helper.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | 3 | const data = {}; 4 | 5 | jest.mock('axios', () => { 6 | return { 7 | post: () => { 8 | return new Promise((resolve, reject) => { 9 | if (data.error) { 10 | return reject(data.error); 11 | } 12 | resolve(data.body); 13 | }); 14 | } 15 | }; 16 | }); 17 | 18 | const CustomError = require('../../src/errors/custom.error'); 19 | const RecaptchaError = require('../../src/errors/recaptcha.error'); 20 | const recaptcha = require('../../src/helpers/recaptcha.helper'); 21 | 22 | beforeAll(() => { 23 | process.env.VALIDATE_RECAPTCHA = 'YES'; 24 | }); 25 | 26 | afterEach(() => { 27 | data.error = undefined; 28 | data.body = undefined; 29 | }); 30 | 31 | test('should call next middleware when recaptcha is validated successfully', async () => { 32 | data.body = { 33 | data: { 34 | success: true 35 | } 36 | }; 37 | const mReq = { 38 | headers: { 39 | recaptcha: 'token' 40 | } 41 | }; 42 | 43 | const mRes = {}; 44 | 45 | const mNext = jest.fn((err) => { 46 | expect(err).not.toBeTruthy(); 47 | }); 48 | 49 | recaptcha.validateReCaptcha(mReq, mRes, mNext); 50 | }); 51 | 52 | test('should throw recaptcha error when recaptcha token is missing', async () => { 53 | const mReq = { 54 | headers: {} 55 | }; 56 | 57 | const mRes = {}; 58 | 59 | const mNext = jest.fn((err) => { 60 | expect(err).toBeTruthy(); 61 | expect(err instanceof RecaptchaError).toBeTruthy(); 62 | }); 63 | 64 | recaptcha.validateReCaptcha(mReq, mRes, mNext); 65 | }); 66 | 67 | test('should throw recaptcha error when recaptcha validation result is false', async () => { 68 | data.body = { 69 | data: { 70 | success: false 71 | } 72 | }; 73 | const mReq = { 74 | headers: { 75 | recaptcha: 'token' 76 | } 77 | }; 78 | 79 | const mRes = {}; 80 | 81 | const mNext = jest.fn((err) => { 82 | expect(err).toBeTruthy(); 83 | expect(err instanceof RecaptchaError).toBeTruthy(); 84 | }); 85 | 86 | recaptcha.validateReCaptcha(mReq, mRes, mNext); 87 | }); 88 | 89 | test('should throw custom error when axios request fails', async () => { 90 | data.error = new Error('http error'); 91 | 92 | const mReq = { 93 | headers: { 94 | recaptcha: 'token' 95 | } 96 | }; 97 | 98 | const mRes = {}; 99 | 100 | const mNext = jest.fn((err) => { 101 | expect(err).toBeTruthy(); 102 | expect(err instanceof CustomError).toBeTruthy(); 103 | }); 104 | 105 | recaptcha.validateReCaptcha(mReq, mRes, mNext); 106 | }); 107 | 108 | test('should not validate recaptcha when validate recaptcha is NO', async () => { 109 | process.env.VALIDATE_RECAPTCHA = 'NO'; 110 | 111 | const mReq = { 112 | headers: {} 113 | }; 114 | 115 | const mRes = {}; 116 | 117 | const mNext = jest.fn((err) => { 118 | expect(err).not.toBeTruthy(); 119 | }); 120 | 121 | recaptcha.validateReCaptcha(mReq, mRes, mNext); 122 | }); 123 | -------------------------------------------------------------------------------- /test/unit/user.helper.test.js: -------------------------------------------------------------------------------- 1 | const AuthenticationError = require('../../src/errors/authentication.error'); 2 | const { checkUser } = require('../../src/helpers/user.helper'); 3 | 4 | test('should forward control to next controller when given type and user type is same', async () => { 5 | const mReq = { 6 | user: { 7 | kind: 'github' 8 | } 9 | }; 10 | const mRes = {}; 11 | const mNext = jest.fn((x) => { 12 | expect(x).not.toBeTruthy(); 13 | }); 14 | 15 | checkUser('github')(mReq, mRes, mNext); 16 | 17 | expect(mNext).toBeCalledTimes(1); 18 | }); 19 | 20 | test('should not forward control to next controller when given type and user type is not same', async () => { 21 | const mReq = { 22 | user: { 23 | kind: 'github' 24 | } 25 | }; 26 | const mRes = {}; 27 | const mNext = jest.fn((x) => { 28 | expect(x).toBeTruthy(); 29 | expect(x instanceof AuthenticationError).toBeTruthy(); 30 | }); 31 | 32 | checkUser('gitlab')(mReq, mRes, mNext); 33 | 34 | expect(mNext).toBeCalledTimes(1); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/user.test.js: -------------------------------------------------------------------------------- 1 | require('../enviroment.test'); 2 | 3 | const user = require('../../src/controllers/user.controler'); 4 | 5 | test('should return profile when called with proper token', async () => { 6 | const mReq = { 7 | user: { 8 | name: 'Test User', 9 | username: 'Test-User', 10 | profileUrl: 'https://github.com/Test-User', 11 | avatar: 'https://avatars.githubusercontent.com/u/99425406?s=200&v=4', 12 | kind: 'github', 13 | accessToken: 'accessToken' 14 | } 15 | }; 16 | const mRes = { 17 | status: jest.fn().mockReturnThis(), 18 | json: jest.fn((x) => { 19 | expect(x).toBeTruthy(); 20 | expect(x.error).toBeUndefined(); 21 | expect(x.user).toBeTruthy(); 22 | expect(x.user.username).toBe(mReq.user.username); 23 | }) 24 | }; 25 | const mNext = jest.fn(); 26 | 27 | await user.profile(mReq, mRes, mNext); 28 | }); 29 | --------------------------------------------------------------------------------