├── verification
├── .dockerignore
├── public
│ ├── did:india
│ ├── robots.txt
│ ├── verify.png
│ ├── favicon.ico
│ ├── manifest.json
│ ├── verify.webmanifest
│ └── index.html
├── Makefile
├── src
│ ├── .DS_Store
│ ├── assets
│ │ ├── .DS_Store
│ │ └── img
│ │ │ ├── loading.gif
│ │ │ ├── feedback-small.png
│ │ │ ├── ValidCertificate.png
│ │ │ ├── verify-certificate.png
│ │ │ ├── InvalidCertificate.jpeg
│ │ │ ├── sample_ceritificate.png
│ │ │ ├── download-certificate-small.png
│ │ │ ├── next-arrow.svg
│ │ │ ├── certificate-valid.svg
│ │ │ ├── certificate-invalid.svg
│ │ │ └── qr-code.svg
│ ├── redux
│ │ ├── reducers
│ │ │ ├── index.js
│ │ │ └── events.js
│ │ └── store.js
│ ├── setupTests.js
│ ├── components
│ │ ├── CustomButton
│ │ │ ├── index.js
│ │ │ └── index.css
│ │ ├── Loader
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ ├── QRScanner
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ ├── CertificateStatus
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ └── VerifyCertificate
│ │ │ ├── index.css
│ │ │ └── index.js
│ ├── App.test.js
│ ├── utils
│ │ ├── utils.js
│ │ └── credentials.json
│ ├── App.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── index.js
│ ├── App.css
│ ├── config.js
│ └── constants.js
├── Dockerfile
├── certificatePublicKey
├── nginx
│ └── nginx.conf
├── package.json
└── README.md
├── .gitignore
├── .DS_Store
├── sample.pdf
├── scripts
├── docker-entrypoint.sh
└── font-config.sh
├── README.md
├── LICENSE
├── schemas
├── templates
│ └── TrainingCertificate.html
└── TrainingCertificate.json
├── docker-compose.yml
├── .ipynb_checkpoints
└── certificate-api-checkpoint.ipynb
├── certificate-api.ipynb
└── sample.html
/verification/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | build
4 |
--------------------------------------------------------------------------------
/verification/public/did:india:
--------------------------------------------------------------------------------
1 | CFNB8DgNsmD9D8y52FsTQVKC5ar8dmGoXe9uRQFzQiuF
--------------------------------------------------------------------------------
/verification/Makefile:
--------------------------------------------------------------------------------
1 |
2 | docker:
3 | docker build -t tejashjl/verification .
4 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/.DS_Store
--------------------------------------------------------------------------------
/sample.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/sample.pdf
--------------------------------------------------------------------------------
/verification/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/verification/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/.DS_Store
--------------------------------------------------------------------------------
/verification/public/verify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/public/verify.png
--------------------------------------------------------------------------------
/verification/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/public/favicon.ico
--------------------------------------------------------------------------------
/verification/src/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/.DS_Store
--------------------------------------------------------------------------------
/verification/src/assets/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/loading.gif
--------------------------------------------------------------------------------
/verification/src/assets/img/feedback-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/feedback-small.png
--------------------------------------------------------------------------------
/verification/src/assets/img/ValidCertificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/ValidCertificate.png
--------------------------------------------------------------------------------
/verification/src/assets/img/verify-certificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/verify-certificate.png
--------------------------------------------------------------------------------
/verification/src/assets/img/InvalidCertificate.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/InvalidCertificate.jpeg
--------------------------------------------------------------------------------
/verification/src/assets/img/sample_ceritificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/sample_ceritificate.png
--------------------------------------------------------------------------------
/verification/src/assets/img/download-certificate-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sphere/ref-sunbirdrc-certificate/main/verification/src/assets/img/download-certificate-small.png
--------------------------------------------------------------------------------
/verification/src/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import {eventsReducer} from "./events";
3 |
4 | export default combineReducers({ events: eventsReducer });
5 |
--------------------------------------------------------------------------------
/scripts/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | if [ ! -f .initialized ]; then
2 | echo "Initializing container"
3 | # run initializing commands
4 | sh /scripts/font-config.sh
5 | touch .initialized
6 | else
7 | echo "Already initialized"
8 | fi
9 | echo "Starting the server"
10 | npm start
--------------------------------------------------------------------------------
/verification/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/verification/src/components/CustomButton/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 |
4 | export const CustomButton = ({children, className, ...props}) => {
5 | return (
6 |
7 | )
8 | }
--------------------------------------------------------------------------------
/verification/src/components/Loader/index.css:
--------------------------------------------------------------------------------
1 | .loader-wrapper {
2 | position: absolute;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100vw;
7 | height: 100vh;
8 | z-index: 10;
9 | background: rgb(222 226 230 / 60%);
10 | }
--------------------------------------------------------------------------------
/verification/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/verification/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import LoadingImg from "../../assets/img/loading.gif";
3 | import "./index.css";
4 |
5 | export const Loader = () => {
6 | return (
7 |
8 |

9 |
10 |
11 | )
12 | };
--------------------------------------------------------------------------------
/verification/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | export function ordinal_suffix_of(i) {
2 | const j = i % 10,
3 | k = i % 100;
4 | if (j == 1 && k != 11) {
5 | return i + "st";
6 | }
7 | if (j == 2 && k != 12) {
8 | return i + "nd";
9 | }
10 | if (j == 3 && k != 13) {
11 | return i + "rd";
12 | }
13 | return i + "th";
14 | }
--------------------------------------------------------------------------------
/verification/src/components/QRScanner/index.css:
--------------------------------------------------------------------------------
1 | #videoview {
2 | position: relative;
3 | width: 100%;
4 | }
5 |
6 | #video {
7 | position: relative;
8 | width: 100%;
9 | height: 100%;
10 | z-index: 1
11 | }
12 |
13 | #overlay {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | width: 100%;
18 | height: 100%;
19 | z-index: 2
20 | }
--------------------------------------------------------------------------------
/verification/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import {VerifyCertificate} from "./components/VerifyCertificate";
3 | import {Provider} from "react-redux";
4 | import {store} from "./redux/store";
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/verification/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/verification/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/verification/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:13.12.0-alpine as build
2 | WORKDIR /app
3 | ENV PATH /app/node_modules/.bin:$PATH
4 | COPY package.json ./
5 | COPY package-lock.json ./
6 | RUN npm ci --silent
7 | COPY . ./
8 | RUN npm run build
9 |
10 | # production environment
11 | FROM nginx:stable-alpine
12 | COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
13 | COPY --from=build /app/build /usr/share/nginx/html
14 | EXPOSE 80
15 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/verification/certificatePublicKey:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnXQalrgztecTpc+INjRQ8s73FSE1kU5QSlwBdICCVJBUKiuQUt7s+Z5epgCvLVAOCbP1mm5lV7bfgV/iYWDio7lzX4MlJwDedWLiufr3Ajq+79CQiqPaIbZTo0i13zijKtX7wgxQ78wT/HkJRLkFpmGeK3za21tEfttytkhmJYlwaDTEc+Kx3RJqVhVh/dfwJGeuV4Xc/e2NH++ht0ENGuTk44KpQ+pwQVqtW7lmbDZQJoOJ7HYmmoKGJ0qt2hrj15uwcD1WEYfY5N7N0ArTzPgctExtZFDmituLGzuAZfv2AZZ9/7Y+igshzfB0reIFdUKw3cdVTzfv5FNrIqN5pwIDAQAB
3 | -----END PUBLIC KEY-----
4 |
5 |
--------------------------------------------------------------------------------
/scripts/font-config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | apk --no-cache add msttcorefonts-installer fontconfig
3 | wget https://www.1001fonts.com/download/canterbury.zip
4 | unzip canterbury.zip
5 | install Canterbury.ttf /usr/share/fonts/truetype/
6 | wget -O Nautigal.zip 'https://fonts.google.com/download?family=The%20Nautigal'
7 | wget -O Imperial.zip 'https://fonts.google.com/download?family=Imperial%20Script'
8 | unzip -o Nautigal.zip
9 | unzip -o Imperial.zip
10 | install TheNautigal-Regular.ttf /usr/share/fonts/truetype/
11 | install ImperialScript-Regular.ttf /usr/share/fonts/truetype/
12 | fc-cache -f && rm -rf /var/cache/*
--------------------------------------------------------------------------------
/verification/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/verification/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import 'bootstrap/dist/css/bootstrap.css';
7 |
8 |
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ref-sunbirdrc-certificate
2 | Reference sample of certificate registry using Sunbird RC
3 |
4 | # Run your own certificate registry in 5 minutes
5 |
6 |
7 | Prerequesite:
8 | * docker
9 | * docker-compose
10 | * git (or download the zip)
11 |
12 | 1. run `git clone https://github.com/dileepbapat/ref-sunbirdrc-certificate.git`
13 | 2. cd ref-sunbirdrc-certificate
14 | 3. run `docker-compose up -d --force-recreate`
15 |
16 | API sample is available in jupyter notebook, needs additional dependency of python and jupyter.
17 | check certificate-api.ipynb
18 |
19 | ## Troubleshooting
20 | run `docker-compose logs -f registry` and check logs
21 |
--------------------------------------------------------------------------------
/verification/public/verify.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "gray",
3 | "description": "Verifies vaccination certificates by scanning",
4 | "display": "fullscreen",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "16x16",
9 | "type": "image/x-icon"
10 | },
11 | {
12 | "src": "verify.png",
13 | "sizes": "192x192",
14 | "type": "image/png"
15 | }
16 | ],
17 | "name": "Verify vaccination certificate (DiVoc)",
18 | "short_name": "DiVoc",
19 | "start_url": "/verify-certificate/",
20 | "display": "standalone",
21 | "theme_color": "#005050",
22 | "background_color": "#afafaf"
23 | }
--------------------------------------------------------------------------------
/verification/src/assets/img/next-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/verification/src/assets/img/certificate-valid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/verification/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location / {
5 | root /usr/share/nginx/html;
6 | try_files $uri /index.html;
7 | }
8 |
9 | add_header X-Frame-Options "SAMEORIGIN";
10 | add_header X-Content-Type-Options nosniff;
11 | add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
12 | add_header X-XSS-Protection '1; mode=block';
13 | add_header Content-Security-Policy "connect-src 'self'; default-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data:; manifest-src 'self';script-src-elem 'self' 'unsafe-inline' 'unsafe-eval'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'self'; worker-src 'self';";
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/verification/src/components/CustomButton/index.css:
--------------------------------------------------------------------------------
1 | .custom-button {
2 | border-radius: 4px;
3 | color: white;
4 | border: none;
5 | padding: 0.5rem 3rem;
6 | margin-top: 1rem
7 | }
8 | .green-btn {
9 | background: transparent linear-gradient(90deg, #2CD889 0%, #4CA07A 100%) 0% 0% no-repeat padding-box;
10 | }
11 |
12 | .blue-btn {
13 | background: transparent linear-gradient(90deg, #59BCF9 0%, #4E67D1 100%) 0% 0% no-repeat padding-box;
14 | }
15 |
16 | .yellow-btn {
17 | background: transparent linear-gradient(91deg, #FDBD22 0%, #DE9D00 100%) 0% 0% no-repeat padding-box;
18 | }
19 |
20 | .purple-btn {
21 | background: transparent linear-gradient(91deg, #8DA2FF 0%, #4E67D1 100%) 0% 0% no-repeat padding-box;
22 | }
--------------------------------------------------------------------------------
/verification/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/verification/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import {createStore} from "redux";
2 | import rootReducer from "./reducers";
3 |
4 | const loadState = () => {
5 | try {
6 | const serializedState = localStorage.getItem('state');
7 | if (serializedState === null) {
8 | return undefined;
9 | }
10 | return JSON.parse(serializedState);
11 | } catch (err) {
12 | return undefined;
13 | }
14 | };
15 |
16 | const saveState = (state) => {
17 | try {
18 | const serializedState = JSON.stringify(state);
19 | localStorage.setItem('state', serializedState);
20 | } catch {
21 | // ignore write errors
22 | }
23 | };
24 |
25 | const persistedState = loadState();
26 |
27 | export const store = createStore(
28 | rootReducer,
29 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
30 | );
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Dileep Bapat
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 |
--------------------------------------------------------------------------------
/schemas/templates/TrainingCertificate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
Certificate of Training
10 |
11 |
12 | |
13 |
14 | This is to certify that
15 | {{credentialSubject.name}}
16 | has successfully completed training requirements for
17 | {{credentialSubject.trainedOn}}
18 | and is awarded this certificate on
19 | {{dateFormat proof.created "dddd, MMMM Do YYYY"}}
20 |
21 |
22 | |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/verification/src/components/CertificateStatus/index.css:
--------------------------------------------------------------------------------
1 | .certificate-status-wrapper {
2 |
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | margin: 0 1rem;
8 |
9 | }
10 | .certificate-status-wrapper .certificate-status-image {
11 | margin: 1rem 0rem;
12 | }
13 |
14 | .certificate-status-wrapper .certificate-status {
15 | text-align: center;
16 | }
17 | .certificate-status-wrapper .valid {
18 | color: #4CA07A
19 | }
20 | .certificate-status-wrapper .invalid {
21 | color: #FC573B
22 | }
23 |
24 | .certificate-status-wrapper .verify-another-btn {
25 | background: transparent linear-gradient(90deg, #59BCF9 0%, #4E67D1 100%) 0% 0% no-repeat padding-box;
26 | border-radius: 4px;
27 | color: white;
28 | border: none;
29 | padding: 0.5rem 3rem;
30 | margin-top: 1rem
31 | }
32 |
33 | .small-info-card-wrapper {
34 | display: flex;
35 | width: 100%;
36 | box-shadow: 0px 6px 20px #C1CFD933;
37 | border-radius: 10px;
38 | min-height: 82px;
39 | }
40 | .small-card-img {
41 | height: 5rem;
42 | }
43 |
44 | .certificate-status-wrapper table {
45 | border-collapse: separate;
46 | border-spacing: 0 10px;
47 | }
48 |
49 | .certificate-status-wrapper table .value-col{
50 | width: 150px;
51 | word-wrap: break-word;
52 | }
--------------------------------------------------------------------------------
/verification/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "verification",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.11.4",
7 | "@testing-library/react": "^11.1.0",
8 | "@testing-library/user-event": "^12.1.10",
9 | "axios": "^0.21.0",
10 | "bootstrap": "^4.5.3",
11 | "jsonld-signatures": "^6.0.0",
12 | "jszip": "^3.5.0",
13 | "ramda": "^0.27.1",
14 | "react": "^17.0.1",
15 | "react-bootstrap": "^1.4.0",
16 | "react-dom": "^17.0.1",
17 | "react-redux": "^7.2.2",
18 | "react-router-dom": "^5.2.0",
19 | "react-scripts": "4.0.3",
20 | "redux": "^4.0.5",
21 | "request": "^2.88.2",
22 | "test-certificate-context": "^1.0.2",
23 | "vc-js": "^0.6.4",
24 | "web-vitals": "^1.0.1",
25 | "zbar.wasm": "^2.0.3"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/verification/src/assets/img/certificate-invalid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/verification/src/config.js:
--------------------------------------------------------------------------------
1 | const urlPath = "/certificate";
2 | const registerMemberLimit = 4;
3 | const certificatePublicKey = process.env.CERTIFICATE_PUBLIC_KEY || "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnXQalrgztecTpc+INjRQ8s73FSE1kU5QSlwBdICCVJBUKiuQUt7s+Z5epgCvLVAOCbP1mm5lV7bfgV/iYWDio7lzX4MlJwDedWLiufr3Ajq+79CQiqPaIbZTo0i13zijKtX7wgxQ78wT/HkJRLkFpmGeK3za21tEfttytkhmJYlwaDTEc+Kx3RJqVhVh/dfwJGeuV4Xc/e2NH++ht0ENGuTk44KpQ+pwQVqtW7lmbDZQJoOJ7HYmmoKGJ0qt2hrj15uwcD1WEYfY5N7N0ArTzPgctExtZFDmituLGzuAZfv2AZZ9/7Y+igshzfB0reIFdUKw3cdVTzfv5FNrIqN5pwIDAQAB\n-----END PUBLIC KEY-----\n"
4 | const certificatePublicKeyBase58 = process.env.CERTIFICATE_PUBLIC_KEY_BASE58 ||"DaipNW4xaH2bh1XGNNdqjnSYyru3hLnUgTBSfSvmZ2hi";
5 |
6 | const CERTIFICATE_CONTROLLER_ID = process.env.REACT_APP_CERTIFICATE_CONTROLLER_ID || 'https://sunbird.org/';
7 | const CERTIFICATE_NAMESPACE = process.env.REACT_APP_CERTIFICATE_NAMESPACE || "https://cvstatus.icmr.gov.in/credentials/testCertificate/v1";
8 | const CERTIFICATE_PUBKEY_ID = process.env.REACT_APP_CERTIFICATE_PUBKEY_ID || 'https://cvstatus.icmr.gov.in/i/india';
9 | const CERTIFICATE_DID = process.env.REACT_APP_CERTIFICATE_DID || 'did:india';
10 | const CERTIFICATE_SCAN_TIMEOUT = process.env.REACT_APP_CERTIFICATE_SCAN_TIMEOUT || '45000';
11 | const CERTIFICATE_SIGNED_KEY_TYPE = process.env.CERTIFICATE_SIGNED_KEY_TYPE || 'ED25519';
12 |
13 | module.exports = {
14 | urlPath,
15 | certificatePublicKey,
16 | registerMemberLimit,
17 | CERTIFICATE_CONTROLLER_ID,
18 | CERTIFICATE_DID,
19 | CERTIFICATE_NAMESPACE,
20 | CERTIFICATE_PUBKEY_ID,
21 | CERTIFICATE_SCAN_TIMEOUT,
22 | CERTIFICATE_SIGNED_KEY_TYPE,
23 | certificatePublicKeyBase58
24 | };
25 |
--------------------------------------------------------------------------------
/verification/src/components/VerifyCertificate/index.css:
--------------------------------------------------------------------------------
1 | .verify-certificate-wrapper{
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | overflow: auto;
6 | }
7 | .verify-certificate-wrapper .qr-camera {
8 | width: 95%;
9 | }
10 |
11 | .verify-certificate-wrapper .scan-btn{
12 | background: transparent linear-gradient(90deg, #2CD889 0%, #4CA07A 100%) 0% 0% no-repeat padding-box;
13 | border-radius: 4px;
14 | color: white;
15 | border: none;
16 | padding: 0.5rem 3rem;
17 | margin-top: 1rem
18 | }
19 |
20 | @media (min-width:961px) {
21 | .verify-certificate-wrapper .qr-camera {
22 | width: 25%;
23 | }
24 | }
25 |
26 | .container{
27 | color: #4D4F5C;
28 | box-shadow: 0px 6px 20px #C1CFD933;
29 | border-radius: 10px;
30 | border: none;
31 | text-align: left;
32 | }
33 |
34 | ol.verify-steps {
35 | list-style: none;
36 | counter-reset: item;
37 | padding: 0;
38 | }
39 | .verify-steps > li {
40 | counter-increment: item;
41 | margin-bottom: 20px;
42 | }
43 |
44 | .verify-steps img {
45 | margin-bottom: 20px;
46 | margin-left: 20px;
47 | height: 40vh;
48 | border: 1px solid gainsboro;
49 | }
50 |
51 | .verify-steps > ul {
52 | counter-increment: item;
53 | margin-bottom: 20px;
54 | }
55 | .verify-steps > li:before {
56 | margin-right: 10px;
57 | content: counter(item);
58 | background: #dce0e0;
59 | border-radius: 100%;
60 | border-color: #E6E6E6;
61 | box-shadow: 0px 6px 20px #C1CFD933;
62 | width: 1.5em;
63 | text-align: center;
64 | display: inline-block;
65 | }
66 |
67 | ul.success-verify {
68 | list-style: none;
69 | }
70 | .success-verify > li {
71 | margin-bottom: 10px;
72 | }
73 | .success-verify > li:before {
74 | content: "- ";
75 | }
76 |
--------------------------------------------------------------------------------
/verification/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
24 | Vaccination certificate verification application
25 |
26 |
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/schemas/TrainingCertificate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema",
3 | "type": "object",
4 | "properties": {
5 | "TrainingCertificate": {
6 | "$ref": "#/definitions/TrainingCertificate"
7 | }
8 | },
9 | "required": [
10 | "TrainingCertificate"
11 | ],
12 | "title": "TrainingCertificate",
13 | "definitions": {
14 | "TrainingCertificate": {
15 | "$id": "#/properties/TrainingCertificate",
16 | "type": "object",
17 | "title": "The TrainingCertificate Schema",
18 | "required": [
19 | "name",
20 | "contact"
21 | ],
22 | "properties": {
23 | "name": {
24 | "type": "string"
25 | },
26 | "trainingTitle": {
27 | "type": "string"
28 | },
29 | "contact": {
30 | "type": "string"
31 | },
32 | "date": {
33 | "type": "string",
34 | "format": "date"
35 | },
36 | "note": {
37 | "type": "string"
38 | }
39 | }
40 | }
41 | },
42 | "_osConfig": {
43 | "uniqueIndexFields": [
44 | "contact"
45 | ],
46 | "ownershipAttributes": [],
47 | "roles": [
48 | "admin"
49 | ],
50 | "inviteRoles": [
51 | "anonymous"
52 | ],
53 | "enableLogin": false,
54 | "credentialTemplate": {
55 | "@context": [
56 | "https://www.w3.org/2018/credentials/v1",
57 | "https://gist.githubusercontent.com/dileepbapat/eb932596a70f75016411cc871113a789/raw/498e5af1d94784f114b32c1ab827f951a8a24def/skill"
58 | ],
59 | "type": [
60 | "VerifiableCredential"
61 | ],
62 | "issuanceDate": "2021-08-27T10:57:57.237Z",
63 | "credentialSubject": {
64 | "type": "Person",
65 | "name": "{{name}}",
66 | "trainedOn": "{{trainingTitle}}"
67 | },
68 | "issuer": "did:web:sunbirdrc.dev/vc/skill"
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/verification/src/redux/reducers/events.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const EVENT_ACTION_TYPES = {
4 | ADD_EVENT: "ADD_EVENT",
5 | REMOVE_EVENT: "REMOVE_EVENT"
6 | };
7 | export const EVENT_TYPES = {
8 | CERTIFICATE_DOWNLOAD: "certificate-download",
9 | VALID_VERIFICATION: "valid-verification",
10 | INVALID_VERIFICATION: "invalid-verification",
11 | REVOKED_CERTIFICATE: "revoked-certificate",
12 | };
13 | const initialState = {
14 | data: [],
15 | };
16 |
17 | export function eventsReducer(state = initialState, action) {
18 | switch (action.type) {
19 | case EVENT_ACTION_TYPES.ADD_EVENT: {
20 | return {
21 | ...state,
22 | data: [...state.data, {id: state.data.length, ...action.payload}],
23 |
24 | };
25 | }
26 | case EVENT_ACTION_TYPES.REMOVE_EVENT: {
27 | return {
28 | ...state,
29 | data: state.data.filter(event => !action.payload.includes(event.id)),
30 |
31 | };
32 | }
33 | default:
34 | return state;
35 | }
36 | }
37 |
38 | export const addEventAction = (event) => {
39 | return {
40 | type: EVENT_ACTION_TYPES.ADD_EVENT,
41 | payload: {...event, date: new Date().toISOString()}
42 | }
43 | };
44 |
45 | const removeEventsAction = (eventIds) => {
46 | return {
47 | type: EVENT_ACTION_TYPES.REMOVE_EVENT,
48 | payload: eventIds
49 | }
50 | };
51 |
52 | export const postEvents = ({data}, dispatch) => {
53 | if (data.length > 0) {
54 | try {
55 | axios
56 | .post("/divoc/api/v1/events/", data)
57 | .then((res) => {
58 | return dispatch(removeEventsAction(data.map(e => e.id)));
59 | }).catch((e) => {
60 | console.log(e);
61 | });
62 | } catch (e) {
63 | console.log(e);
64 | }
65 | }
66 | };
--------------------------------------------------------------------------------
/verification/src/assets/img/qr-code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/verification/src/constants.js:
--------------------------------------------------------------------------------
1 | const monthNames = [
2 | "Jan", "Feb", "Mar", "Apr",
3 | "May", "Jun", "Jul", "Aug",
4 | "Sep", "Oct", "Nov", "Dec"
5 | ];
6 |
7 | export function formatDate(givenDate) {
8 | const dob = new Date(givenDate);
9 | let day = (dob.getDate()).toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false});
10 | let monthName = monthNames[dob.getMonth()];
11 | let year = dob.getFullYear();
12 |
13 | return `${day}-${monthName}-${year}`;
14 | }
15 |
16 | export const CertificateDetailsPaths = {
17 | "Name": {
18 | path: ["credentialSubject", "name"],
19 | format: (data) => (data)
20 | },
21 | "Age": {
22 | path: ["credentialSubject", "age"],
23 | format: (data) => (data)
24 | },
25 | "Gender": {
26 | path: ["credentialSubject", "gender"],
27 | format: (data) => (data)
28 | },
29 | "Certificate ID": {
30 | path: ["evidence", "0", "certificateId"],
31 | format: (data) => (data)
32 | },
33 | "Beneficiary ID": {
34 | path: ["credentialSubject", "refId"],
35 | format: (data) => (data)
36 | },
37 | "Vaccine Name": {
38 | path: ["evidence", "0", "vaccine"],
39 | format: (data) => (data)
40 | },
41 | "Date of ${dose} Dose": {
42 | path: ["evidence", "0", "effectiveStart"],
43 | format: (data) => (formatDate(data))
44 | },
45 | "Vaccination Status": {
46 | path: ["evidence", "0"],
47 | format: (data) => {
48 | if (data.dose !== data.totalDoses) {
49 | return "Partially Vaccinated"
50 | } else {
51 | return "Fully Vaccinated"
52 | }
53 | }
54 | },
55 | "Vaccination at": {
56 | path: ["evidence", "0", "facility", "name"],
57 | format: (data) => (data)
58 | }
59 | };
60 |
61 | export const TestCertificateDetailsPaths = {
62 | "Name": {
63 | path: ["credentialSubject", "name"],
64 | format: (data) => (data)
65 | },
66 | "Trained on": {
67 | path: ["credentialSubject", "trainedOn"],
68 | format: (data) => (data)
69 | },
70 | "Issuance Date": {
71 | path: ["issuanceDate"],
72 | format: (data) => (data)
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/verification/src/components/VerifyCertificate/index.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react";
2 | import "./index.css";
3 | import VerifyCertificateImg from "../../assets/img/verify-certificate.png"
4 | import ValidCertificateImg from "../../assets/img/ValidCertificate.png"
5 | import InvalidCertificateImg from "../../assets/img/InvalidCertificate.jpeg"
6 | import SampleCertificateImg from "../../assets/img/sample_ceritificate.png"
7 | import QRCodeImg from "../../assets/img/qr-code.svg"
8 | import {CertificateStatus} from "../CertificateStatus";
9 | import {CustomButton} from "../CustomButton";
10 | import QRScanner from "../QRScanner";
11 | import JSZip from "jszip";
12 | import Container from "react-bootstrap/Container";
13 | import Row from "react-bootstrap/Row";
14 | import Col from "react-bootstrap/Col";
15 | export const CERTIFICATE_FILE = "certificate.json";
16 |
17 | export const VerifyCertificate = () => {
18 | const [result, setResult] = useState("");
19 | const [showScanner, setShowScanner] = useState(false);
20 | const handleScan = data => {
21 | if (data) {
22 | const zip = new JSZip();
23 | zip.loadAsync(data).then((contents) => {
24 | return contents.files[CERTIFICATE_FILE].async('text')
25 | }).then(function (contents) {
26 | setResult(contents)
27 | }).catch(err => {
28 | setResult(data)
29 | }
30 | );
31 |
32 | }
33 | };
34 | const handleError = err => {
35 | console.error(err)
36 | };
37 | return (
38 |
39 | {
40 | !result &&
41 | <>
42 | {!showScanner &&
43 | <>
44 |

45 |
Verify Sunbird RC Certificate
46 |
setShowScanner(true)}>
47 | Scan QR code
48 |
49 |
50 | >}
51 | {showScanner &&
52 | <>
53 |
55 | setShowScanner(false)}>BACK
56 | >
57 | }
58 | >
59 | }
60 | {
61 | result && {
62 | setShowScanner(false);
63 | setResult("");
64 | }
65 | }/>
66 | }
67 |
68 |
69 |
70 | )
71 | };
72 |
--------------------------------------------------------------------------------
/verification/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "2.4"
2 |
3 | services:
4 | redis:
5 | image: redis
6 | ports:
7 | - "6379:6379"
8 | es:
9 | image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1
10 | environment:
11 | - discovery.type=single-node
12 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
13 | ports:
14 | - "9200:9200"
15 | - "9300:9300"
16 | healthcheck:
17 | test: ["CMD", "curl", "-f", "localhost:9200/_cluster/health"]
18 | interval: 30s
19 | timeout: 10s
20 | retries: 4
21 | db:
22 | image: postgres
23 | ports:
24 | - "5432:5432"
25 | environment:
26 | - POSTGRES_DB=registry
27 | - POSTGRES_USER=postgres
28 | - POSTGRES_PASSWORD=postgres
29 | registry:
30 | image: dockerhub/sunbird-rc-core:v0.0.3
31 | volumes:
32 | - ${PWD}/schemas:/home/sunbirdrc/config/public/_schemas
33 |
34 | environment:
35 | - connectionInfo_uri=jdbc:postgresql://db:5432/registry
36 | - connectionInfo_username=postgres
37 | - connectionInfo_password=postgres
38 | - elastic_search_connection_url=es:9200
39 | - search_provider=dev.sunbirdrc.registry.service.ElasticSearchService
40 | - sunbird_sso_realm=sunbird-rc
41 | - sunbird_sso_url=http://keycloak:8080/auth
42 | - sunbird_sso_admin_client_id=admin-api
43 | - sunbird_sso_client_id=registry-frontend
44 | - sunbird_sso_admin_client_secret=0358fa30-6014-4192-9551-7c61b15b774c
45 | - claims_url=http://claim-ms:8082
46 | - sign_url=http://certificate-signer:8079/sign
47 | - signature_enabled=true
48 | - pdf_url=http://certificate-api:8078/api/v1/certificatePDF
49 | - template_base_url=http://registry:8081/api/v1/templates/ #Looks for certificate templates for pdf copy of the signed certificate
50 | ports:
51 | - "8081:8081"
52 | depends_on:
53 | es:
54 | condition: service_healthy
55 | db:
56 | condition: service_started
57 | # keycloak:
58 | # image: dockerhub/ndear-keycloak
59 | # volumes:
60 | # - ${PWD}/imports:/opt/jboss/keycloak/imports
61 | # environment:
62 | # - DB_VENDOR=postgres
63 | # - DB_ADDR=db
64 | # - DB_PORT=5432
65 | # - DB_DATABASE=registry
66 | # - DB_USER=postgres
67 | # - DB_PASSWORD=postgres
68 | # - KEYCLOAK_USER=admin
69 | # - KEYCLOAK_PASSWORD=admin
70 | # - KEYCLOAK_IMPORT=/opt/jboss/keycloak/imports/realm-export.json
71 | # healthcheck:
72 | # test:
73 | # ["CMD-SHELL", "curl -f http://localhost:9990/ || exit 1"]
74 | # interval: 30s
75 | # timeout: 10s
76 | # retries: 5
77 | # ports:
78 | # - "8080:8080"
79 | # - "9990:9990"
80 | # depends_on:
81 | # db:
82 | # condition: service_started
83 | # claim-ms:
84 | # image: dockerhub/sunbird-rc-claim-ms
85 | # environment:
86 | # - connectionInfo_uri=jdbc:postgresql://db:5432/registry
87 | # - connectionInfo_username=postgres
88 | # - connectionInfo_password=postgres
89 | # - sunbirdrc_url=http://registry:8081
90 | # ports:
91 | # - "8082:8082"
92 | # depends_on:
93 | # db:
94 | # condition: service_started
95 | # registry:
96 | # condition: service_started
97 | certificate-signer:
98 | image: dockerhub/sunbird-rc-certificate-signer:v0.0.3
99 | environment:
100 | - PORT=8079
101 | ports:
102 | - "8079:8079"
103 | certificate-api:
104 | image: dockerhub/sunbird-rc-certificate-api:v0.0.3
105 | volumes:
106 | - ${PWD}/scripts:/scripts
107 | entrypoint: ["sh", "/scripts/docker-entrypoint.sh"]
108 | environment:
109 | - PORT=8078
110 | ports:
111 | - "8078:8078"
112 | verification-ui:
113 | image: tejashjl/verification
114 | ports:
115 | - "80:80"
116 |
117 | # file-storage:
118 | # image: quay.io/minio/minio
119 | # volumes:
120 | # - ${HOME}/minio/data:/data
121 | # environment:
122 | # - MINIO_ROOT_USER=admin
123 | # - MINIO_ROOT_PASSWORD=12345678
124 | # command: server --address 0.0.0.0:9000 --console-address 0.0.0.0:9001 /data
125 | # ports:
126 | # - "9000:9000"
127 | # - "9001:9001"
128 | # healthcheck:
129 | # test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
130 | # interval: 30s
131 | # timeout: 20s
132 | # retries: 3
133 |
--------------------------------------------------------------------------------
/verification/src/components/QRScanner/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import './index.css';
3 | import {scanImageData} from "zbar.wasm";
4 |
5 | const SCAN_PERIOD_MS = 100;
6 |
7 | function hasGetUserMedia() {
8 | return !!(
9 | (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) ||
10 | navigator.webkitGetUserMedia ||
11 | navigator.mozGetUserMedia ||
12 | navigator.msGetUserMedia
13 | );
14 | }
15 |
16 | export default class QRScanner extends Component {
17 | static defaultProps = {
18 | className: '',
19 | height: 1000,
20 | width: 1000,
21 | videoConstraints: {
22 | facingMode: "environment"
23 | }
24 | };
25 |
26 |
27 | static mountedInstances = [];
28 |
29 | static userMediaRequested = false;
30 |
31 | static scanTimer = null;
32 |
33 | constructor(props) {
34 | super(props);
35 | this.state = {
36 | hasUserMedia: false,
37 | };
38 | }
39 |
40 | componentDidMount() {
41 | if (!hasGetUserMedia()) return;
42 |
43 | QRScanner.mountedInstances.push(this);
44 |
45 | if (!this.state.hasUserMedia && !QRScanner.userMediaRequested) {
46 | this.requestUserMedia();
47 | }
48 | QRScanner.scanTimer = setInterval(() => {
49 | this.scanBarcode();
50 | }, SCAN_PERIOD_MS);
51 |
52 |
53 | }
54 |
55 | componentWillUpdate(nextProps) {
56 | if (
57 | JSON.stringify(nextProps.videoConstraints) !==
58 | JSON.stringify(this.props.videoConstraints)
59 | ) {
60 | this.requestUserMedia();
61 | }
62 | }
63 |
64 | componentWillUnmount() {
65 | clearInterval(QRScanner.scanTimer);
66 | const index = QRScanner.mountedInstances.indexOf(this);
67 | QRScanner.mountedInstances.splice(index, 1);
68 |
69 | QRScanner.userMediaRequested = false;
70 | if (QRScanner.mountedInstances.length === 0 && this.state.hasUserMedia) {
71 | if (this.stream.getVideoTracks && this.stream.getAudioTracks) {
72 | this.stream.getVideoTracks().map(track => track.stop());
73 | } else {
74 | this.stream.stop();
75 | }
76 | window.URL.revokeObjectURL(this.state.src);
77 | }
78 | }
79 |
80 | scanBarcode = async () => {
81 |
82 | let canvas = document.createElement('canvas');
83 | canvas.width = this.props.width;
84 | canvas.height = this.props.height
85 | let ctx = canvas.getContext('2d');
86 | ctx.drawImage(this.video, 0, 0, this.props.width, this.props.height);
87 | let data = ctx.getImageData(0, 0, canvas.width, canvas.height);
88 | const symbols = await scanImageData(data);
89 | scanImageData(data)
90 | // console.log(symbols, Date.now());
91 | for (let i = 0; i < symbols.length; ++i) {
92 | const sym = symbols[i];
93 |
94 | this.props.onScan(sym.decode())
95 | }
96 |
97 | }
98 |
99 |
100 | requestUserMedia() {
101 | navigator.getUserMedia =
102 | navigator.mediaDevices.getUserMedia ||
103 | navigator.webkitGetUserMedia ||
104 | navigator.mozGetUserMedia ||
105 | navigator.msGetUserMedia;
106 |
107 | const sourceSelected = (videoConstraints) => {
108 | const constraints = {
109 | video: videoConstraints || true,
110 | };
111 |
112 | navigator.mediaDevices
113 | .getUserMedia(constraints)
114 | .then((stream) => {
115 | QRScanner.mountedInstances.forEach(instance =>
116 | instance.handleUserMedia(null, stream),
117 | );
118 | })
119 | .catch((e) => {
120 | QRScanner.mountedInstances.forEach(instance =>
121 | instance.handleUserMedia(e),
122 | );
123 | });
124 | };
125 |
126 | if ('mediaDevices' in navigator) {
127 | sourceSelected(this.props.videoConstraints);
128 | } else {
129 | const optionalSource = id => ({optional: [{sourceId: id}]});
130 |
131 | const constraintToSourceId = (constraint) => {
132 | const deviceId = (constraint || {}).deviceId;
133 |
134 | if (typeof deviceId === 'string') {
135 | return deviceId;
136 | } else if (Array.isArray(deviceId) && deviceId.length > 0) {
137 | return deviceId[0];
138 | } else if (typeof deviceId === 'object' && deviceId.ideal) {
139 | return deviceId.ideal;
140 | }
141 |
142 | return null;
143 | };
144 |
145 | MediaStreamTrack.getSources((sources) => {
146 |
147 | let videoSource = null;
148 |
149 | sources.forEach((source) => {
150 | if (source.kind === 'video') {
151 | videoSource = source.id;
152 | }
153 | });
154 |
155 |
156 | const videoSourceId = constraintToSourceId(this.props.videoConstraints);
157 | if (videoSourceId) {
158 | videoSource = videoSourceId;
159 | }
160 |
161 | sourceSelected(
162 | optionalSource(videoSource),
163 | );
164 | });
165 | }
166 |
167 | QRScanner.userMediaRequested = true;
168 | }
169 |
170 | handleUserMedia(err, stream) {
171 | if (err) {
172 | this.setState({hasUserMedia: false});
173 | this.props.onError(err);
174 |
175 | return;
176 | }
177 |
178 | this.stream = stream;
179 |
180 | try {
181 | this.video.srcObject = stream;
182 | this.setState({hasUserMedia: true});
183 | } catch (error) {
184 | this.setState({
185 | hasUserMedia: true,
186 | src: window.URL.createObjectURL(stream),
187 | });
188 | }
189 |
190 | }
191 |
192 | render() {
193 | return (
194 |
195 |
208 | );
209 | }
210 | }
--------------------------------------------------------------------------------
/.ipynb_checkpoints/certificate-api-checkpoint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 497,
6 | "id": "f0d66103",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import requests\n",
11 | "import random\n",
12 | "import json\n",
13 | "\n",
14 | "base_url = \"http://localhost:8081\"\n",
15 | "\n",
16 | "resp = requests.get(base_url)\n",
17 | "assert resp.status_code == 404\n",
18 | "assert resp.json()[\"status\"] == 404\n",
19 | "assert resp.json()[\"error\"] == \"Not Found\"\n"
20 | ]
21 | },
22 | {
23 | "cell_type": "code",
24 | "execution_count": 498,
25 | "id": "fc57f642",
26 | "metadata": {},
27 | "outputs": [],
28 | "source": [
29 | "\n",
30 | "resp = requests.get(\"%s/api/docs/swagger.json\"%base_url)\n",
31 | "assert resp.status_code == 200\n",
32 | "assert resp.json()[\"swagger\"] == \"2.0\"\n",
33 | "assert resp.json()[\"paths\"] != None\n",
34 | "\n",
35 | "swaggerJson = resp.json()\n",
36 | "swaggerJson[\"paths\"].keys()\n",
37 | "\n",
38 | "jsonUrl = [f for f in swaggerJson[\"paths\"].keys() if \".json\" in f][0]\n",
39 | "jsonUrl\n",
40 | "\n",
41 | "resp = requests.get(\"%s%s\"%(base_url, jsonUrl))\n",
42 | "assert resp.status_code == 200\n"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": 499,
48 | "id": "0e3b1059",
49 | "metadata": {},
50 | "outputs": [
51 | {
52 | "data": {
53 | "text/plain": [
54 | "{'TrainingCertificate': {'$id': '#/properties/TrainingCertificate',\n",
55 | " 'type': 'object',\n",
56 | " 'title': 'The TrainingCertificate Schema',\n",
57 | " 'required': ['name', 'contact'],\n",
58 | " 'properties': {'name': {'type': 'string'},\n",
59 | " 'trainingTitle': {'type': 'string'},\n",
60 | " 'contact': {'type': 'string'},\n",
61 | " 'date': {'type': 'string', 'format': 'date'},\n",
62 | " 'note': {'type': 'string'}}}}"
63 | ]
64 | },
65 | "execution_count": 499,
66 | "metadata": {},
67 | "output_type": "execute_result"
68 | }
69 | ],
70 | "source": [
71 | "resp.json()"
72 | ]
73 | },
74 | {
75 | "cell_type": "code",
76 | "execution_count": 500,
77 | "id": "3343a6fe",
78 | "metadata": {},
79 | "outputs": [
80 | {
81 | "name": "stdout",
82 | "output_type": "stream",
83 | "text": [
84 | "Available entities ['TrainingCertificate']\n"
85 | ]
86 | }
87 | ],
88 | "source": [
89 | "entities = list(resp.json().keys())\n",
90 | "print(\"Available entities \", entities)"
91 | ]
92 | },
93 | {
94 | "cell_type": "code",
95 | "execution_count": 501,
96 | "id": "41559bae",
97 | "metadata": {},
98 | "outputs": [
99 | {
100 | "name": "stdout",
101 | "output_type": "stream",
102 | "text": [
103 | "Using entity TrainingCertificate\n"
104 | ]
105 | }
106 | ],
107 | "source": [
108 | "entity_name=entities[0]\n",
109 | "print(\"Using entity %s\"%entity_name)\n"
110 | ]
111 | },
112 | {
113 | "cell_type": "code",
114 | "execution_count": 502,
115 | "id": "161ca6b9",
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "userId =str(random.randint(1e10,1e11))\n",
120 | "resp = requests.post(\"%s%s\"%(base_url, '/api/v1/%s'%entity_name), json={\n",
121 | " \"name\":\"Sunbird Learner\", \n",
122 | " \"contact\": userId, \n",
123 | " \"trainingTitle\":\"Sunbird RC Certificate Module\"\n",
124 | " \n",
125 | "})\n",
126 | "assert resp.status_code == 200 or print (resp.json())\n",
127 | "idx = resp.json()[\"result\"][entity_name][\"osid\"]\n"
128 | ]
129 | },
130 | {
131 | "cell_type": "code",
132 | "execution_count": 503,
133 | "id": "e73084c3",
134 | "metadata": {},
135 | "outputs": [
136 | {
137 | "data": {
138 | "text/plain": [
139 | "{'id': 'sunbird-rc.registry.create',\n",
140 | " 'ver': '1.0',\n",
141 | " 'ets': 1640861196414,\n",
142 | " 'params': {'resmsgid': '',\n",
143 | " 'msgid': '0bae36e4-2d45-4e16-b792-31d4f38dc5bd',\n",
144 | " 'err': '',\n",
145 | " 'status': 'SUCCESSFUL',\n",
146 | " 'errmsg': ''},\n",
147 | " 'responseCode': 'OK',\n",
148 | " 'result': {'TrainingCertificate': {'osid': '1-ab225141-73ff-4c39-b107-97ad2c0f8942'}}}"
149 | ]
150 | },
151 | "execution_count": 503,
152 | "metadata": {},
153 | "output_type": "execute_result"
154 | }
155 | ],
156 | "source": [
157 | "resp.json()\n"
158 | ]
159 | },
160 | {
161 | "cell_type": "code",
162 | "execution_count": 504,
163 | "id": "345cfbf2",
164 | "metadata": {},
165 | "outputs": [
166 | {
167 | "name": "stdout",
168 | "output_type": "stream",
169 | "text": [
170 | "{'id': 'sunbird-rc.registry.create', 'ver': '1.0', 'ets': 1640861196414, 'params': {'resmsgid': '', 'msgid': '0bae36e4-2d45-4e16-b792-31d4f38dc5bd', 'err': '', 'status': 'SUCCESSFUL', 'errmsg': ''}, 'responseCode': 'OK', 'result': {'TrainingCertificate': {'osid': '1-ab225141-73ff-4c39-b107-97ad2c0f8942'}}}\n"
171 | ]
172 | },
173 | {
174 | "data": {
175 | "text/plain": [
176 | "(200, '43497638543')"
177 | ]
178 | },
179 | "execution_count": 504,
180 | "metadata": {},
181 | "output_type": "execute_result"
182 | }
183 | ],
184 | "source": [
185 | "print(resp.json())\n",
186 | "resp.status_code, userId\n"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": 505,
192 | "id": "e880f599",
193 | "metadata": {},
194 | "outputs": [
195 | {
196 | "name": "stdout",
197 | "output_type": "stream",
198 | "text": [
199 | "{\"@context\": [\"https://www.w3.org/2018/credentials/v1\", \"https://gist.githubusercontent.com/dileepbapat/eb932596a70f75016411cc871113a789/raw/498e5af1d94784f114b32c1ab827f951a8a24def/skill\"], \"type\": [\"VerifiableCredential\"], \"issuanceDate\": \"2021-08-27T10:57:57.237Z\", \"credentialSubject\": {\"type\": \"Person\", \"name\": \"Sunbird Learner\", \"trainedOn\": \"Sunbird RC Certificate Module\"}, \"issuer\": \"did:web:sunbirdrc.dev/vc/skill\", \"proof\": {\"type\": \"Ed25519Signature2018\", \"created\": \"2021-12-30T10:46:38Z\", \"verificationMethod\": \"did:india\", \"proofPurpose\": \"assertionMethod\", \"jws\": \"eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..-mr1QgOcenpzCwM7GCgCJJ3J09uLL2PIHYxEvPsXluX4AznDVQfgWE-JQSlaHG_4ze5Yl2OIGGFAn2aiLS6sCQ\"}}\n"
200 | ]
201 | }
202 | ],
203 | "source": [
204 | "resp = requests.get(\"%s/api/v1/%s/%s\"%(base_url, entity_name, idx), headers={\"Accept\":\"application/vc+ld+json\"})\n",
205 | "print(json.dumps(resp.json()))\n",
206 | "assert resp.json()[\"proof\"][\"type\"] == \"Ed25519Signature2018\"\n"
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": 506,
212 | "id": "932efa6f",
213 | "metadata": {},
214 | "outputs": [],
215 | "source": [
216 | "resp = requests.get(\"%s/api/v1/%s/%s\"%(base_url, entity_name, idx), headers={\"Accept\":\"application/pdf\"})\n"
217 | ]
218 | },
219 | {
220 | "cell_type": "code",
221 | "execution_count": 507,
222 | "id": "f6854518",
223 | "metadata": {},
224 | "outputs": [],
225 | "source": [
226 | "resp.status_code, resp.content\n",
227 | "\n",
228 | "assert resp.content[:5].decode().startswith(\"%PDF\")\n",
229 | "with open('sample.pdf', 'wb') as f:\n",
230 | " f.write(resp.content)\n",
231 | " "
232 | ]
233 | },
234 | {
235 | "cell_type": "code",
236 | "execution_count": 508,
237 | "id": "3fdae280",
238 | "metadata": {},
239 | "outputs": [
240 | {
241 | "data": {
242 | "text/plain": [
243 | "[]"
244 | ]
245 | },
246 | "execution_count": 508,
247 | "metadata": {},
248 | "output_type": "execute_result"
249 | }
250 | ],
251 | "source": [
252 | "%system open 'sample.pdf'\n"
253 | ]
254 | }
255 | ],
256 | "metadata": {
257 | "kernelspec": {
258 | "display_name": "Python 3",
259 | "language": "python",
260 | "name": "python3"
261 | },
262 | "language_info": {
263 | "codemirror_mode": {
264 | "name": "ipython",
265 | "version": 3
266 | },
267 | "file_extension": ".py",
268 | "mimetype": "text/x-python",
269 | "name": "python",
270 | "nbconvert_exporter": "python",
271 | "pygments_lexer": "ipython3",
272 | "version": "3.9.9"
273 | }
274 | },
275 | "nbformat": 4,
276 | "nbformat_minor": 5
277 | }
278 |
--------------------------------------------------------------------------------
/verification/src/utils/credentials.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": {
3 | "@version": 1.1,
4 | "@protected": true,
5 |
6 | "id": "@id",
7 | "type": "@type",
8 |
9 | "VerifiableCredential": {
10 | "@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
11 | "@context": {
12 | "@version": 1.1,
13 | "@protected": true,
14 |
15 | "id": "@id",
16 | "type": "@type",
17 |
18 | "cred": "https://www.w3.org/2018/credentials#",
19 | "sec": "https://w3id.org/security#",
20 | "xsd": "http://www.w3.org/2001/XMLSchema#",
21 |
22 | "credentialSchema": {
23 | "@id": "cred:credentialSchema",
24 | "@type": "@id",
25 | "@context": {
26 | "@version": 1.1,
27 | "@protected": true,
28 |
29 | "id": "@id",
30 | "type": "@type",
31 |
32 | "cred": "https://www.w3.org/2018/credentials#",
33 |
34 | "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018"
35 | }
36 | },
37 | "credentialStatus": {"@id": "cred:credentialStatus", "@type": "@id"},
38 | "credentialSubject": {"@id": "cred:credentialSubject", "@type": "@id"},
39 | "evidence": {"@id": "cred:evidence", "@type": "@id"},
40 | "expirationDate": {"@id": "cred:expirationDate", "@type": "xsd:dateTime"},
41 | "holder": {"@id": "cred:holder", "@type": "@id"},
42 | "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"},
43 | "issuer": {"@id": "cred:issuer", "@type": "@id"},
44 | "issuanceDate": {"@id": "cred:issuanceDate", "@type": "xsd:dateTime"},
45 | "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"},
46 | "refreshService": {
47 | "@id": "cred:refreshService",
48 | "@type": "@id",
49 | "@context": {
50 | "@version": 1.1,
51 | "@protected": true,
52 |
53 | "id": "@id",
54 | "type": "@type",
55 |
56 | "cred": "https://www.w3.org/2018/credentials#",
57 |
58 | "ManualRefreshService2018": "cred:ManualRefreshService2018"
59 | }
60 | },
61 | "termsOfUse": {"@id": "cred:termsOfUse", "@type": "@id"},
62 | "validFrom": {"@id": "cred:validFrom", "@type": "xsd:dateTime"},
63 | "validUntil": {"@id": "cred:validUntil", "@type": "xsd:dateTime"}
64 | }
65 | },
66 |
67 | "VerifiablePresentation": {
68 | "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
69 | "@context": {
70 | "@version": 1.1,
71 | "@protected": true,
72 |
73 | "id": "@id",
74 | "type": "@type",
75 |
76 | "cred": "https://www.w3.org/2018/credentials#",
77 | "sec": "https://w3id.org/security#",
78 |
79 | "holder": {"@id": "cred:holder", "@type": "@id"},
80 | "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"},
81 | "verifiableCredential": {"@id": "cred:verifiableCredential", "@type": "@id", "@container": "@graph"}
82 | }
83 | },
84 |
85 | "EcdsaSecp256k1Signature2019": {
86 | "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019",
87 | "@context": {
88 | "@version": 1.1,
89 | "@protected": true,
90 |
91 | "id": "@id",
92 | "type": "@type",
93 |
94 | "sec": "https://w3id.org/security#",
95 | "xsd": "http://www.w3.org/2001/XMLSchema#",
96 |
97 | "challenge": "sec:challenge",
98 | "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"},
99 | "domain": "sec:domain",
100 | "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
101 | "jws": "sec:jws",
102 | "nonce": "sec:nonce",
103 | "proofPurpose": {
104 | "@id": "sec:proofPurpose",
105 | "@type": "@vocab",
106 | "@context": {
107 | "@version": 1.1,
108 | "@protected": true,
109 |
110 | "id": "@id",
111 | "type": "@type",
112 |
113 | "sec": "https://w3id.org/security#",
114 |
115 | "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"},
116 | "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"}
117 | }
118 | },
119 | "proofValue": "sec:proofValue",
120 | "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}
121 | }
122 | },
123 |
124 | "EcdsaSecp256r1Signature2019": {
125 | "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019",
126 | "@context": {
127 | "@version": 1.1,
128 | "@protected": true,
129 |
130 | "id": "@id",
131 | "type": "@type",
132 |
133 | "sec": "https://w3id.org/security#",
134 | "xsd": "http://www.w3.org/2001/XMLSchema#",
135 |
136 | "challenge": "sec:challenge",
137 | "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"},
138 | "domain": "sec:domain",
139 | "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
140 | "jws": "sec:jws",
141 | "nonce": "sec:nonce",
142 | "proofPurpose": {
143 | "@id": "sec:proofPurpose",
144 | "@type": "@vocab",
145 | "@context": {
146 | "@version": 1.1,
147 | "@protected": true,
148 |
149 | "id": "@id",
150 | "type": "@type",
151 |
152 | "sec": "https://w3id.org/security#",
153 |
154 | "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"},
155 | "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"}
156 | }
157 | },
158 | "proofValue": "sec:proofValue",
159 | "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}
160 | }
161 | },
162 |
163 | "Ed25519Signature2018": {
164 | "@id": "https://w3id.org/security#Ed25519Signature2018",
165 | "@context": {
166 | "@version": 1.1,
167 | "@protected": true,
168 |
169 | "id": "@id",
170 | "type": "@type",
171 |
172 | "sec": "https://w3id.org/security#",
173 | "xsd": "http://www.w3.org/2001/XMLSchema#",
174 |
175 | "challenge": "sec:challenge",
176 | "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"},
177 | "domain": "sec:domain",
178 | "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
179 | "jws": "sec:jws",
180 | "nonce": "sec:nonce",
181 | "proofPurpose": {
182 | "@id": "sec:proofPurpose",
183 | "@type": "@vocab",
184 | "@context": {
185 | "@version": 1.1,
186 | "@protected": true,
187 |
188 | "id": "@id",
189 | "type": "@type",
190 |
191 | "sec": "https://w3id.org/security#",
192 |
193 | "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"},
194 | "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"}
195 | }
196 | },
197 | "proofValue": "sec:proofValue",
198 | "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}
199 | }
200 | },
201 |
202 | "RsaSignature2018": {
203 | "@id": "https://w3id.org/security#RsaSignature2018",
204 | "@context": {
205 | "@version": 1.1,
206 | "@protected": true,
207 |
208 | "challenge": "sec:challenge",
209 | "created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:dateTime"},
210 | "domain": "sec:domain",
211 | "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
212 | "jws": "sec:jws",
213 | "nonce": "sec:nonce",
214 | "proofPurpose": {
215 | "@id": "sec:proofPurpose",
216 | "@type": "@vocab",
217 | "@context": {
218 | "@version": 1.1,
219 | "@protected": true,
220 |
221 | "id": "@id",
222 | "type": "@type",
223 |
224 | "sec": "https://w3id.org/security#",
225 |
226 | "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"},
227 | "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"}
228 | }
229 | },
230 | "proofValue": "sec:proofValue",
231 | "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}
232 | }
233 | },
234 |
235 | "proof": {"@id": "https://w3id.org/security#proof", "@type": "@id", "@container": "@graph"}
236 | }
237 | }
--------------------------------------------------------------------------------
/certificate-api.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "id": "f0d66103",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import requests\n",
11 | "import random\n",
12 | "import json\n",
13 | "\n",
14 | "base_url = \"http://localhost:8081\"\n",
15 | "\n",
16 | "resp = requests.get(base_url)\n",
17 | "assert resp.status_code == 404\n",
18 | "assert resp.json()[\"status\"] == 404\n",
19 | "assert resp.json()[\"error\"] == \"Not Found\"\n"
20 | ]
21 | },
22 | {
23 | "cell_type": "code",
24 | "execution_count": 2,
25 | "id": "fc57f642",
26 | "metadata": {},
27 | "outputs": [],
28 | "source": [
29 | "\n",
30 | "resp = requests.get(\"%s/api/docs/swagger.json\"%base_url)\n",
31 | "assert resp.status_code == 200\n",
32 | "assert resp.json()[\"swagger\"] == \"2.0\"\n",
33 | "assert resp.json()[\"paths\"] != None\n",
34 | "\n",
35 | "swaggerJson = resp.json()\n",
36 | "swaggerJson[\"paths\"].keys()\n",
37 | "\n",
38 | "jsonUrl = [f for f in swaggerJson[\"paths\"].keys() if \".json\" in f][0]\n",
39 | "jsonUrl\n",
40 | "\n",
41 | "resp = requests.get(\"%s%s\"%(base_url, jsonUrl))\n",
42 | "assert resp.status_code == 200\n"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": 3,
48 | "id": "0e3b1059",
49 | "metadata": {},
50 | "outputs": [
51 | {
52 | "data": {
53 | "text/plain": [
54 | "{'TrainingCertificate': {'$id': '#/properties/TrainingCertificate',\n",
55 | " 'type': 'object',\n",
56 | " 'title': 'The TrainingCertificate Schema',\n",
57 | " 'required': ['name', 'contact'],\n",
58 | " 'properties': {'name': {'type': 'string'},\n",
59 | " 'trainingTitle': {'type': 'string'},\n",
60 | " 'contact': {'type': 'string'},\n",
61 | " 'date': {'type': 'string', 'format': 'date'},\n",
62 | " 'note': {'type': 'string'}}}}"
63 | ]
64 | },
65 | "execution_count": 3,
66 | "metadata": {},
67 | "output_type": "execute_result"
68 | }
69 | ],
70 | "source": [
71 | "resp.json()"
72 | ]
73 | },
74 | {
75 | "cell_type": "code",
76 | "execution_count": 4,
77 | "id": "3343a6fe",
78 | "metadata": {},
79 | "outputs": [
80 | {
81 | "name": "stdout",
82 | "output_type": "stream",
83 | "text": [
84 | "Available entities ['TrainingCertificate']\n"
85 | ]
86 | }
87 | ],
88 | "source": [
89 | "entities = list(resp.json().keys())\n",
90 | "print(\"Available entities \", entities)"
91 | ]
92 | },
93 | {
94 | "cell_type": "code",
95 | "execution_count": 5,
96 | "id": "41559bae",
97 | "metadata": {},
98 | "outputs": [
99 | {
100 | "name": "stdout",
101 | "output_type": "stream",
102 | "text": [
103 | "Using entity TrainingCertificate\n"
104 | ]
105 | }
106 | ],
107 | "source": [
108 | "entity_name=entities[0]\n",
109 | "print(\"Using entity %s\"%entity_name)\n"
110 | ]
111 | },
112 | {
113 | "cell_type": "code",
114 | "execution_count": 6,
115 | "id": "161ca6b9",
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "userId =str(random.randint(1e10,1e11))\n",
120 | "resp = requests.post(\"%s%s\"%(base_url, '/api/v1/%s'%entity_name), json={\n",
121 | " \"name\":\"Sunbird Learner\", \n",
122 | " \"contact\": userId, \n",
123 | " \"trainingTitle\":\"Sunbird RC Certificate Module\"\n",
124 | " \n",
125 | "})\n",
126 | "assert resp.status_code == 200 or print (resp.json())\n",
127 | "idx = resp.json()[\"result\"][entity_name][\"osid\"]\n"
128 | ]
129 | },
130 | {
131 | "cell_type": "code",
132 | "execution_count": 7,
133 | "id": "e73084c3",
134 | "metadata": {},
135 | "outputs": [
136 | {
137 | "data": {
138 | "text/plain": [
139 | "{'id': 'sunbird-rc.registry.create',\n",
140 | " 'ver': '1.0',\n",
141 | " 'ets': 1645117010600,\n",
142 | " 'params': {'resmsgid': '',\n",
143 | " 'msgid': '7c5472ef-fd75-4b8f-8877-3cb2e0e31077',\n",
144 | " 'err': '',\n",
145 | " 'status': 'SUCCESSFUL',\n",
146 | " 'errmsg': ''},\n",
147 | " 'responseCode': 'OK',\n",
148 | " 'result': {'TrainingCertificate': {'osid': '1-8589ca85-ab48-4bcd-ad7c-9548553a0995'}}}"
149 | ]
150 | },
151 | "execution_count": 7,
152 | "metadata": {},
153 | "output_type": "execute_result"
154 | }
155 | ],
156 | "source": [
157 | "resp.json()\n"
158 | ]
159 | },
160 | {
161 | "cell_type": "code",
162 | "execution_count": 8,
163 | "id": "345cfbf2",
164 | "metadata": {},
165 | "outputs": [
166 | {
167 | "name": "stdout",
168 | "output_type": "stream",
169 | "text": [
170 | "{'id': 'sunbird-rc.registry.create', 'ver': '1.0', 'ets': 1645117010600, 'params': {'resmsgid': '', 'msgid': '7c5472ef-fd75-4b8f-8877-3cb2e0e31077', 'err': '', 'status': 'SUCCESSFUL', 'errmsg': ''}, 'responseCode': 'OK', 'result': {'TrainingCertificate': {'osid': '1-8589ca85-ab48-4bcd-ad7c-9548553a0995'}}}\n"
171 | ]
172 | },
173 | {
174 | "data": {
175 | "text/plain": [
176 | "(200, '85764892031')"
177 | ]
178 | },
179 | "execution_count": 8,
180 | "metadata": {},
181 | "output_type": "execute_result"
182 | }
183 | ],
184 | "source": [
185 | "print(resp.json())\n",
186 | "resp.status_code, userId\n"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": 9,
192 | "id": "e880f599",
193 | "metadata": {},
194 | "outputs": [
195 | {
196 | "name": "stdout",
197 | "output_type": "stream",
198 | "text": [
199 | "{\"@context\": [\"https://www.w3.org/2018/credentials/v1\", \"https://gist.githubusercontent.com/dileepbapat/eb932596a70f75016411cc871113a789/raw/498e5af1d94784f114b32c1ab827f951a8a24def/skill\"], \"type\": [\"VerifiableCredential\"], \"issuanceDate\": \"2021-08-27T10:57:57.237Z\", \"credentialSubject\": {\"type\": \"Person\", \"name\": \"Sunbird Learner\", \"trainedOn\": \"Sunbird RC Certificate Module\"}, \"issuer\": \"did:web:sunbirdrc.dev/vc/skill\", \"proof\": {\"type\": \"Ed25519Signature2018\", \"created\": \"2022-02-17T16:56:51Z\", \"verificationMethod\": \"did:india\", \"proofPurpose\": \"assertionMethod\", \"jws\": \"eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..NJOK8YrkkucvYFwcf_P6-0CIg3Lbhu5GnAtPwbjkuSLRlhwFKGk8oHkZszs7RxwCcz1DG-hCIispLvDx_0aSCA\"}}\n"
200 | ]
201 | }
202 | ],
203 | "source": [
204 | "resp = requests.get(\"%s/api/v1/%s/%s\"%(base_url, entity_name, idx), headers={\"Accept\":\"application/vc+ld+json\"})\n",
205 | "print(json.dumps(resp.json()))\n",
206 | "assert resp.json()[\"proof\"][\"type\"] == \"Ed25519Signature2018\"\n"
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": 10,
212 | "id": "932efa6f",
213 | "metadata": {},
214 | "outputs": [],
215 | "source": [
216 | "resp = requests.get(\"%s/api/v1/%s/%s\"%(base_url, entity_name, idx), headers={\"Accept\":\"application/pdf\"})\n"
217 | ]
218 | },
219 | {
220 | "cell_type": "code",
221 | "execution_count": 11,
222 | "id": "f6854518",
223 | "metadata": {},
224 | "outputs": [],
225 | "source": [
226 | "resp.status_code, resp.content\n",
227 | "\n",
228 | "assert resp.content[:5].decode().startswith(\"%PDF\")\n",
229 | "with open('sample.pdf', 'wb') as f:\n",
230 | " f.write(resp.content)\n",
231 | " "
232 | ]
233 | },
234 | {
235 | "cell_type": "code",
236 | "execution_count": 12,
237 | "id": "3fdae280",
238 | "metadata": {},
239 | "outputs": [
240 | {
241 | "data": {
242 | "text/plain": [
243 | "[]"
244 | ]
245 | },
246 | "execution_count": 12,
247 | "metadata": {},
248 | "output_type": "execute_result"
249 | }
250 | ],
251 | "source": [
252 | "%system open 'sample.pdf'\n"
253 | ]
254 | },
255 | {
256 | "cell_type": "code",
257 | "execution_count": 13,
258 | "id": "66ade90a",
259 | "metadata": {},
260 | "outputs": [
261 | {
262 | "data": {
263 | "text/plain": [
264 | "[]"
265 | ]
266 | },
267 | "execution_count": 13,
268 | "metadata": {},
269 | "output_type": "execute_result"
270 | }
271 | ],
272 | "source": [
273 | "resp = requests.get(\"%s/api/v1/%s/%s\"%(base_url, entity_name, idx), headers={\"Accept\":\"text/html\"})\n",
274 | "resp.status_code, resp.content\n",
275 | "\n",
276 | "assert resp.content[:5].decode().startswith(\" {
60 | console.log("checking " + url);
61 | const c = {
62 | "did:india": config.certificatePublicKey,
63 | "https://example.com/i/india": config.certificatePublicKey,
64 | "https://w3id.org/security/v1": contexts.get("https://w3id.org/security/v1"),
65 | 'https://www.w3.org/2018/credentials#': credentialsv1,
66 | "https://www.w3.org/2018/credentials/v1": credentialsv1,
67 | [testCertificateContextUrl]: testCertificateContext,
68 | };
69 | let context = c[url];
70 | if (context === undefined) {
71 | context = contexts[url];
72 | }
73 | if (context !== undefined) {
74 | return {
75 | contextUrl: null,
76 | documentUrl: url,
77 | document: context
78 | };
79 | }
80 | if (url.startsWith("{")) {
81 | return JSON.parse(url);
82 | }
83 | console.log("Fallback url lookup for document :" + url)
84 | return documentLoader({secure: false, strictSSL: false, request: request})(url);
85 | };
86 |
87 | export const CertificateStatus = ({certificateData, goBack}) => {
88 | const [isLoading, setLoading] = useState(false);
89 | const [isValid, setValid] = useState(false);
90 | const [data, setData] = useState({});
91 | const history = useHistory();
92 |
93 | const dispatch = useDispatch();
94 | useEffect(() => {
95 | setLoading(true);
96 | async function verifyData() {
97 | try {
98 | const signedJSON = JSON.parse(certificateData);
99 | const {AssertionProofPurpose} = jsigs.purposes;
100 | let result;
101 | debugger
102 | if (CERTIFICATE_SIGNED_KEY_TYPE === "RSA") {
103 | const publicKey = {
104 | '@context': jsigs.SECURITY_CONTEXT_URL,
105 | id: CERTIFICATE_DID,
106 | type: 'RsaVerificationKey2018',
107 | controller: CERTIFICATE_CONTROLLER_ID,
108 | publicKeyPem: config.certificatePublicKey
109 | };
110 | const controller = {
111 | '@context': jsigs.SECURITY_CONTEXT_URL,
112 | id: CERTIFICATE_CONTROLLER_ID,
113 | publicKey: [publicKey],
114 | // this authorizes this key to be used for making assertions
115 | assertionMethod: [publicKey.id]
116 | };
117 | const key = new RSAKeyPair({...publicKey});
118 |
119 | const {RsaSignature2018} = jsigs.suites;
120 | result = await jsigs.verify(signedJSON, {
121 | suite: new RsaSignature2018({key}),
122 | purpose: new AssertionProofPurpose({controller}),
123 | documentLoader: customLoader,
124 | compactProof: false
125 | });
126 | } else if (CERTIFICATE_SIGNED_KEY_TYPE === "ED25519") {
127 | const publicKey = {
128 | '@context': jsigs.SECURITY_CONTEXT_URL,
129 | id: CERTIFICATE_DID,
130 | type: 'Ed25519VerificationKey2018',
131 | controller: CERTIFICATE_CONTROLLER_ID,
132 | };
133 |
134 | const controller = {
135 | '@context': jsigs.SECURITY_CONTEXT_URL,
136 | id: CERTIFICATE_CONTROLLER_ID,
137 | publicKey: [publicKey],
138 | // this authorizes this key to be used for making assertions
139 | assertionMethod: [publicKey.id]
140 | };
141 |
142 | const purpose = new AssertionProofPurpose({
143 | controller: controller
144 | });
145 | const {Ed25519Signature2018} = jsigs.suites;
146 | const key = new Ed25519KeyPair(
147 | {
148 | publicKeyBase58: certificatePublicKeyBase58,
149 | id: CERTIFICATE_DID
150 | }
151 | );
152 | result = await vc.verifyCredential({
153 | credential: signedJSON,
154 | suite: new Ed25519Signature2018({key}),
155 | purpose: purpose,
156 | documentLoader: customLoader,
157 | compactProof: false
158 | });
159 | }
160 | if (result.verified) {
161 | const revokedResponse = await checkIfRevokedCertificate(signedJSON)
162 | if (revokedResponse.response.status !== 200) {
163 | console.log('Signature verified.');
164 | setValid(true);
165 | setData(signedJSON);
166 | dispatch(addEventAction({
167 | type: EVENT_TYPES.VALID_VERIFICATION,
168 | extra: signedJSON.credentialSubject
169 | }));
170 | setLoading(false);
171 | return;
172 | }
173 | }
174 | dispatch(addEventAction({type: EVENT_TYPES.INVALID_VERIFICATION, extra: signedJSON}));
175 | setValid(false);
176 | setLoading(false);
177 | } catch (e) {
178 | console.log('Invalid data', e);
179 | setValid(false);
180 | dispatch(addEventAction({type: EVENT_TYPES.INVALID_VERIFICATION, extra: certificateData}));
181 | setLoading(false);
182 | }
183 |
184 | }
185 | setTimeout(() => {
186 | verifyData()
187 | }, 500)
188 |
189 | }, []);
190 |
191 | async function checkIfRevokedCertificate(data) {
192 | return axios
193 | .post("/divoc/api/v1/certificate/revoked", data)
194 | .then((res) => {
195 | dispatch(addEventAction({type: EVENT_TYPES.REVOKED_CERTIFICATE, extra: certificateData}));
196 | return res
197 | }).catch((e) => {
198 | console.log(e);
199 | return e
200 | });
201 | }
202 |
203 | function getCertificateStatusAsString(data) {
204 | if (!data || !data["evidence"]) {
205 | return ""
206 | }
207 |
208 | const dose = data["evidence"][0]["dose"]
209 | const totalDoses = data["evidence"][0]["totalDoses"] || 2
210 |
211 | if (dose === totalDoses) {
212 | return "Final Certificate for COVID-19 Vaccination"
213 | } else {
214 | return `Provisional Certificate for COVID-19 Vaccination (${getDose(data)} Dose)`
215 | }
216 | }
217 |
218 | function getDose(data) {
219 | if (!data || !data["evidence"]) {
220 | return ""
221 | }
222 | return ordinal_suffix_of(data["evidence"][0]["dose"])
223 | }
224 |
225 | return (
226 | isLoading ? :
227 |

229 |
230 | {
231 | isValid ? "Certificate Successfully Verified" : "Certificate Invalid"
232 | }
233 |
234 |
235 | {
236 | isValid &&
Training Certificate
237 | }
238 | {
239 | isValid &&
240 | {
241 | Object.keys(TestCertificateDetailsPaths).map((key, index) => {
242 | const context = TestCertificateDetailsPaths[key];
243 | return (
244 |
245 | | {key} |
246 | {context.format(pathOr("NA", context.path, data))} |
247 |
248 | )
249 | })
250 | }
251 |
252 |
253 | }
254 |
255 |
Verify Another Certificate
256 | {/*
{*/}
258 | {/* history.push("/side-effects")*/}
259 | {/* }}*/}
260 | {/* img={FeedbackSmallImg} backgroundColor={"#FFFBF0"}/>*/}
261 | {/* {*/}
263 | {/* history.push("/learn")*/}
264 | {/* }}*/}
265 | {/* backgroundColor={"#EFF5FD"}/>*/}
266 |
267 | )
268 | };
269 |
270 | export const SmallInfoCards = ({text, img, onClick, backgroundColor}) => (
271 |
272 |
273 |

274 |
275 |
277 |
{text}
278 |

279 |
280 |
281 | );
282 |
--------------------------------------------------------------------------------
/sample.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
Certificate of Training
10 |
11 |
12 | |
13 |
14 | This is to certify that
15 | Sunbird Learner
16 | has successfully completed training requirements for
17 | Sunbird RC Certificate Module
18 | and is awarded this certificate on
19 | Thursday, February 17th 2022
20 |
21 |
22 | |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------