├── .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 |
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 |
68 |
69 |
Visitor's 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 |
--------------------------------------------------------------------------------