Skip to content

Instantly share code, notes, and snippets.

@robertsLando
Last active February 20, 2024 12:11
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save robertsLando/7bcb0b049df4fa3c962294137ebaec19 to your computer and use it in GitHub Desktop.
Save robertsLando/7bcb0b049df4fa3c962294137ebaec19 to your computer and use it in GitHub Desktop.
Security Pin Dialog UI Template for Node-Red-Dashboard

Custom UI-template (with example usage) that shows a Dialog that asks the user to insert a 4 digits PIN.

This flow can be used as middleware to authorize actions.

Allowed PINS are saved in an array of stings in the node "verify_pin", this can be customized by saving authorized pins in a file or others safer ways.

The ui-group that contains the dialog template must have the property "Display group name" unchecked; it will not be visible in dashboard so do not place anything else in this ui-group.

UI Preview

pin-dialog_UI

Any suggestion to improve this flow is welcome.

Enjoy

[{"id":"1936b658.43326a","type":"ui_button","z":"83ed406e.f53e8","name":"","group":"160ddf0a.756f41","order":0,"width":0,"height":0,"passthru":false,"label":"Unlock_door","tooltip":"","color":"","bgcolor":"","icon":"lock","payload":"true","payloadType":"bool","topic":"show","x":190,"y":580,"wires":[["67060ec.9d7f1f"]]},{"id":"67060ec.9d7f1f","type":"ui_template","z":"83ed406e.f53e8","group":"4f3d9279.22b82c","name":"Pin_Unlock","order":0,"width":"0","height":"0","format":"<div ng-init=\"init()\" id=\"pin_insert\" class=\"dialog\">\n \n <div class=\"dialog_content\">\n \n <div class=\"dialog_header\">\n <span ng-click=\"closeDialog()\" class=\"close\">&times;</span>\n <h2 style=\"margin:10px\">Insert PIN</h2>\n </div>\n \n <div class=\"dialog_body\">\n\n <div layout=\"row\" layout-align=\"center\">\n <div class=\"number_placeholder\">\n {{passcode.substring(0, 1)}}\n </div>\n <div class=\"number_placeholder\">\n {{passcode.substring(1, 2)}}\n </div>\n <div class=\"number_placeholder\">\n {{passcode.substring(2, 3)}}\n </div>\n <div class=\"number_placeholder\">\n {{passcode.substring(3, 4)}}\n </div>\n </div>\n \n <div layout=\"column\" layout-align=\"center\" style=\"margin-top: 10px\">\n <div layout=\"row\" layout-align=\"center\">\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(1)\">1</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(2)\">2</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(3)\">3</md-button>\n </div>\n </div>\n <div layout=\"row\" layout-align=\"center\">\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(4)\">4</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(5)\">5</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(6)\">6</md-button>\n </div>\n </div>\n <div layout=\"row\" layout-align=\"center\">\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(7)\">7</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(8)\">8</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(9)\">9</md-button>\n </div>\n </div>\n <div layout=\"row\" layout-align=\"center\">\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"confirm()\">\n <ng-md-icon icon=\"done\" style=\"color:#fff;\"></ng-md-icon>\n </md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"add(0)\">0</md-button>\n </div>\n <div class=\"number_box\">\n <md-button class=\"pin\" ng-click=\"delete()\">\n <ng-md-icon icon=\"arrow_back\" style=\"color:#fff;\"></ng-md-icon>\n </md-button>\n </div>\n </div>\n </div> \n \n </div> <!--dialog_body-->\n </div> <!--dialog_content-->\n</div> <!--dialog-->\n\n\n<style>\n\n/* The Dialog (background) */\n.dialog {\n display: none; /* Hidden by default */\n position: fixed; /* Stay in place */\n z-index: 9999; /* Sit on top */\n left: 0;\n top: 0;\n width: 100%; /* Full width */\n height: 100%; /* Full height */\n overflow: auto; /* Enable scroll if needed */\n background-color: rgb(0,0,0); /* Fallback color */\n background-color: rgba(0,0,0,0.4); /* Black w/ opacity */\n -webkit-transform: translateZ(0px);\n -webkit-transform: translate3d(0,0,0);\n -webkit-perspective: 1000;\n}\n\n.dialog_content {\n position: absolute;\n background-color: #fff;\n left: calc(50% - 170px);\n top: 30px;\n border-radius: 10px;\n padding: 0;\n width: 340px;\n box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);\n -webkit-animation-name: animatetop;\n animation-name: animatetop;\n animation-duration: 0.4s;\n}\n\n/* Media query for smartphones (to Fix?) */\n@media only screen and (min-device-width : 375px) and (max-device-width : 667px) { \n .dialog_content {\n margin-top: 5%;\n margin-left: 5%;\n}\n}\n\n/* Add Animation */\n@-webkit-keyframes animatetop {\n from {top: -300px; opacity: 0} \n to {top: 0; opacity: 1}\n}\n\n@keyframes animatetop {\n from {top: -300px; opacity: 0}\n to {top: 0; opacity: 1}\n}\n\n/* Dialog Header */\n.dialog_header {\n padding: 2px 16px;\n background-color: #03A9F4;\n border-radius: 10px 10px 0 0;\n color: white;\n}\n\n/* Dialog Body */\n.dialog_body {padding: 5px;}\n\n/* The Close Button */\n.close {\n color: #fff;\n float: right;\n font-size: 28px;\n font-weight: bold;\n cursor: pointer;\n}\n\n.close:hover,\n.close:focus {\n color: #1565C0;\n text-decoration: none;\n cursor: pointer;\n}\n\n/* __ */\n.number_placeholder{\n width: 50px;\n height: 34px;\n margin: 10px;\n font-size: 20pt;\n text-align: center;\n border-bottom: 1px solid black;\n}\n\n/* Number container */\n.number_box{\n margin: 5px;\n}\n\n/* Buttons style */\n.pin {\n min-height: 50px;\n min-width: 50px;\n font-weight: bold;\n margin: 0px 10px 10px 0px;\n box-shadow: 4px 4px 6px 0 #dadada;\n background-color: #29B6F6;\n color: #fff;\n}\n\n.pin:not([disabled]):hover {\n background-color: #03A9F4;\n}\n\n.btn1 {\n color : rgb(49, 46, 46);\n background-color: rgba(255, 222, 121, 0.96);\n border-radius: 10px 0 0 10px;\n font-size: 16px;\n}\n\n.btn1:not([disabled]):hover {\n background-color: rgba(107, 103, 91, 0.96);\n color: white;\n}\n\n.btn1[disabled] {\n color : rgb(187, 187, 187);\n background-color: rgba(230, 230, 229, 0.96);\n}\n\n</style>\n\n<script>\n\n/**\n * pin_dialog.js\n * Node-Red UI template for Node-Red Dashboard. \n * Custom dialog that asks for a PIN to allow actions\n * Enjoy it :). \n * -- Daniel\n *\n *\n * @license The Unlicense, http://unlicense.org/\n * @version 0.2\n * @author Daniel Lando, https://github.com/robertsLando\n * @updated 2019-03-18\n * @link ----\n *\n *\n */\n\nvar dialog;\n\n/* ==== */\n(function(scope) {\n \n scope.passcode = \"\";\n scope.payload = \"\";\n scope.inited = false;\n \n scope.init = function() {\n scope.passcode = \"\";\n //Hide the md-panel\n $('#pin_insert').parent().parent().css(\"display\", \"none\");\n //This trick make it works on smartphones too :)\n dialog = $('#pin_insert').detach();\n //remove any previously added pin dialog\n $('.dialog').remove();\n }\n \n scope.showDialog = function() {\n dialog.appendTo(document.body);\n dialog.css(\"display\", \"block\");\n }\n \n scope.closeDialog = function(){\n dialog.css(\"display\", \"none\");\n }\n \n scope.add = function(value) {\n if(scope.passcode.length < 4) {\n scope.passcode = scope.passcode + value;\n if(scope.passcode.length == 4) {\n console.log(\"The four digit code was entered\");\n \n }\n }\n }\n \n scope.delete = function() {\n if(scope.passcode.length > 0) {\n scope.passcode = scope.passcode.substring(0, scope.passcode.length - 1);\n }\n }\n \n scope.confirm = function() {\n if(scope.passcode.length == 4) {\n scope.send({passcode: scope.passcode, payload : scope.payload});\n scope.closeDialog();\n scope.passcode = \"\";\n scope.payload = \"\";\n }\n }\n\n scope.$watch('msg', function(data) {\n if(data && data.topic){\n switch(data.topic){\n case \"show\":\n if(scope.inited){\n scope.payload = data.payload;\n scope.showDialog();\n }\n else\n scope.inited = true;\n break;\n case \"close\": \n scope.closeDialog(); \n break;\n }\n }\n });\n})(scope);\n\n</script>\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":375.3750343322754,"y":579.9642696380615,"wires":[["797ddf5e.2b5ab"]]},{"id":"797ddf5e.2b5ab","type":"function","z":"83ed406e.f53e8","name":"verify_pin","func":"var pins = [\"2136\"];\nvar verified = false;\n\nfor(var i=0;i<pins.length;i++){\n if(msg.passcode == pins[i]){\n verified = true;\n break;\n }\n}\n\nmsg.verified = verified;\n\nreturn msg;","outputs":1,"noerr":0,"x":540,"y":580,"wires":[["41443642.dd9aa8"]]},{"id":"41443642.dd9aa8","type":"switch","z":"83ed406e.f53e8","name":"check","property":"verified","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"false","outputs":2,"x":690,"y":580,"wires":[["4a288444.8cbf0c"],["39892ab3.134bf6"]]},{"id":"4a288444.8cbf0c","type":"function","z":"83ed406e.f53e8","name":"pin_ok","func":"var msg2 = {};\nmsg2.topic = \"Pin successfully verified!\";\nmsg2.payload = \"\";\n \nreturn [msg, msg2];","outputs":"2","noerr":0,"x":855.8749961853027,"y":559.9999942779541,"wires":[["d2bfadda.bb314"],["2a87f426.517a6c"]]},{"id":"39892ab3.134bf6","type":"function","z":"83ed406e.f53e8","name":"pin_error","func":" msg.topic = \"Wrong PIN\";\n msg.payload = \"\";\n \nreturn msg;","outputs":1,"noerr":0,"x":865.8749961853027,"y":599.9999942779541,"wires":[["2a87f426.517a6c"]]},{"id":"d2bfadda.bb314","type":"debug","z":"83ed406e.f53e8","name":"do_Something","active":true,"console":"false","complete":"payload","x":1065.8749961853027,"y":519.9999942779541,"wires":[]},{"id":"2a87f426.517a6c","type":"ui_toast","z":"83ed406e.f53e8","position":"top right","displayTime":"3","outputs":0,"ok":"OK","cancel":"","topic":"","name":"","x":1074,"y":573,"wires":[]},{"id":"160ddf0a.756f41","type":"ui_group","z":"","name":"Secure","tab":"f83ce942.20d488","disp":true,"width":"6"},{"id":"4f3d9279.22b82c","type":"ui_group","z":"","name":"pin","tab":"f83ce942.20d488","disp":false,"width":"1"},{"id":"f83ce942.20d488","type":"ui_tab","z":"","name":"Home","icon":"home","order":1}]
@paulsalibe
Copy link

