restructure stuff, add logging, use node 20

This commit is contained in:
Aljaž Starc
2023-10-23 22:54:08 +02:00
parent acfa8461e5
commit 5323504a87
11 changed files with 16 additions and 18 deletions

146
views/form.ejs Normal file
View File

@@ -0,0 +1,146 @@
<!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>UPN-QR :: FORM</title>
<link rel="stylesheet" href="/public/style.css">
</head>
<style>
div {
display: grid;
}
.card {
row-gap: 1rem
}
.title {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min-content, 1fr));
align-content: center;
justify-content: center;
padding: 1rem;
column-gap: 1rem;
text-align: center;
}
.title > * {
margin: auto;
}
.form {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
row-gap: .5rem;
column-gap: 1rem;;
}
.code {
display: grid;
grid-template-rows: auto auto;
grid-template-columns: auto;
row-gap: 1rem;
background-size: contain;
background-position: top center;
background-repeat: no-repeat;
background-image: url("/public/invalid-content.png");
min-height: 210px;
min-width: 210px;
}
@media screen and (min-width: 700px) {
.card {
grid-template-columns: min-content 1fr;
column-gap: 1rem;
}
.title { grid-column: span 2; }
}
</style>
<body>
<div class="card">
<div class="title">
<h1>Form: <%= q["title"] %></h1>
<span>Create your form here: <a href="/#maker">/#maker</a></span>
</div>
<div class="code" id="qrcode"></div>
<div class="form">
<%
const items = [
{ n: "client-name", d: "Client name", p: "Peter Novak", t: "text" },
{ n: "client-address", d: "Client address", p: "Ravna Ulica 13", t: "text" },
{ n: "client-city", d: "Client city", p: "1000 Ljubljana", t: "text" },
{ n: "issuer-name", d: "Issuer name", p: "Spletne strani n'123", t: "text" },
{ n: "issuer-address", d: "Issuer address", p: "Za deveto smreko 15 k", t: "text" },
{ n: "issuer-city", d: "Issuer city", p: "1000 Ljubljana", t: "text" },
{ n: "iban", d: "IBAN", p: "SI56047500000280672", t: "text" },
{ n: "amount", d: "Amount", p: "35090 (350.90€)", t: "number" },
{ n: "code", d: "Code", p: "OTHR", t: "text" },
{ n: "purpose", d: "Purpose", p: "moutain bike first half", t: "text" },
{ n: "reference", d: "Reference", p: "SI121234567890120", t: "text" },
{ n: "deadline", d: "Deadline", p: "01.02.2034", t: "text" }
]
%>
<% for (const i of items) { %>
<%
const str = String(q[i.n])
const readonly = (str != "" && !str.includes("*") ? "readonly" : "")
const val = str.replace("*", "")
%>
<div>
<label for="<%= i.n %>"><%= i.d %></label>
<input value="<%= val %>" <%= readonly %> type="<%= i.t %>" placeholder="<%= i.p %>" name="<%= i.n %>">
</div>
<% } %>
<!-- <div>
<input type="submit" value="Update details" id="update-details">
</div> -->
</div>
</div>
</body>
<script>
const qrcode = document.getElementById("qrcode")
function val (name) { return document.getElementsByName(name)?.[0]?.value || "" }
let debounce = null
function updateQRdeb () {
clearTimeout(debounce)
debounce = setTimeout(() => {
clearTimeout(debounce)
updateQR()
}, 500);
}
function getNewUrl () {
const qstring = [
["client_name", val("client-name")],
["client_address", val("client-address")],
["client_city", val("client-city")],
["amount", val("amount").padStart(11, "0")],
["purpose_code", val("code")],
["payment_purpose", val("purpose")],
["iban", val("iban")],
["reference", val("reference")],
["issuer_name", val("issuer-name")],
["issuer_address", val("issuer-address")],
["issuer_city", val("issuer-city")],
["deadline", val("deadline")]
].map(v => `${v[0]}=${v[1]}`).join("&")
return `${window.location.origin}/api/qrcode?${qstring}`
}
function updateQR (e) {
try {
const preloadImg = new Image();
const newurl = getNewUrl()
preloadImg.addEventListener("load", () => qrcode.style.setProperty('background-image', `url("${newurl}"), url("/public/invalid-content.png")`))
preloadImg.addEventListener("error", () => qrcode.style.setProperty('background-image', `url("/public/invalid-content.png")`))
preloadImg.src=newurl;
} catch (error) {
setImgUrl(`url("/public/invalid-content.png")`)
}
}
// Add input listener to all inputs
for (el of document.getElementsByTagName("input")) {
el.addEventListener("input", updateQRdeb)
}
updateQR()
</script>
</html>

364
views/index.ejs Normal file
View File

