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}]
@robertsLando
Copy link
Author

 scope.confirm = function() {
        if(scope.passcode.length == 4) {
            scope.send({passcode: scope.passcode, payload : scope.payload});
            scope.closeDialog();
            scope.passcode = "";
            scope.payload = "";
        }
    }

This is the function triggered on confirm, what you could do is to make the validation here and show an error in case the pin is not correct

@tjareson
Copy link

tjareson commented Nov 9, 2020

Hi there,
I understand that I can use this pin code function as kind of a "checking entitlement step" within the flows triggered e.g. by a dashboard.
Is there also an implementation approach possible to just hide the complete dashboard and only show it after the pin was entered?
Use case is a home automation controller based on an old tablet where one can also adjust parameters of the heating, starting garden irrigation etc. So it would be good, if it is locked by a pin code. The regular username/password as part of node red is not really suitable, as it takes too long to open.
Best
Tjareson

@robertsLando
Copy link
Author

@tjareson you can but it needs some edits

@RealKanashii
Copy link

RealKanashii commented Nov 15, 2020

Just a note. I used the digest node from node-red-contrib-crypto-js to ofuscate the PIN number.

  • Use the digest node with a inject node of your pin string to get the final digest.
  • Put this 3 nodes between Pin_Unlock and the verify_pin node.
[{"id":"724ee236.8c6c9c","type":"function","z":"bd2439c9.52c888","name":"passcode to payload","func":"msg.payload = msg.passcode;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":440,"y":360,"wires":[["8e7ed336.f2c67"]]},{"id":"8e7ed336.f2c67","type":"digest","z":"bd2439c9.52c888","name":"","algorithm":"SHA512","x":630,"y":360,"wires":[["b7d8a57c.199a38"]]},{"id":"b7d8a57c.199a38","type":"function","z":"bd2439c9.52c888","name":"payload to passcode","func":"msg.passcode = msg.payload;\nmsg.payload = true;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":820,"y":360,"wires":[["797ddf5e.2b5ab"]]}]
  • Change "verify pin" from:
for(var i=0;i<pins.length;i++){
    if(msg.passcode == pins[i]){
        verified = true;
        break;
    }
}

for:

    if(msg.passcode === "YOUR PREVIOUS CALCULATED DIGEST" ){
        verified = true;
    }

Basically I precalculated the digest (selecting your digest type) . Then converting the form msg from passcode to payload, pass to the digest and return the payload to passcode. Finally change the code to compare the digested strings.
image

It works fine and you don't expose your pin, not even on messages.

@RealKanashii
Copy link

RealKanashii commented Nov 19, 2020

The first time I load the page I have to click twice the login button to show the pin panel. Anyone?

I see two errors when loading:

app.min.js:148 You are using the ngTouch module. 
AngularJS Material already has mobile click, tap, and swipe support... 
ngTouch is not supported with AngularJS Material!

app.min.js:134 GET http://192.168.2.168:1880/ui/loading.html 404 (Not Found)

@robertsLando
Copy link
Author

The first time I load the page I have to click twice the login button to show the pin panel. Anyone?

It's on purpose, without that check it opens on every deploy

@RealKanashii
Copy link

Thanks for your answer. I'm using the pin pad as security for login. Then I unblock all tab/groups. But I want to activate on every deploy. How can I change it?

@RealKanashii
Copy link

RealKanashii commented Nov 22, 2020

Hi there,
I understand that I can use this pin code function as kind of a "checking entitlement step" within the flows triggered e.g. by a dashboard.
Is there also an implementation approach possible to just hide the complete dashboard and only show it after the pin was entered?
Use case is a home automation controller based on an old tablet where one can also adjust parameters of the heating, starting garden irrigation etc. So it would be good, if it is locked by a pin code. The regular username/password as part of node red is not really suitable, as it takes too long to open.
Best
Tjareson

I'm using this as you said. I made the final action to show/activate all tabs and groups. Then I put a logoff button that hiddens all tabs/groups but Security tab and pass the focus to it. If you need help I can give you some code.

@robertsLando
Copy link
Author

