├── 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 |
qr_code 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 | banner-img 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 | 246 | 247 | 248 | ) 249 | }) 250 | } 251 | 252 |
{key}{context.format(pathOr("NA", context.path, data))}
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 |
qr_code 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 | --------------------------------------------------------------------------------