@@ -0,0 +1,364 @@
<!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>UPN-QR | Open generator for UPN QR codes</title>
<link rel="stylesheet" href="/public/style.css">
</head>
<style>
section div {
padding-left: 25px;
display: grid;
}
.credits {
display: grid;
align-items: center;
justify-content: center;
text-align: center;
}
.maker {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
row-gap: .5rem;
column-gap: .5rem;
}
.code {
grid-column: 1;
background-size: contain;
background-position: top center;
background-repeat: no-repeat;
background-image: url("/public/invalid-content.png");
min-height: 210px;
min-width: 210px;
}
.maker button,
.maker input[type="submit"] {
margin-bottom: auto;
}
</style>
<body>
<section>
<h1>About</h1>
<p>
This API was developed for personal use, but modified and later released for public use.
It is an unofficial application with the sole purpose of simplifying QR code generation for universal payment ordes (<a href="https://upn-qr.si">https://upn-qr.si</a>).
You can read the specification <a href="/public/NavodilaZaProgramerjeUPNQR.pdf">here</a>.
The application does not keep any long-term logs.
</p>
</section>
<section>
<h1 id="maker"><a href="#maker">&#128279;</a> Form maker</h1>
<span>Fill in the fields with reciever's bank information and leave empty the ones that user has to fill out.</span>
<form action="/form" method="get" class="maker">
<div>
<label for="title">Form title</label>
<input type="text" placeholder="First half of the bike" name="title">
</div>
<div>
<label for="client-name">Client name</label>
<input type="text" placeholder="Peter Novak" name="client-name">
</div>
<div>
<label for="client-address">Client address</label>
<input type="text" placeholder="Ravna ulica 13 a" name="client-address">
</div>
<div>
<label for="client-city">Client city</label>
<input type="text" placeholder="1000 Ljubljana" name="client-city">
</div>
<div>
<label for="amount">Amount</label>
<input type="number" placeholder="00000001132" min="0" max="99999999999" name="amount">
</div>
<div>
<label for="deadline">Deadline</label>
<input type="text" placeholder="01.02.2034" min=0 max=10 name="deadline">
</div>
<div>
<label for="code">Purpose code</label>
<input type="text" placeholder="OTHR" name="code">
</div>
<div>
<label for="purpose">Payment purpose</label>
<input type="text" placeholder="Moutain bike first half" name="purpose">
</div>
<div>
<label for="iban">IBAN</label>
<input type="text" placeholder="SI56047500000280672" name="iban">
</div>
<div>
<label for="reference">Reference</label>
<input type="text" placeholder="SI121234567890120" name="reference">
</div>
<div>
<label for="issuer-name">Issuer name</label>
<input type="text" placeholder="Kolesarstvo Hrib" name="issuer-name">
</div>
<div>
<label for="issuer-address">Issuer address</label>
<input type="text" placeholder="Za deveto smreko 15 k" name="issuer-address">
</div>
<div>
<label for="issuer-city">Issuer city</label>
<input type="text" placeholder="1000 Ljubljana" name="issuer-city">
</div>
<div class="code" id="qrcode"></div>
<div>
<button id="copyurl" type="button">Copy image URL</button>
</div>
<div>
<input type="submit" value="Create form">
</div>
</div>
</section>
<script>
function val (name) { return encodeURIComponent(document.getElementsByName(name)?.[0]?.value) || "" }
function getNewUrl () {
const qstring = [
["client_name", val("client-name")],
["client_address", val("client-address")],
["client_city", val("client-city")],
["amount", val("amount").padStart(11, "0")],
["purpose_code", val("code")],
["payment_purpose", val("purpose")],
["iban", val("iban")],
["reference", val("reference")],
["issuer_name", val("issuer-name")],
["issuer_address", val("issuer-address")],
["issuer_city", val("issuer-city")],
["deadline", val("deadline")],
].map(v => `${v[0]}=${v[1]}`).join("&")
return encodeURI(`${window.location.origin}/api/qrcode?${qstring}`)
}
const copyBtn = document.getElementById("copyurl")
copyBtn.addEventListener("click", () => copyUrl())
function copyUrl () {
try {
const dummy = document.createElement('input')
document.body.appendChild(dummy)
dummy.value = getNewUrl()
dummy.select()
document.execCommand('copy')
document.body.removeChild(dummy)
copyBtn.innerText = "Copied!"
copyBtn.classList.add("success")
setTimeout(() => {
copyBtn.innerText = "Copy image URL"
copyBtn.classList.remove("success")
}, 750)
} catch (error) {
console.error(error)
copyBtn.innerText = "Failed!"
copyBtn.classList.add("error")
setTimeout(() => {
copyBtn.innerText = "Copy image URL"
copyBtn.classList.remove("error")
}, 750)
}
}
const qrcode = document.getElementById("qrcode")
function val (name) { return document.getElementsByName(name)?.[0]?.value || "" }
let debounce = null
function updateQRdeb () {
clearTimeout(debounce)
debounce = setTimeout(() => {
clearTimeout(debounce)
updateQR()
}, 500);
}
function updateQR (e) {
try {
const preloadImg = new Image();
const newurl = getNewUrl()
preloadImg.addEventListener("load", () => qrcode.style.setProperty('background-image', `url("${newurl}"), url("/public/invalid-content.png")`))
preloadImg.addEventListener("error", () => qrcode.style.setProperty('background-image', `url("/public/invalid-content.png")`))
preloadImg.src=newurl;
} catch (error) {
setImgUrl(`url("/public/invalid-content.png")`)
}
}
// Add input listener to all inputs
for (el of document.getElementsByTagName("input")) {
el.addEventListener("input", updateQRdeb)
}
updateQR()
</script>
<section>
<h1 id="api"><a href="#api">&#128279;</a> API</h1>
<h4 id="api-qrcode"><a href="#api-qrcode">&#128279;</a> <code>GET /api/qrcode</code></h4>
<div>
<span>Returns a <code>image/png</code> image if successful (OK 200), else returns the following JSON <code>{ ok: false, errors: String[] }</code> where <code>errors</code> is an array of descriptive strings.</span>
<br>
<pre>
&#x3C;!-- Meant to be used as direct image source, for example --&#x3E;
&#x3C;img src="https://upn-qr.gitapp.si/api/qrcode?client_name=Dobri človek&client_address=Kristanova ulica 1&client_city=1000 Ljubljana&amount=00000001000&deadline=01.02.2034&payment_purpose=Donacija&iban=SI56021400015556761&reference=SI99&issuer_name=Slovenska Karitas&issuer_address=Kristanova ulica 1&issuer_city=1000 Ljubljana"&#x3E;
</pre>
<img src="/api/qrcode?client_name=Dobri človek&client_address=Kristanova ulica 1&client_city=1000 Ljubljana&amount=00000001000&deadline=01.02.2034&payment_purpose=Donacija&iban=SI56021400015556761&reference=SI99&issuer_name=Slovenska Karitas&issuer_address=Kristanova ulica 1&issuer_city=1000 Ljubljana">
</div>
<br>
<div>
<b>Query parameters:</b>
<p>Following the specification from official documentation - <a href="https://www.upn-qr.si/uploads/files/NavodilaZaProgramerjeUPNQR.pdf">NavodilaZaProgramerjeUPNQR.pdf</a>, section <code>4. Vsebina kode QR</code>.</p>
<h5 id="api-qrcode-client_name"><a href="#api-qrcode-client_name">&#128279;</a> <code>client_name</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ'](?:[A-Z0-9 ČŠŽĐ']{0,31}[A-Z0-9ČŠŽĐ'])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/pNmOI0/1">regex101.com/r/pNmOI0/1</a></span>
<br>
<span>Description: Name and surname of the client. Max length 33 characters including spaces and numbers.</span>
<br>
<span>Example: Peter Novak</span>
</div>
<h5 id="api-qrcode-client_address"><a href="#api-qrcode-client_address">&#128279;</a> <code>client_address</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ](?:[A-Z0-9 ČŠŽĐ]{0,31}[A-Z0-9ČŠŽĐ])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/JA4wmM/1">regex101.com/r/JA4wmM/1</a></span>
<br>
<span>Description: Full client address in long form. Max length 33 characters including spaces and numbers.</span>
<br>
<span>Example: Ravna ulica 13 a</span>
</div>
<h5 id="api-qrcode-client_city"><a href="#api-qrcode-client_city">&#128279;</a> <code>client_city</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ](?:[A-Z0-9 ČŠŽĐ]{0,31}[A-Z0-9ČŠŽĐ])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/pK3oEm/1">regex101.com/r/5QMpTn/1</a></span>
<br>
<span>Description: Client's city including postal code</span>
<br>
<span>Example: 1000 Ljubljana</span>
</div>
<h5 id="api-qrcode-amount"><a href="#api-qrcode-amount">&#128279;</a> <code>amount</code></h5>
<div>
<span>Regex: <code>^(?=.{11}$)[0]{1,11}[0-9]{0,11}$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/Tyq5S1/1">regex101.com/r/Tyq5S1/1</a></span>
<br>
<span>Description: Total amount to pay. Must contain 11 numbers. Last two numbers are decimal places (cents).</span>
<br>
<span>Example (11,32€): 00000001132</span>
</div>
<h5 id="api-qrcode-deadline"><a href="#api-qrcode-deadline">&#128279;</a> <code>deadline</code></h5>
<div>
<span>Regex: <code>^[0-9]{2}\.[0-9]{2}\.[0-9]{4}$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/JDZT9P/1">regex101.com/r/JDZT9P/1</a></span>
<br>
<span>Description: Payment deadline. Field is optional. Format DD.MM.YYYY</span>
<br>
<span>Example: 01.02.2034</span>
</div>
<h5 id="api-qrcode-purpose_code"><a href="#api-qrcode-purpose_code">&#128279;</a> <code>purpose_code</code></h5>
<div>
<span>Regex: <code>^[A-Z]{4}$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/TsiZQJ/1">regex101.com/r/TsiZQJ/1</a></span>
<br>
<span>Default: OTHR</span>
<br>
<span>Description: Must contain 4 uppercase characters compliant with public purpose code library <a href="https://www.nlb.si/sepa-koda-namena-prebivalstvo">www.nlb.si/sepa-koda-namena-prebivalstvo</a></span>
<br>
<span>Example: OTHR</span>
</div>
<h5 id="api-qrcode-payment_purpose"><a href="#api-qrcode-payment_purpose">&#128279;</a> <code>payment_purpose</code></h5>
<div>
<span>Regex: <code>^[A-Z0-9ČŠŽĐ](?:[A-Z0-9 ČŠŽĐ\-:;_'"]{0,40}[A-Z0-9ČŠŽĐ])?$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/egl24t/1">regex101.com/r/egl24t/1</a></span>
<br>
<span>Description: Can contain any character including ŠČŽĐ and space. Max length 33 characters including space.</span>
<br>
<span>Example: Moutain bike first half</span>
</div>
<h5 id="api-qrcode-iban"><a href="#api-qrcode-iban">&#128279;</a> <code>iban</code></h5>
<div>
<span>Regex: <code>^[A-Z]{2}\d{17}$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/8bXDvh/1">regex101.com/r/8bXDvh/1</a></span>
<br>
<span>Description: Must contain country code (exp. SI56). No spaces allowed. Max length 34 characters</span>
<br>
<span>Example: SI56047500000280672</span>
</div>
<h5 id="api-qrcode-reference"><a href="#api-qrcode-reference">&#128279;</a> <code>reference</code></h5>
<div>
<span>Regex: <code>^[A-Z]{2}[0-9\-]{1,24}$</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/2tSYMw/1">regex101.com/r/2tSYMw/1</a></span>
<br>
<span>Description: Must contain reference model (exp. SI00) and reference (exp. 1234-2002). Max length 4+22 numbers and minus symbols. No spaces allowed. Reference model and structure must comply with <a href="https://www.nlb.si/navodila-upn">standard reference model.</a></span>
<br>
<span>Example: SI121234567890120</span>
</div>
<h5 id="api-qrcode-issuer_name"><a href="#api-qrcode-issuer_name">&#128279;</a> <code>issuer_name</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ.'](?:[A-Z0-9 ČŠŽĐ.'\-]{0,31}[A-Z0-9ČŠŽĐ.'])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/ND8T1c/1">regex101.com/r/ubDgZL/1</a></span>
<br>
<span>Description: Name and surname of the issuer or company name. Max length 33 characters including spaces and numbers.</span>
<br>
<span>Example: Kolesarstvo Hrib</span>
</div>
<h5 id="api-qrcode-issuer_address"><a href="#api-qrcode-issuer_address">&#128279;</a> <code>issuer_address</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ](?:[A-Z0-9 ČŠŽĐ\-.]{0,31}[A-Z0-9ČŠŽĐ])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/qgkRMO/1">regex101.com/r/5oKk0T/1</a></span>
<br>
<span>Description: Full issuer address in long form. Max length 33 characters including spaces, numbers, minus and dot.</span>
<br>
<span>Example: Za deveto smreko 15-k p.p. 15</span>
</div>
<h5 id="api-qrcode-issuer_city"><a href="#api-qrcode-issuer_city">&#128279;</a> <code>issuer_city</code></h5>
<div>
<span>Regex: <code>^[a-zA-Z0-9ČŠŽĐ](?:[A-Z0-9 ČŠŽĐ]{0,31}[A-Z0-9ČŠŽĐ])?$/i</code></span>
<br>
<span>Demo: <a href="https://regex101.com/r/JfuNU1/1">regex101.com/r/JfuNU1/1</a></span>
<br>
<span>Description: Issuer city including postal code</span>
<br>
<span>Example: 1000 Ljubljana</span>
</div>
<!-- <h5><code>query</code></h5>
<div>
Regex: <code></code>
<br>
Demo: <a href=""></a>
</div> -->
</div>
<div class="credits">
<span>-- - --</span>
Development and hosting: <a href="https://aljaxus.eu">Aljaž Starc</a>
Initial insight: <a href="http://www.valentincic.eu/">Tjaž Valentinčič</a>
<span>-</span>
<a href="https://gitplac.si/aljaxus/upn-qr">gitplac.si/aljaxus/upn-qr</a>
<span>-</span>
</div>
</section>
</body>
</html>