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


Any suggestion to improve this flow is welcome.


\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 { if (d.type === 'subflow') { tabids.push( tabs.push({ id:, label:, type: 'subslow' }) } else if (d.z) { if (tabids.indexOf(d.z) === -1) { tabids.push(d.z) tabs.push({ id: d.z, label: d.z, type: 'tab', }) } } }) return tabs } function addPanZoom () { var svgs = d3.selectAll('.flowviewer svg'); svgs.each(function() { var svg =; svg.html('' + svg.html() + ''); var inner ='g'); var zoom = d3.zoom() .translateExtent([[0, 0], [3000, 3000]]) .scaleExtent([0.5, 1]) .on("start", function () { svg.classed("dragging", true); }) .on('zoom', function(event) { inner.attr('transform', event.transform); }) .on("end", function () { svg.classed("dragging", false); });; }); } const clearFlow = function () { $('.flowviewer svg .flowGridlines').empty() $('.flowviewer svg .containerGroup').empty() $('.flowviewer svg .flowGroups').empty() $('.flowviewer svg .flowWires').empty() $('.flowviewer svg .flowNodes').empty() } // get the tabs/subflows for our provided flow const tabs = processFlow(flow) // sort them such that tabs render first tabs.sort((a, b) => { return a.type > b.type ? -1 : (a.type < b.type) ? 1 : 0 }) function openTab (id) { // clear any existing flow clearFlow() // draw the new flow - uses function from `public/js/flowviewer.js` renderFlow(id, flow, $('.flowviewer svg')); addPanZoom() } function addTab (tab, index) { const classes = 'flowviewer-tab flowviewer-' + tab.type const name = tab.type === 'tab' ? 'Flow ' + (index + 1) : tab.label const tabDOM = $('.flowviewer-tabs') .append(`
`) .on('click', `#flowviewer-tab-${index}`, function () { // add on click to each new tab $('.flowviewer-tab').removeClass('active') $(this).addClass('active') openTab( }) } tabs.forEach((tab, index) => { addTab(tab, index) }) clearFlow(); renderFlow(tabs[0].id, flow, $('.flowviewer svg')); addPanZoom(); $('#flowviewer-tab-0').addClass('active') $('#copy-flow').on('click', function() { navigator.clipboard.writeText(JSON.stringify(flow)); }) })()
[{"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    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\[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,\n * @version 0.2\n * @author  Daniel Lando,\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}]

Flow Info

Created 7 years, 1 month ago
Updated 3 years, 1 month ago
Rating: 5 3




Node Types

  • debug (x1)
  • function (x3)
  • switch (x1)
  • ui_button (x1)
  • ui_group (x2)
  • ui_tab (x1)
  • ui_template (x1)
  • ui_toast (x1)


  • dashboard
  • uitemplate
  • ui
  • template
  • pin
  • security
  • dialog
  • popup
  • auth
  • ui-template
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option