Based on work from @nygma2004
Initial screen:
![image](https://global.discourse-cdn.com/business6/uploads/nodered/original/3X/1/6/16199ece685831f204ecfacba58da0e1ac9195da.png)
After pressing UploadLogin:
After correct credentials are entered group with file input appears:
Note: I use global npm module "crypto-js" not to store passwords as plain text, so add it in your settings.js or load in function block Setup
functionGlobalContext: {
cryptojs:require('crypto-js')
},
[{"id":"bdfba4adce8879a9","type":"ui-file-input","z":"d3d8a490d0e9960a","group":"04c08c114913c192","name":"","order":1,"width":"4","height":"1","topic":"topic","topicType":"msg","label":"HikvisionConfig.json upload","icon":"paperclip","allowMultiple":false,"accept":".json","className":"","x":220,"y":80,"wires":[["2af42c7221c7d9e7"]]},{"id":"6c4fdd05d4e3c10d","type":"debug","z":"d3d8a490d0e9960a","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":660,"y":80,"wires":[]},{"id":"2af42c7221c7d9e7","type":"json","z":"d3d8a490d0e9960a","name":"","property":"payload","action":"obj","pretty":false,"x":470,"y":80,"wires":[["6c4fdd05d4e3c10d"]]},{"id":"d23f5dfb6ce75c58","type":"ui-button","z":"d3d8a490d0e9960a","group":"ad43ab200ca5494d","name":"","label":"Validate","order":1,"width":"1","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"check","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":160,"y":160,"wires":[[]]},{"id":"9cc172e14e78982e","type":"ui-button","z":"d3d8a490d0e9960a","group":"ad43ab200ca5494d","name":"","label":"Download","order":2,"width":"1","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"download","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":160,"y":200,"wires":[[]]},{"id":"fbd619879a13c7c9","type":"ui-button","z":"d3d8a490d0e9960a","group":"ad43ab200ca5494d","name":"","label":"UploadLogin","order":3,"width":"1","height":"1","emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"mdi-login","iconPosition":"left","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":170,"y":240,"wires":[["ef150be92d36d820"]]},{"id":"e80306b59b972f8f","type":"ui-control","z":"d3d8a490d0e9960a","name":"","ui":"65cf368b48fdb5f6","events":"all","x":760,"y":440,"wires":[[]]},{"id":"0997738cf03d9068","type":"function","z":"d3d8a490d0e9960a","name":"Show","func":"msg.payload = {\n\n groups: {\n show: [\"Hikvision:File Input\"]\n }\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":420,"wires":[["e80306b59b972f8f"]]},{"id":"7b121e4c813a7c93","type":"inject","z":"d3d8a490d0e9960a","name":"Hide","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":390,"y":500,"wires":[["8b8e181044168f49"]]},{"id":"8b8e181044168f49","type":"function","z":"d3d8a490d0e9960a","name":"Hide","func":"msg.payload = {\n\n groups: {\n hide: [\"Hikvision:File Input\"]\n }\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":460,"wires":[["e80306b59b972f8f"]]},{"id":"7b5a6c6c32ce89c7","type":"ui-control","z":"d3d8a490d0e9960a","name":"","ui":"65cf368b48fdb5f6","events":"all","x":240,"y":620,"wires":[["707d17ee380326ab","4a1d39d89b861855"]]},{"id":"707d17ee380326ab","type":"debug","z":"d3d8a490d0e9960a","name":"_clientHikvision","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":420,"y":620,"wires":[]},{"id":"ef150be92d36d820","type":"ui-template","z":"d3d8a490d0e9960a","group":"","page":"7bc2300bd082d6d4","ui":"","name":"Login Dialog","order":0,"width":0,"height":0,"head":"","format":"<template>\n <v-dialog v-model=\"dialog\" min-width = \"20%\" max-width=\"20%\">\n <!-- <v-card color=\"white\"> -->\n\n <v-card class=\"mx-auto pa-12 pb-8\" elevation=\"8\" min-width=\"448\" max-width=\"448\" rounded=\"lg\">\n <div class=\"text-subtitle-1 text-medium-emphasis\">User name</div>\n\n <v-text-field density=\"compact\" placeholder=\"User name\" prepend-inner-icon=\"mdi-account-circle\"\n variant=\"outlined\" v-model=\"username\"></v-text-field>\n\n <div class=\"text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between\">\n Password\n </div>\n\n <!-- <v-text-field \n :append-icon=\"showPsw ? 'mdi-eye' : 'mdi-eye-off'\"\n :type=\"showPsw ? 'text' : 'password'\"\n density=\"compact\" \n placeholder=\"Enter your password\" \n prepend-inner-icon=\"mdi-lock-outline\" \n variant=\"outlined\"\n @click:append-inner=\"showPsw = !showPsw\" \n v-model=\"password\">\n </v-text-field> -->\n\n <v-text-field\n :append-icon=\"showPsw ? 'mdi-eye' : 'mdi-eye-off'\"\n :type=\"showPsw ? 'text' : 'password'\"\n class=\"input-group--focused\"\n name=\"input-10-2\"\n prepend-inner-icon=\"mdi-lock-outline\"\n v-model=\"password\"\n @click:append=\"showPsw = !showPsw\"\n ></v-text-field>\n\n <v-card class=\"mb-14\" :color=\"color\" variant=\"tonal\">\n <v-card-text class=\"font-weight-bold text-caption\">\n {{ message }}\n </v-card-text>\n </v-card>\n\n <v-btn class=\"mb-8\" color=\"blue\" size=\"large\" variant=\"tonal\" block @click=\"LoginButton\">\n Log In\n </v-btn>\n\n <v-card-text class=\"text-center\">\n </v-card-text>\n </v-card>\n <!-- </v-card> -->\n </v-dialog>\n</template>\n\n\n\n\n<script>\nexport default {\n data() {\n return {\n dialog: false,\n showPsw: false,\n color: \"surface-variant\",\n message: \"\",\n username: \"\",\n password: \"\"\n }\n },\n watch: {\n msg: function(){\n // only show the dialog if msg.payload is an object and the socket id in the message\n // matches our socket id (which means the popup was initiated from this session) or\n // there is no _client property present which indicates it should be shown on all sessions.\n if (this.msg.topic===\"close\") {\n this.dialog = false;\n } else {\n if (!this.msg._client || this.msg._client.socketId === this.$socket.id) { \n console.log(\"OK we are in\");\n if (typeof this.msg.payload !== object) msg.payload = {};\n this.message = this.msg.payload.message ?? \"Access to Upload functionality is restricted. Please provide your user name and password in order to upload your files\";\n this.color = this.msg.payload.color ?? \"surface-variant\"; \n this.dialog = true;\n this.username = \"\";\n this.password = \"\";\n }\n }\n // prevent redraw on deploy\n this.msg.payload = null;\n }\n },\n methods:{\n LoginButton:function(){\n // Do not hide the dialog, in case login fails\n //this.dialog = false;\n this.msg.topic = \"authenticate\";\n this.msg.payload = { username: this.username, password: this.password };\n this.send(this.msg);\n }\n },\n computed : {\n },\n unmounted () {\n this.dialog = false;\n }\n}\n</script>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"widget:page","className":"","x":170,"y":300,"wires":[["dbee019f331c0d93"]]},{"id":"d9e665a5de3bc42e","type":"ui-template","z":"d3d8a490d0e9960a","d":true,"group":"","page":"7bc2300bd082d6d4","ui":"","name":"Login Dialog","order":0,"width":0,"height":0,"head":"","format":"<template>\n <div>\n <v-img\n class=\"mx-auto my-6\"\n max-width=\"228\"\n src=\"https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg\"\n ></v-img>\n\n <v-card\n class=\"mx-auto pa-12 pb-8\"\n elevation=\"8\"\n max-width=\"448\"\n rounded=\"lg\"\n >\n <div class=\"text-subtitle-1 text-medium-emphasis\">Account</div>\n\n <v-text-field\n density=\"compact\"\n placeholder=\"Email address\"\n prepend-inner-icon=\"mdi-email-outline\"\n variant=\"outlined\"\n ></v-text-field>\n\n <div\n class=\"text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between\"\n >\n Password\n\n <a\n class=\"text-caption text-decoration-none text-blue\"\n href=\"#\"\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n >\n Forgot login password?</a\n >\n </div>\n\n <v-text-field\n :append-inner-icon=\"visible ? 'mdi-eye-off' : 'mdi-eye'\"\n :type=\"visible ? 'text' : 'password'\"\n density=\"compact\"\n placeholder=\"Enter your password\"\n prepend-inner-icon=\"mdi-lock-outline\"\n variant=\"outlined\"\n @click:append-inner=\"visible = !visible\"\n ></v-text-field>\n\n <v-card class=\"mb-12\" color=\"surface-variant\" variant=\"tonal\">\n <v-card-text class=\"text-medium-emphasis text-caption\">\n Warning: After 3 consecutive failed login attempts, you account will\n be temporarily locked for three hours. If you must login now, you can\n also click \"Forgot login password?\" below to reset the login password.\n </v-card-text>\n </v-card>\n\n <v-btn class=\"mb-8\" color=\"blue\" size=\"large\" variant=\"tonal\" block>\n Log In\n </v-btn>\n\n <v-card-text class=\"text-center\">\n <a\n class=\"text-blue text-decoration-none\"\n href=\"#\"\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n >\n Sign up now <v-icon icon=\"mdi-chevron-right\"></v-icon>\n </a>\n </v-card-text>\n </v-card>\n </div>\n</template>\n\n\n\n\n<script>\nexport default {\n data() {\n return {\n dialog: false,\n color: \"surface-variant\",\n message: \"\",\n username: \"\",\n password: \"\"\n }\n },\n watch: {\n msg: function(){\n // only show the dialog if msg.payload is an object and the socket id in the message\n // matches our socket id (which means the popup was initiated from this session) or\n // there is no _client property present which indicates it should be shown on all sessions.\n if (this.msg.topic===\"close\") {\n this.dialog = false;\n } else {\n if (!this.msg._client || this.msg._client.socketId === this.$socket.id) { \n console.log(\"OK we are in\");\n if (typeof this.msg.payload !== object) msg.payload = {};\n this.message = this.msg.payload.message ?? \"Access to this page is restricted. Please provide your user name and password in order to access this page.\";\n this.color = this.msg.payload.color ?? \"surface-variant\"; \n this.dialog = true;\n }\n }\n // prevent redraw on deploy\n this.msg.payload = null;\n }\n },\n methods:{\n LoginButton:function(){\n // Do not hide the dialog, in case login fails\n //this.dialog = false;\n this.msg.topic = \"authenticate\";\n this.msg.payload = { username: this.username, password: this.password };\n this.send(this.msg);\n }\n },\n computed : {\n },\n unmounted () {\n this.dialog = false;\n }\n}\n</script>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"widget:page","className":"","x":170,"y":340,"wires":[[]]},{"id":"4a1d39d89b861855","type":"function","z":"d3d8a490d0e9960a","name":"connectionHandle","func":"const connectionList = flow.get(\"connectionList\") || []\n\n\nclass ConnectionManager {\n constructor() {\n this.connectionList = this.initConnections();\n }\n\n initConnections() {\n return flow.get(\"connectionList\") || [];\n }\n\n setConnections() {\n return flow.set(\"connectionList\", this.connectionList);\n }\n\n handleIncomingObject(msg) {\n const { payload, name, _client } = msg;\n \n switch (payload) {\n case 'connect':\n this.addConnection(_client, name);\n break;\n case 'change':\n this.changePage(_client, name);\n break;\n case 'lost':\n this.removeConnection(_client);\n break;\n default:\n console.log(`Unknown payload: ${payload}`);\n }\n }\n\n addConnection(client, page) {\n if (!this.connectionList.find(conn => conn.client.socketId === client.socketId)) {\n const newConnection = {\n client,\n page,\n timestamp: this.addTimestamp()\n }\n this.connectionList.push(newConnection);\n //node.warn(`Added connection: ${socketId}`);\n } else {\n node.warn(`Connection ${client.socketId} already exists`);\n }\n this.setConnections();\n }\n\n changePage(client, page) {\n const connection = this.connectionList.find(conn => conn.client.socketId === client.socketId);\n if (connection) {\n connection.page = page;\n connection.timestamp = this.addTimestamp()\n //node.warn(`Changed page for connection ${socketId} to ${name}`);\n } else {\n node.warn(`Connection ${client.socketId} not found`);\n }\n this.setConnections();\n }\n\n addTimestamp(){\n return new Date().toISOString()\n }\n\n removeConnection(client) {\n this.connectionList = this.connectionList.filter(conn => conn.client.socketId !== client.socketId);\n node.warn(`Removed connection: ${client.socketId}`);\n this.setConnections();\n }\n\n\n}\n\n// Usage\nconst connectionManager = new ConnectionManager();\nconnectionManager.handleIncomingObject(msg)\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":580,"wires":[["5252bf4fba7e7f87"]]},{"id":"0222ae18d780749a","type":"inject","z":"d3d8a490d0e9960a","name":"User access defaults","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"users\":[{\"group\":\"mod\",\"username\":\"mod\",\"password\":\"0caaf4bb118927a2dc43115b7123f243443930a4\"},{\"group\":\"mod\",\"username\":\"gos\",\"password\":\"0caaf4bb118927a2dc43115b7123f243443930a4\"},{\"group\":\"mod\",\"username\":\"lika\",\"password\":\"0caaf4bb118927a2dc43115b7123f243443930a4\"}],\"settings\":{\"timeout\":3600}}","payloadType":"json","x":210,"y":660,"wires":[["608da81036e9a561"]]},{"id":"608da81036e9a561","type":"change","z":"d3d8a490d0e9960a","name":"","rules":[{"t":"set","p":"useraccess","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":660,"wires":[[]]},{"id":"3c08efcae56c6470","type":"inject","z":"d3d8a490d0e9960a","name":"Psw","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"tgwconfig","payloadType":"str","x":230,"y":740,"wires":[["b37d5853ada3ee6e"]]},{"id":"b37d5853ada3ee6e","type":"function","z":"d3d8a490d0e9960a","name":"hashPsw","func":"const cryptoJs = global.get(\"cryptojs\");\n\nconst hash = cryptoJs.SHA1(msg.payload).toString()\n\nmsg.payload = hash\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":740,"wires":[["35795fbb104bd3e5"]]},{"id":"35795fbb104bd3e5","type":"debug","z":"d3d8a490d0e9960a","name":"pswHash","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":580,"y":740,"wires":[]},{"id":"dbee019f331c0d93","type":"function","z":"d3d8a490d0e9960a","name":"Validate credentials","func":"const useraccess = flow.get(\"useraccess\");\nconst connectionList = flow.get(\"connectionList\") || []\nconst cryptoJs = global.get(\"cryptojs\");\nconst now = new Date();\n\nconst passHash = cryptoJs.SHA1(msg.payload.password).toString()\n\nif ((Array.isArray(useraccess.users) ? 1 : 0) + (typeof useraccess.settings === \"object\" ? 1 : 0) !== 2) {\n node.status({fill:\"red\",shape:\"ring\",text:\"Invalid useraccess config\"});\n return [null, null];\n}\n\n// Check if an authentication request is received (logon attempt)\nif (msg.topic===\"authenticate\") {\n let authenticated = false;\n //node.warn(\"Check authetication\");\n // Check if the username/password is valid\n for (let i=0; i<useraccess.users.length; i++) {\n //node.warn(\"Useraccess loop: \" + i);\n if ((msg.payload.username === useraccess.users[i].username) && (passHash === useraccess.users[i].password)) {\n msg._client.user = useraccess.users[i];\n authenticated = true;\n\n //Find user client data\n const connUserIndex = connectionList.findIndex(conn => conn.client.socketId === msg._client.socketId)\n if (connUserIndex !== -1) {\n // Connection exists, update user and logonTime\n connectionList[connUserIndex].client.user = msg.payload.username;\n connectionList[connUserIndex].logonTime = now.getTime();\n flow.set(\"connectionList\", connectionList)\n }\n break;\n } \n }\n if (authenticated) {\n msg.topic = \"close\";\n msg.payload = \"\";\n node.status({ fill: \"green\", shape: \"ring\", text: \"User accepted\" });\n return [msg, msg];\n } \n else {\n msg.topic = \"login\";\n msg.payload = { color: \"red\", message: \"Invalid username or password.\" };\n node.status({ fill: \"red\", shape: \"ring\", text: \"Invalid user\" });\n return [msg, null];\n }\n\n}\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":340,"wires":[["ef150be92d36d820"],["0997738cf03d9068"]]},{"id":"003f2613ed2057d1","type":"inject","z":"d3d8a490d0e9960a","name":"Show","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":390,"y":400,"wires":[["0997738cf03d9068"]]},{"id":"5252bf4fba7e7f87","type":"function","z":"d3d8a490d0e9960a","name":"SearchSocketID","func":"const connectionList = flow.get(\"connectionList\") || []\nconst client = msg?._client\n\nif (connectionList.length > 0){\n const connUserIndex = connectionList.findIndex(conn => conn?.client?.socketId === client.socketId)\n //check if user is logged in with this socket ID.\n if (connectionList[connUserIndex].client.user) {\n return [msg, null]\n }\n else return [null, msg]\n}\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":580,"wires":[["0a178d25241b6bf7"],["e9764b1176c38447","0cacd9dba27e365c"]]},{"id":"e9764b1176c38447","type":"function","z":"d3d8a490d0e9960a","name":"PreventPopUp","func":"msg.topic = \"close\"\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":820,"y":580,"wires":[["f49f276f352bfb16"]]},{"id":"f49f276f352bfb16","type":"link out","z":"d3d8a490d0e9960a","name":"link out 10","mode":"link","links":["b77cfda3c7b3a881"],"x":945,"y":580,"wires":[]},{"id":"b77cfda3c7b3a881","type":"link in","z":"d3d8a490d0e9960a","name":"link in 5","links":["f49f276f352bfb16"],"x":35,"y":300,"wires":[["ef150be92d36d820"]]},{"id":"0a178d25241b6bf7","type":"link out","z":"d3d8a490d0e9960a","name":"link out 11","mode":"link","links":["d432840f7e6b5855"],"x":755,"y":540,"wires":[]},{"id":"d432840f7e6b5855","type":"link in","z":"d3d8a490d0e9960a","name":"link in 6","links":["0a178d25241b6bf7"],"x":475,"y":400,"wires":[["0997738cf03d9068"]]},{"id":"0cacd9dba27e365c","type":"link out","z":"d3d8a490d0e9960a","name":"link out 12","mode":"link","links":["f055c2134fd17b54"],"x":755,"y":620,"wires":[]},{"id":"f055c2134fd17b54","type":"link in","z":"d3d8a490d0e9960a","name":"link in 7","links":["0cacd9dba27e365c"],"x":475,"y":500,"wires":[["8b8e181044168f49"]]},{"id":"04c08c114913c192","type":"ui-group","name":"File Input","page":"7bc2300bd082d6d4","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"ad43ab200ca5494d","type":"ui-group","name":"Hikvision file control","page":"7bc2300bd082d6d4","width":"3","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"65cf368b48fdb5f6","type":"ui-base","name":"My Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control","ui-file-input"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"7bc2300bd082d6d4","type":"ui-page","name":"Hikvision","ui":"65cf368b48fdb5f6","path":"/Hikvision","icon":"camera","layout":"grid","theme":"d1f899a55e3aa9ab","order":5,"className":"","visible":"true","disabled":"false"},{"id":"d1f899a55e3aa9ab","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]