Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# qrcodegenerator
|
||||
|
||||
This utility is for generating QR Code for any text / url.
|
||||
|
||||
## clonning and Installation
|
||||
```
|
||||
git clone https://github.com/developedbysom/qrcodegenerator.git
|
||||
cd qrcodegenerator
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
const express = require("express");
|
||||
const axios = require('axios');
|
||||
const ejs = require("ejs");
|
||||
const path = require("path");
|
||||
const qrcode = require("qrcode");
|
||||
const exp = require("constants");
|
||||
|
||||
const html5QrcodeScanner = require("html5-qrcode");
|
||||
|
||||
const { DCC } = require('dcc-utils/src');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.port || 3000;
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.set("views", path.join(__dirname, "view"));
|
||||
|
||||
app.use(express.static("public"));
|
||||
|
||||
app.get("/", (req, res, next) => {
|
||||
res.render("index");
|
||||
});
|
||||
|
||||
app.post("/scan", (req, res, next) => {
|
||||
|
||||
const input_text = req.body.cert;
|
||||
|
||||
DCC.fromRaw(input_text).then((dcc) => {
|
||||
console.log(dcc.payload)
|
||||
//console.log(dcc.raw)
|
||||
res.status(200);
|
||||
res.send(dcc.payload);
|
||||
|
||||
|
||||
//console.log(dcc);
|
||||
})
|
||||
.catch(error =>{
|
||||
res.status(500);
|
||||
res.send('Error: invalid data');
|
||||
});
|
||||
});
|
||||
app.post("/sendc19", (req, res, next) => {
|
||||
|
||||
const output_text = req.body.contenuto;
|
||||
console.log(output_text);
|
||||
const optionsf2 = {
|
||||
method: 'POST',
|
||||
// mode: 'no-cors',
|
||||
body: output_text,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
axios
|
||||
.post('https://ufficio.egalware.com/GPW/Api/api/VC19', output_text)
|
||||
.then(res => {
|
||||
console.log(`statusCode: ${res.status}`)
|
||||
console.log(res)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
res.status(200);
|
||||
|
||||
res.send('Recorded');
|
||||
|
||||
});
|
||||
app.listen(port, console.log(`Listening on port ${port}`));
|
||||
Generated
+20987
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "qrcodeapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"proxy": "http://localhost:3000",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^0.23.0",
|
||||
"dcc-utils": "^0.3.0",
|
||||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"html5-qrcode": "^2.1.0",
|
||||
"qrcode": "^1.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.4.1"
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,98 @@
|
||||
const fs = require('fs');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const Jimp = require('jimp');
|
||||
const jsQR = require('jsqr');
|
||||
const base45 = require('base45');
|
||||
const cbor = require('cbor');
|
||||
const cose = require('cose-js');
|
||||
const rs = require('jsrsasign');
|
||||
const { verify, webcrypto, SignatureMismatchError } = require('cosette/build/sign');
|
||||
|
||||
class DCC {
|
||||
static async fromRaw(certificateRaw) {
|
||||
const dcc = new DCC();
|
||||
dcc._raw = certificateRaw;
|
||||
const base45Data = base45.decode(certificateRaw.slice(4));
|
||||
dcc._coseRaw = zlib.inflateSync(base45Data);
|
||||
const cborPayload = cbor.decodeFirstSync(dcc._coseRaw).value[2];
|
||||
const jsonCBOR = cbor.decodeFirstSync(cborPayload);
|
||||
dcc._payload = jsonCBOR.get(-260).get(1);
|
||||
return dcc;
|
||||
}
|
||||
|
||||
static async fromImage(certificateImagePath) {
|
||||
const buffer = fs.readFileSync(certificateImagePath);
|
||||
const image = await Jimp.read(buffer);
|
||||
const code = jsQR(image.bitmap.data, image.bitmap.width, image.bitmap.height);
|
||||
return DCC.fromRaw(code.data);
|
||||
}
|
||||
|
||||
get raw() {
|
||||
return this._raw;
|
||||
}
|
||||
|
||||
get payload() {
|
||||
return this._payload;
|
||||
}
|
||||
|
||||
async checkSignature(signatureKey) {
|
||||
const verifier = {
|
||||
key: signatureKey,
|
||||
};
|
||||
return cose.sign.verify(this._coseRaw, verifier);
|
||||
}
|
||||
|
||||
async checkSignatureWithCertificate(certificate) {
|
||||
const key = rs.KEYUTIL.getKey(certificate);
|
||||
let verifier;
|
||||
if (key.type === 'EC') {
|
||||
verifier = key.getPublicKeyXYHex();
|
||||
} else if (key.type === 'RSA') {
|
||||
const jwk = rs.KEYUTIL.getJWKFromKey(key);
|
||||
verifier = {
|
||||
n: Buffer.from(jwk.n, 'base64'),
|
||||
e: Buffer.from(jwk.e, 'base64'),
|
||||
};
|
||||
} else {
|
||||
throw new Error('Certificate not supported');
|
||||
}
|
||||
return cose.sign.verify(this._coseRaw, { key: verifier });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param keys associative array of keys, [kid => {key}]
|
||||
* @returns {Promise<boolean|*>} return a promise, if the certificate is validly signed with
|
||||
* a listed key, return the key with infos about authority that signed it. If the certificate
|
||||
* is not validly signed, return false. If the keys is not in list, throw Error.
|
||||
*/
|
||||
async checkSignatureWithKeysList(keys) {
|
||||
let cert;
|
||||
try {
|
||||
await verify(this._coseRaw, async (kid) => {
|
||||
cert = keys[kid.toString('base64')];
|
||||
return {
|
||||
key: await webcrypto.subtle.importKey(
|
||||
'spki',
|
||||
Buffer.from(cert.publicKeyPem, 'base64'),
|
||||
cert.publicKeyAlgorithm,
|
||||
true, ['verify'],
|
||||
),
|
||||
};
|
||||
});
|
||||
return cert;
|
||||
} catch (e) {
|
||||
if (e instanceof SignatureMismatchError) {
|
||||
return false;
|
||||
}
|
||||
if (typeof cert === 'undefined') {
|
||||
throw new Error('Cannot verify signature: the key that signed the certificate is not listed',
|
||||
{ cause: e });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DCC;
|
||||
@@ -0,0 +1,121 @@
|
||||
function docReady(fn) {
|
||||
// see if DOM is already available
|
||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||
// call on next available tick
|
||||
setTimeout(fn, 1);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", fn);
|
||||
}
|
||||
}
|
||||
certClass = function(rawVal) {
|
||||
this.cert = rawVal;
|
||||
};
|
||||
docReady(function() {
|
||||
var resultContainer = document.getElementById('qr-reader-results');
|
||||
var lastResult, countResults = 0;
|
||||
|
||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
"qr-reader", { fps: 20, qrbox: 250 });
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
if (decodedText !== lastResult) {
|
||||
++countResults;
|
||||
lastResult = decodedText;
|
||||
|
||||
input_text = 'HC1:6BFOXN%TS3DHPVO13J /G-/2YRVA.QKW8SFBXG4CH23IRM*4Z8EHLTKQC:3DCV4*XUA2PSGH.+HIMIBRU SITK292W7*RBT1KCGTHQSEQBKLP64-HQ/HQ3IRE+QJDO-B5ET42HPPEPHCR6W9FDON95U/3-58 KE2+GKHG:3D6JK9+GFKMWKN7JJEHGRHHIUJR.KL.KR+G/IKM*G1JJ*KMO-K3OM.IA.C8KRDL4O54O4IGUJKAHIYIABGEX3E1.BLEE$JDG2OFI9L+93NKF9E3-9TC90JA4E15IAXMFU*GZEG-8A%FGSKE MCTPI8%MDPIW7CR5KBBQFZMD11-97S75JWTE.S$7K0:JSCCV7J$%25I3HC31835AL5:4A93OHBIFT.EJDG3L*8B89T3CT7WJI4WCW9WQ$LNU3OU72CWQIU7JBW.22S+5WIA%5RR-ND6RBTE-*6LYQN440Q82TPS2PH1S4%7+V0Q3S0C70T6J3V3VH3Y4430G*5B0';
|
||||
//console.log(input_text);
|
||||
const myBody = new certClass(input_text);
|
||||
const jsonBody= JSON.stringify(myBody);
|
||||
// console.log(jsonBody);
|
||||
// request options
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: jsonBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
var obj='';
|
||||
fetch('/scan', options).then(function (res) {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
// res => res.json();
|
||||
// risposta = res.json();
|
||||
// html5QrcodeScanner.clear();
|
||||
// console.log(risposta);
|
||||
// resultContainer.innerHTML += `<div style="background-color:green;text-align:center;padding:20px"><h1>Scansione corretta</h1> <h3> ${risposta.nam.fn} ${risposta.nam.gn} ${risposta.dob}</h3></div>`;
|
||||
} else {
|
||||
return Promise.reject(res);
|
||||
}
|
||||
})
|
||||
.then(function (risposta)
|
||||
{
|
||||
html5QrcodeScanner.clear();
|
||||
// console.log(risposta);
|
||||
resultContainer.innerHTML += `<div style="background-color:green;text-align:center;padding:20px"><h1>Scansione corretta</h1> <h3> ${risposta.nam.fn} ${risposta.nam.gn} ${risposta.dob}</h3></div>`;
|
||||
return risposta;
|
||||
}
|
||||
)
|
||||
.then(function (data) {
|
||||
|
||||
// Store the post data to a variable
|
||||
// post = data;
|
||||
// contenuto = data;
|
||||
var myObject = new Object();
|
||||
myObject.contenuto = data;
|
||||
const jsonBodyv2 = JSON.stringify(myObject);
|
||||
|
||||
const data2 = {
|
||||
method: 'POST',
|
||||
body: jsonBodyv2,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
// log 01
|
||||
console.log('log1');
|
||||
console.log(data2);
|
||||
// log 02
|
||||
console.log('log2');
|
||||
console.log(jsonBodyv2);
|
||||
// .then(res => console.log(data2));
|
||||
// Fetch another API
|
||||
return fetch('/sendc19', data2);
|
||||
// .then(res => console.log(res));
|
||||
|
||||
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
window.setTimeout(function(){location.reload()},5000);
|
||||
return response;
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
// }).then(function (data) {
|
||||
// console.log(post, data);
|
||||
}).catch(function (error) {
|
||||
console.warn(error);
|
||||
});
|
||||
// fetch('/scan', options)
|
||||
// .then(res => res.json())
|
||||
// .then(res =>
|
||||
// {
|
||||
// html5QrcodeScanner.clear();
|
||||
// console.log(res);
|
||||
// resultContainer.innerHTML += `<div style="background-color:green;text-align:center;padding:20px"><h1>Scansione corretta</h1> <h3> ${res.nam.fn} ${res.nam.gn} ${res.dob}</h3></div>`;
|
||||
// return fetch('/sendc19',res);
|
||||
// //
|
||||
// // this.dccress = res;
|
||||
// })
|
||||
// .catch(err => console.error(err));
|
||||
}
|
||||
}
|
||||
// Optional callback for error, can be ignored.
|
||||
function onScanError(qrCodeError) {
|
||||
// This callback would be called in case of qr code scan error or setup error.
|
||||
// You can avoid this callback completely, as it can be very verbose in nature.
|
||||
}
|
||||
|
||||
html5QrcodeScanner.render(onScanSuccess, onScanError);
|
||||
});
|
||||
+7
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
||||
const DCC = require('./dcc');
|
||||
const Rule = require('./rule');
|
||||
|
||||
module.exports = {
|
||||
DCC, Rule,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
function docReady(fn) {
|
||||
// see if DOM is already available
|
||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||
// call on next available tick
|
||||
setTimeout(fn, 1);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", fn);
|
||||
}
|
||||
}
|
||||
certClass = function(rawVal) {
|
||||
this.cert = rawVal;
|
||||
};
|
||||
docReady(function() {
|
||||
var resultContainer = document.getElementById('qr-reader-results');
|
||||
var lastResult, countResults = 0;
|
||||
|
||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
||||
"qr-reader", { fps: 20, qrbox: 250 });
|
||||
|
||||
function onScanSuccess(decodedText, decodedResult) {
|
||||
if (decodedText !== lastResult) {
|
||||
++countResults;
|
||||
lastResult = decodedText;
|
||||
|
||||
input_text = 'HC1:6BFOXN%TS3DHPVO13J /G-/2YRVA.QKW8SFBXG4CH23IRM*4Z8EHLTKQC:3DCV4*XUA2PSGH.+HIMIBRU SITK292W7*RBT1KCGTHQSEQBKLP64-HQ/HQ3IRE+QJDO-B5ET42HPPEPHCR6W9FDON95U/3-58 KE2+GKHG:3D6JK9+GFKMWKN7JJEHGRHHIUJR.KL.KR+G/IKM*G1JJ*KMO-K3OM.IA.C8KRDL4O54O4IGUJKAHIYIABGEX3E1.BLEE$JDG2OFI9L+93NKF9E3-9TC90JA4E15IAXMFU*GZEG-8A%FGSKE MCTPI8%MDPIW7CR5KBBQFZMD11-97S75JWTE.S$7K0:JSCCV7J$%25I3HC31835AL5:4A93OHBIFT.EJDG3L*8B89T3CT7WJI4WCW9WQ$LNU3OU72CWQIU7JBW.22S+5WIA%5RR-ND6RBTE-*6LYQN440Q82TPS2PH1S4%7+V0Q3S0C70T6J3V3VH3Y4430G*5B0';
|
||||
//console.log(input_text);
|
||||
const myBody = new certClass(input_text);
|
||||
const jsonBody= JSON.stringify(myBody);
|
||||
// console.log(jsonBody);
|
||||
// request options
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: jsonBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
var obj='';
|
||||
fetch('/scan', options)
|
||||
.then(res => res.json())
|
||||
// .then(res => console.log(res))
|
||||
.then(res =>
|
||||
{
|
||||
html5QrcodeScanner.clear();
|
||||
console.log(res);
|
||||
resultContainer.innerHTML += `<div style="background-color:green;text-align:center;padding:20px"><h1>Scansione corretta</h1> <h3> ${res.nam.fn} ${res.nam.gn} ${res.dob}</h3></div>`;
|
||||
//window.setTimeout(function(){location.reload()},15000);
|
||||
// this.dccress = res;
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
}
|
||||
// Optional callback for error, can be ignored.
|
||||
function onScanError(qrCodeError) {
|
||||
// This callback would be called in case of qr code scan error or setup error.
|
||||
// You can avoid this callback completely, as it can be very verbose in nature.
|
||||
}
|
||||
|
||||
html5QrcodeScanner.render(onScanSuccess, onScanError);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const fs = require('fs');
|
||||
const certLogicJs = require('certlogic-js');
|
||||
|
||||
class Rule {
|
||||
static fromFile(filePath, external = {}) {
|
||||
return Rule.fromJSON(JSON.parse(fs.readFileSync(filePath)), external);
|
||||
}
|
||||
|
||||
static fromJSON(ruleJSON, external = {}) {
|
||||
const rule = new Rule();
|
||||
rule._external = external;
|
||||
rule._payload = ruleJSON;
|
||||
return rule;
|
||||
}
|
||||
|
||||
get payload() {
|
||||
return this._payload;
|
||||
}
|
||||
|
||||
getDescription(language = 'en') {
|
||||
const description = this._payload.Description.find((element) => element.lang === language);
|
||||
return description ? description.desc : null;
|
||||
}
|
||||
|
||||
evaluateDCC(dcc, external = {}) {
|
||||
const options = { ...this._external, ...external };
|
||||
return certLogicJs.evaluate(this.payload.Logic, {
|
||||
payload: dcc.payload,
|
||||
external: options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Rule;
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: rgb(66, 159, 235);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: rgb(13, 141, 9);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Response scan</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/styleok.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
setTimeout(function(){
|
||||
window.location.href = 'http://localhost:3000';
|
||||
}, 5000);
|
||||
</script>
|
||||
<div class="container">
|
||||
<h1>Scansione corretta</h1>
|
||||
<h3><%=jsonBody%></h3>
|
||||
<!-- <img src=<%=qr_code %> alt="QR Code"> -->
|
||||
<br>
|
||||
<!-- <a href="/" role="button" class="btn btn-primary">Back</a> -->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Code Reader Green Pass Check</title>
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/style.css" type="text/css">
|
||||
<script src="/html5-qrcode.min.js"></script>
|
||||
<script src="/html5-qrcode-demo.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>QR Code Reader Green Pass Check</h1>
|
||||
<hr>
|
||||
<div style="width: 500px" id="qr-reader"></div>
|
||||
<br>
|
||||
<!-- <form action="/scan" method="POST" class="form">
|
||||
<textarea name="text" id="text" cols="120" rows="10" placeholder="Enter URL or Text" required></textarea>
|
||||
<button type="submit" class="btn btn-primary">Check QR Code</button>
|
||||
|
||||
</form> -->
|
||||
<div id="qr-reader-results"></div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Code</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<img src=<%=qr_code %> alt="QR Code">
|
||||
<br>
|
||||
<a href="/" role="button" class="btn btn-primary">Back</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QR Code</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<img src=<%=qr_code %> alt="QR Code">
|
||||
<br>
|
||||
<a href="/" role="button" class="btn btn-primary">Back</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user