I'm using the pin pad as security for login. Then I unblock all tab/groups. But I want to activate on every deploy. How can I change it?

Check the surce code, there is an init var that is set to true on first message received, just remove it.

I made the final action to show/activate all tabs and groups. Then I put a logoff button that hiddens all tabs/groups but Security tab and pass the focus to it. If you need help I can give you some code.

It may be useful for others so could you paste the code here? I could add it to the gist

@RealKanashii
Copy link

RealKanashii commented Nov 23, 2020

When "pin_ok" ( first output ), using "ui_ui_control" node, I make this:

  • Hide Login Tab ( "Accés" ).
  • Show Main Tab ("Casa").
  • Show UI Groups ( "Casa_seguretat" ). If you have more than one make a show for everyone.
  • Focus Main Tab ( "Casa" ).

Then inside the main group (or any group you want) put a button to block the accés doing the reverse process.

  • Hide Main Tab ( "Casa" )
  • Hide UI Groups in the main Tab ( "Casa_seguretat" )
  • Show Login Tab ( "Accés" )
  • Focus Login Tab ( "Accés" ).

I will paste the code later with an edit.

[{"id":"1936b658.43326a","type":"ui_button","z":"bd2439c9.52c888","name":"","group":"160ddf0a.756f41","order":0,"width":0,"height":0,"passthru":false,"label":"Accedir","tooltip":"","color":"","bgcolor":"","icon":"lock","payload":"true","payloadType":"bool","topic":"show","x":80,"y":360,"wires":[["67060ec.9d7f1f"]]},{"id":"67060ec.9d7f1f","type":"ui_template","z":"bd2439c9.52c888","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,"resendOnRefresh":false,"templateScope":"local","x":230,"y":360,"wires":[["724ee236.8c6c9c"]]},{"id":"797ddf5e.2b5ab","type":"function","z":"bd2439c9.52c888","name":"verify_pin","func":"var pins = [\"XXXX\"];\nvar verified = false;\n\n//for(var i=0;i<pins.length;i++){\n// if(msg.passcode == pins[i]){\n// verified = true;\n// break;\n// }\n//}\n\n if(msg.passcode === \"942639f810131221f32dea771bfc53c42cdaff6db625f060e445d21205bfda64602dda7859a2265d8dcc1a60641436a51b8f501296333063d72ef24f098deffe\" ){\n verified = true;\n }\n\n\nmsg.verified = verified;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1000,"y":360,"wires":[["41443642.dd9aa8"]]},{"id":"41443642.dd9aa8","type":"switch","z":"bd2439c9.52c888","name":"check","property":"verified","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"false","outputs":2,"x":1150,"y":360,"wires":[["4a288444.8cbf0c"],["39892ab3.134bf6"]]},{"id":"4a288444.8cbf0c","type":"function","z":"bd2439c9.52c888","name":"pin_ok","func":"var msg2 = {};\nmsg2.topic = \"Pin successfully verified!\";\nmsg2.payload = \"\";\n \nreturn [msg, msg2];","outputs":"2","noerr":0,"x":1350,"y":360,"wires":[["37801ba9.3deff4","11fa280.a7a03d8","1e21f764.12f569","b7988ad5.bd66c8"],["2a87f426.517a6c"]]},{"id":"39892ab3.134bf6","type":"function","z":"bd2439c9.52c888","name":"pin_error","func":" msg.topic = \"PIN Erroni\";\n msg.payload = \"\";\n \nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1360,"y":420,"wires":[["2a87f426.517a6c"]]},{"id":"2a87f426.517a6c","type":"ui_toast","z":"bd2439c9.52c888","position":"top right","displayTime":"5","highlight":"","sendall":true,"outputs":0,"ok":"OK","cancel":"","raw":false,"topic":"","name":"","x":1580,"y":420,"wires":[]},{"id":"e86a768e.aa3278","type":"ui_ui_control","z":"bd2439c9.52c888","name":"","events":"change","x":1980,"y":260,"wires":[[]]},{"id":"37801ba9.3deff4","type":"function","z":"bd2439c9.52c888","name":"Mostra Seguretat","func":"msg.payload = {\"group\":{\"show\":[\"Casa_Seguretat\"]}}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1790,"y":260,"wires":[["e86a768e.aa3278"]]},{"id":"11fa280.a7a03d8","type":"function","z":"bd2439c9.52c888","name":"Focus Casa","func":"msg.payload = \"Casa\"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1770,"y":200,"wires":[["41a61f35.b92af"]]},{"id":"41a61f35.b92af","type":"ui_ui_control","z":"bd2439c9.52c888","name":"","events":"change","x":1980,"y":200,"wires":[[]]},{"id":"1e21f764.12f569","type":"function","z":"bd2439c9.52c888","name":"Amaga Accés","func":"msg.payload = {\"tabs\":{\"hide\":[\"Accés\"]}}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1780,"y":320,"wires":[["730407c7.d26298"]]},{"id":"730407c7.d26298","type":"ui_ui_control","z":"bd2439c9.52c888","name":"","events":"all","x":2000,"y":320,"wires":[[]]},{"id":"724ee236.8c6c9c","type":"function","z":"bd2439c9.52c888","name":"passcode to payload","func":"msg.payload = msg.passcode;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":440,"y":360,"wires":[["8e7ed336.f2c67"]]},{"id":"b7d8a57c.199a38","type":"function","z":"bd2439c9.52c888","name":"payload to passcode","func":"msg.passcode = msg.payload;\nmsg.payload = true;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":820,"y":360,"wires":[["797ddf5e.2b5ab"]]},{"id":"8e7ed336.f2c67","type":"digest","z":"bd2439c9.52c888","name":"","algorithm":"SHA512","x":630,"y":360,"wires":[["b7d8a57c.199a38"]]},{"id":"b7988ad5.bd66c8","type":"function","z":"bd2439c9.52c888","name":"Show Casa","func":"msg.payload = {\"tabs\":{\"show\":[\"Casa\"]}}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1770,"y":140,"wires":[["1ac99723.808d99"]]},{"id":"1ac99723.808d99","type":"ui_ui_control","z":"bd2439c9.52c888","name":"","events":"change","x":1980,"y":140,"wires":[[]]},{"id":"160ddf0a.756f41","type":"ui_group","name":"Secure","tab":"f83ce942.20d488","order":1,"disp":false,"width":"6","collapse":false},{"id":"4f3d9279.22b82c","type":"ui_group","name":"pin","tab":"f83ce942.20d488","order":2,"disp":false,"width":"5","collapse":false},{"id":"f83ce942.20d488","type":"ui_tab","name":"Accés","icon":"home","order":1,"disabled":false,"hidden":false}]

@AlgemeneBuurman
Copy link

AlgemeneBuurman commented Dec 27, 2020

Hi guys, i'm building a nodered interface to control a alarmsystem from a tablet. I really like this ui template and altered it somewhat to my needs. First thing i changed was the need to press twice the first time after deploying. I just put the initvar on true, like explained above.

After that is wanted to see astrixes instead of the code that was entered. So i added a var called 'scope.passx' and altered every function to add and delete a astrix to the var when done with the real passcode var. An then chnged the input to the boxes. I'm just a copy/paste programmer in javascript, so maybe this is not the nicest way to do it, but it works.

See code below.

<div class="dialog_content">
    
    <div class="dialog_header">
        <span ng-click="closeDialog()" class="close">&times;</span>
        <h2 style="margin:10px">Toets pin</h2>
    </div>
    
    <div class="dialog_body">

       <div layout="row" layout-align="center">
            <div class="number_placeholder">
                {{passx.substring(0, 1)}}
            </div>
            <div class="number_placeholder">
                {{passx.substring(1, 2)}}
            </div>
            <div class="number_placeholder">
                {{passx.substring(2, 3)}}
            </div>
            <div class="number_placeholder">
                {{passx.substring(3, 4)}}
            </div>
        </div>
        
        <div layout="column" layout-align="center" style="margin-top: 10px">
            <div layout="row" layout-align="center">
                <div class="number_box">
                    <md-button class="pin" ng-click="add(1)">1</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(2)">2</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(3)">3</md-button>
                </div>
            </div>
             <div layout="row" layout-align="center">
                <div class="number_box">
                    <md-button class="pin" ng-click="add(4)">4</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(5)">5</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(6)">6</md-button>
                </div>
            </div>
             <div layout="row" layout-align="center">
                <div class="number_box">
                    <md-button class="pin" ng-click="add(7)">7</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(8)">8</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(9)">9</md-button>
                </div>
            </div>
             <div layout="row" layout-align="center">
                <div class="number_box">
                    <md-button class="pin" ng-click="confirm()">
                        <ng-md-icon icon="done" style="color:#fff;"></ng-md-icon>
                    </md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="add(0)">0</md-button>
                </div>
                <div class="number_box">
                    <md-button class="pin" ng-click="delete()">
                        <ng-md-icon icon="arrow_back" style="color:#fff;"></ng-md-icon>
                    </md-button>
                </div>
            </div>
        </div> 
      
    </div> <!--dialog_body-->
</div> <!--dialog_content-->
<style> /* The Dialog (background) */ .dialog { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 9999; /* Sit on top */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ -webkit-transform: translateZ(0px); -webkit-transform: translate3d(0,0,0); -webkit-perspective: 1000; } .dialog_content { position: absolute; background-color: #fff; left: calc(50% - 170px); top: 30px; border-radius: 10px; padding: 0; width: 340px; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); -webkit-animation-name: animatetop; animation-name: animatetop; animation-duration: 0.4s; } /* Media query for smartphones (to Fix?) */ @media only screen and (min-device-width : 375px) and (max-device-width : 667px) { .dialog_content { margin-top: 5%; margin-left: 5%; } } /* Add Animation */ @-webkit-keyframes animatetop { from {top: -300px; opacity: 0} to {top: 0; opacity: 1} } @Keyframes animatetop { from {top: -300px; opacity: 0} to {top: 0; opacity: 1} } /* Dialog Header */ .dialog_header { padding: 2px 16px; background-color: #03A9F4; border-radius: 10px 10px 0 0; color: white; } /* Dialog Body */ .dialog_body {padding: 5px;} /* The Close Button */ .close { color: #fff; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .close:hover, .close:focus { color: #1565C0; text-decoration: none; cursor: pointer; } /* __ */ .number_placeholder{ width: 50px; height: 34px; margin: 10px; font-size: 20pt; text-align: center; border-bottom: 1px solid black; } /* Number container */ .number_box{ margin: 5px; } /* Buttons style */ .pin { min-height: 50px; min-width: 50px; font-weight: bold; margin: 0px 10px 10px 0px; box-shadow: 4px 4px 6px 0 #dadada; background-color: #29B6F6; color: #fff; } .pin:not([disabled]):hover { background-color: #03A9F4; } .btn1 { color : rgb(49, 46, 46); background-color: rgba(255, 222, 121, 0.96); border-radius: 10px 0 0 10px; font-size: 16px; } .btn1:not([disabled]):hover { background-color: rgba(107, 103, 91, 0.96); color: white; } .btn1[disabled] { color : rgb(187, 187, 187); background-color: rgba(230, 230, 229, 0.96); } </style> <script> /** * pin_dialog.js * Node-Red UI template for Node-Red Dashboard. * Custom dialog that asks for a PIN to allow actions * Enjoy it :). * -- Daniel * * * @license The Unlicense, http://unlicense.org/ * @Version 0.2 * @author Daniel Lando, https://github.com/robertsLando * @Updated 2019-03-18 * @link ---- * * */ var dialog; /* ==== */ (function(scope) { scope.passcode = ""; scope.passx = ""; scope.payload = ""; scope.inited = true; scope.init = function() { scope.passcode = ""; //Hide the md-panel $('#pin_insert').parent().parent().css("display", "none"); //This trick make it works on smartphones too :) dialog = $('#pin_insert').detach(); //remove any previously added pin dialog $('.dialog').remove(); } scope.showDialog = function() { dialog.appendTo(document.body); dialog.css("display", "block"); } scope.closeDialog = function(){ dialog.css("display", "none"); } scope.add = function(value) { if(scope.passcode.length < 4) { scope.passcode = scope.passcode + value; scope.passx = scope.passx + "*"; if(scope.passcode.length == 4) { console.log("The four digit code was entered"); } } } scope.delete = function() { if(scope.passcode.length > 0) { scope.passcode = scope.passcode.substring(0, scope.passcode.length - 1); scope.passx = scope.passx.substring(0, scope.passx.length - 1); } } scope.confirm = function() { if(scope.passcode.length == 4) { scope.send({passcode: scope.passcode, payload : scope.payload}); scope.closeDialog(); scope.passcode = ""; scope.passx = ""; scope.payload = ""; } } scope.$watch('msg', function(data) { if(data && data.topic){ switch(data.topic){ case "show": if(scope.inited){ scope.payload = data.payload; scope.showDialog(); } else scope.inited = true; break; case "close": scope.closeDialog(); break; } } }); })(scope); </script>

@paulsalibe
Copy link

Is there a way to use this as a pinpad that is always showing in the dashboard? Like instead of having to click a button for it to pop up and things occur, it is always visible and when the correct pin is entered then things happen?

@robertsLando
Copy link
Author

@paulsalibe yes, just check out the code and edit it

@paulsalibe
Copy link

I'm only 4-5 months or so into Node-Red and have yet to dive too deep into JavaScript. If its easy enough through here, could you just point me in the right direction of that code and how I'd edit that to be always showing?

@robertsLando
Copy link
Author

ATM the pin is shown when pressing a button, the buttons sends a command to open the pin dialog, this happens in the scope msg handler inside the pin template and when it receives show as topic calls scope.showDialog(), you should just add that scope.showDialog() somewhere outside so it is shown when the template is created

@paulsalibe
Copy link

Awesome thank you! I appreciate your help. One other thing I have been battling with the last few days is having multiple of them. Forgive me as this is probably an easy fix, but what do I have to alter to have multiple of them operating with their own passcodes? They seem to be interacting with each other the way I have them.

@robertsLando
Copy link
Author

Unfortunaly having multiple of them is not easy, you should change the unique ids of the elements created on each template if there are more then one in a single page

@paulsalibe
Copy link

Gotcha. Also, I worded my first question wrong. As opposed to having it superimposed over the rest of the dashboard, can I have it on the dashboard just like any other group gauge/widget would be? Sorry for the confusion

@robertsLando
Copy link
Author

can I have it on the dashboard just like any other group gauge/widget would be? Sorry for the confusion

Everything is possible but I have no time to show you how. ATM the tricky part of the template is that it detouch the html element of the pin dialog from the panel to show it full screen, you should remove all the code that hides the group element and detouches the dialog

@Blind228
Copy link

The first time I load the page I have to click twice the login button to show the pin panel. Anyone?

It's on purpose, without that check it opens on every deploy

Is there a way to deactivate that check? I dont deploy that often and would like to have the keypad open on the first click.
Thank you for this code btw!

@robertsLando
Copy link
Author

@Blind228 Just remove the init check

@paulsalibe
Copy link

I assume this would be very complicated, but for our application we would ideally like the end user to be able to manually enter new PINs that would be acceptable. For example, is it possible through the dashboard to enter a new 4 digit code that can be somehow put into the function node behind the scenes therefor making that entered code a new acceptable passcode for the pinpad.

@robertsLando
Copy link
Author

@paulsalibe sure, you could set that pin into flow context var using flow.set and then in the pin check use flow.get to retrive the pin

@paulsalibe
Copy link

Unfortunaly having multiple of them is not easy, you should change the unique ids of the elements created on each template if there are more then one in a single page

I am currently revisiting this issue so sorry for asking again...but what would be an example of some of the unique ids of the elements created? Does this mean each button would have to be renamed, or just things like scope.payload, scope.passcode, etc.

@robertsLando
Copy link
Author

I'm speaking about html ids

@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