Is the only html id in this the pin_insert? Or does this also include all the "column" or "add(1)" etc.

@robertsLando
Copy link
Author

Everything that has id="***" in html eleents now I don't remember how many there are pin_insert is the main one to change, also you need to change any reference in the code of #pin_insert selectors.

BTW like I said I see no reason to use multiple pin dialogs in a single page, you can use the same for multiple validations

@christophebelmont
Copy link

Thank you so much for this pin lock screen that I've been using as a copy/paste user. I use it as "bring your own pin lock screen". Basically a QR code stuck on a door. The person scans the QR code, type the PIN and the door opens. This avoids having an actual QR Code screen on the door.

I had a look at this template (https://codepen.io/carty/pen/NWPrvyE) liked it for a few reasons:

  • nice look and feel
  • obfuscation of pin code in the code
  • numbers do not show on the web page
  • no need to validate when the correct number of digits is entered.

But after a few days of twicking, I have to admit that I am not able to adapt the template to nodered. Here are the problems:

  • the template shows up but I can't find how to make the flow move to the next box in node red. Maybe do I need to close the dialog when code is validated?

Also, for my usage, this would be interesting to close the browser tab after a few seconds when correct code is entered? I am not sure browser tabs can be closed at all.

I know that this request if somehow wide but I wanted also to share usage and possible evolution. Thanks again for the work.

@robertsLando
Copy link
Author

@Christian-Me that templete looks very nice! Unfortunately I have no time to invest in this right now, let me know If you end up with somethig

@FrantaD
Copy link

FrantaD commented Feb 20, 2024

@robertsLando Hi,
Trying to use this feature 2x in one project, but each time the second one doesn't work. Is there a procedure I need to follow?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment