diff --git a/css/ln.vue.css b/css/ln.vue.css index 94e50b7..ef00b44 100644 --- a/css/ln.vue.css +++ b/css/ln.vue.css @@ -73,6 +73,33 @@ body { font-size: 12px; } +.ln-view { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + + display: flex; + flex-direction: column; +} + +section#header { + flex-grow: 0; + margin-bottom: 1em; + background-color: whitesmoke; +} + +section#body { + flex-grow: 1; + overflow: auto; +} + +section#footer { + flex-grow: 0; + background-color: whitesmoke; +} + .json { white-space: pre; font-family: 'Courier New', Courier, monospace; @@ -153,7 +180,6 @@ div.ln-select::after { padding-top: 4px; padding-bottom: 4px; - margin-bottom: 2em; } .ln-navitem { @@ -211,6 +237,83 @@ div.ln-nav-children > .ln-navitem:first-of-type { border-top: none; } +div.ln-statusbar { + position: relative; + bottom: 0px; + left: 0px; + right: 0px; + + border-top: 1px solid black; + + padding: 8px; + + display: flex; + flex-direction: row; +} +div.ln-statusbar > div { + flex-grow: 1; + text-align: center; + + padding: 4px; + padding-left: 8px; + padding-right: 8px; + + border-left: 1px solid #D0D0D0; +} +div.ln-statusbar > div:first-of-type { + text-align: left; + border-left: none; +} + +div.ln-statusbar > div.ln-background-tasks { + flex-grow: 0; +} + +.ln-background-tasks { + position: relative; +} +div.ln-background-tasks > div { + display: block; + position: absolute; + + white-space: pre; + + left: 4px; + bottom: 80%; + + overflow-y: visible; + overflow-x: hidden; + + background-color: blanchedalmond; + border: 1px solid black; + + padding: 4px; + padding-right: 32px; + padding-left: 16px; + + margin-right: 24px; + + opacity: 0; + height: 10px; + + transition: opacity 500ms 50ms, height 0ms 550ms; +} +div.ln-background-tasks:hover > div { + opacity: 1.0; + height: 8em; + + transition: opacity 500ms 50ms, height 500ms 50ms; +} +div.ln-background-tasks > div > div[state="waiting"] { + color: silver; +} +div.ln-background-tasks > div > div[state="failed"] { + color: red; +} +div.ln-background-tasks > div > div[state="ready"] { + color: green; +} + div.ln-upload { display: inline-block; position: relative; diff --git a/demo.html b/demo.html index ae64605..0372b0e 100644 --- a/demo.html +++ b/demo.html @@ -17,19 +17,32 @@ table#controls > tbody > tr > td:nth-child(3) { padding-left: 24px; } + + div#frame { + display: flex; + flex-direction: column; + } -
-

ln.vue Demo Application

- -
-

Please wait for application to be loaded...

-
- - - +
+ +
+
+

Please wait for application to be loaded...

+
+ +
+ +
+
+
diff --git a/js/ln.vue.components.js b/js/ln.vue.components.js index 7e966da..f2a2b8a 100644 --- a/js/ln.vue.components.js +++ b/js/ln.vue.components.js @@ -90,6 +90,36 @@ Vue.directive('tooltip',{ }, }); + +Vue.component('ln-statusbar',{ + computed: { + CurrentPromises: function(){ + return LNVue.$_.getCurrentPromises(); + } + }, + template: ` +
+
{{ LNVue.$_.statusText }}
+
{{ LNVue.$_.socket && LNVue.$_.socket._state }}
+
{{ Object.keys(CurrentPromises).length || "No" }} Background Tasks +
+
{{ promise.label }}
+
+
+
{{ LNVue.$_.Version() }}
+
+ `, +}); + Vue.component('ln-navitem',{ props: { value: { @@ -167,6 +197,18 @@ Vue.component('ln-textfield',{ v-bind:value="value" v-on:input="$emit('input', $event.target.value)">`, }); +Vue.component('ln-password',{ + props: { + value: { + type: String, + required: true, + }, + }, + template: ``, +}); Vue.component('ln-textarea',{ props: { @@ -466,4 +508,97 @@ Vue.component('ln-select',{ }); +Vue.component('ln-login-pane',{ + props: { + }, + methods: { + stage1: function(){ + console.log("login stage1", this.identityName); + + LNVue.$_ + .requestChallenges(this.identityName) + .then((challenges)=>{ + challenges = challenges.message.Challenges; + if (challenges.length > 0){ + LNVue.$_.identity.identityName = this.identityName; + LNVue.$_.identity.challenges = challenges; + } + }); + }, + authenticate(challenge){ + console.log("authenticate()",challenge); + }, + authenticateSeededPassword(challenge){ + let encoder = new TextEncoder(); + let seed = LNVue.decodeHex(challenge.AuthenticationParameters); + let password = encoder.encode(challenge.prove); + + console.log("password", LNVue.encodeHex(password),password); + console.log("seed", LNVue.encodeHex(seed),seed); + + let secretSource = ArrayBuffer.combine(seed, password, seed); + + crypto.subtle.digest("SHA-256",secretSource) + .then((secret)=>{ + let challengebytes = LNVue.decodeB64(challenge.Challenge); + secret = new Uint8Array(secret); + console.log("secret", LNVue.encodeHex(secret),secret); + + let proveSource = ArrayBuffer.combine(challengebytes, secret, challengebytes); + + crypto.subtle.digest("SHA-256",proveSource) + .then((prove)=>{ + prove = LNVue.encodeB64(new Uint8Array(prove)); + LNVue.$_.authenticate(this.identityName,challenge.SecureAttributeID,challenge.Challenge,prove); + }); + }); + + } + + }, + data: ()=>{ + return { + identityName: "", + }; + }, + template: `
+
+ IdentityName: {{ LNVue.$_.identity.uniqueID }} / {{ LNVue.$_.identity.identityName }} +
+
+
+ + +
+
+
+
{{ challenge.SecureAttributeLabel }} + + +
+
+
+
`, +}); + + })(); \ No newline at end of file diff --git a/js/ln.vue.js b/js/ln.vue.js index 3070552..fffe4a2 100644 --- a/js/ln.vue.js +++ b/js/ln.vue.js @@ -1,5 +1,280 @@ var LNVue = (function (){ + let _unique = new Date().getTime(); + function uniqueID(){ + return _unique++; + } + let currentPromises = {}; + + /*\ + |*| + |*| Base64 / binary data / UTF-8 strings utilities (#1) + |*| + |*| https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + |*| + |*| Author: madmurphy + |*| + \*/ + + /* Array of bytes to base64 string decoding */ + + function b64ToUint6 (nChr) { + + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + + } + + function base64DecToArr (sBase64, nBlockSize) { + + var + sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + + return aBytes; + } + + /* Base64 string to array encoding */ + + function uint6ToB64 (nUint6) { + + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; + + } + + function base64EncArr (aBytes) { + + var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; + + for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { + nMod3 = nIdx % 3; + /* Uncomment the following line in order to split the output in lines 76-character long: */ + /* + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } + */ + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + + return eqLen === 0 ? + sB64Enc + : + sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); + + } + + + + + + + + class LNVuePromise + { + constructor(label,executor){ + this.promise = new Promise( + (resolve,reject) => { + executor( + (v) => { + this._s.state = "ready"; + resolve(v); + this.release(); + }, + (e) => { + this._s.state = "failed"; + reject(e); + this.release(); + } + ); + } + ); + + if (!label) + label = "N.D."; + + this._s = { + label, + state: "waiting" + }; + + this.idx = uniqueID(); + + currentPromises[this.idx] = this._s; + } + + label(){ + return this._s.label; + } + + state(){ + return this._s.state; + } + + release(){ + setTimeout(()=>{ + Vue.delete(currentPromises, this.idx); + },1000); + } + + then(){ + return this.promise.then.apply(this.promise, arguments); + } + } + + class LNVueWebSocket + { + constructor(lnvue,o){ + this.LNVue = lnvue; + this.options = Object.assign({},o); + + if (!this.options.url) + this.options.url = this.constructURL(); + + this._id = 1; + this.defaultTimeout = 30000; + this.websocket = null; + this.callbacks = {}; + + this._state = "initialized"; + this._retry = null; + + this.closing = false; + } + + constructURL(){ + var pageURI = window.location; + + var scheme = pageURI.scheme == "https" ? "wss:" : "ws:"; + var host = pageURI.host; + + return scheme + "//" + host + "/socket"; + } + + open(){ + if (this._retry){ + clearTimeout(this._retry); + } + this.closing = false; + + this.websocket = new WebSocket(this.options.url); + this.websocket.onopen = (e) =>{ console.log("WebSocket connected"); this._state = "ONLINE"; }; + this.websocket.onclose = (e)=>{ this._onclose(e); this._state = "OFFLINE"; }; + this.websocket.onerror = (e)=>{ this._onerror(e); this._state = "ERROR"; }; + this.websocket.onmessage = (e)=>{ this._onmessage(e); }; + return this; + } + + close(){ + if (this.websocket){ + this.closing = true; + this.websocket.close(200,"close() called"); + } + } + + request(msgtype,msg,timeout){ + let message = { + id: this._id++, + type: msgtype, + message: msg, + } + + if (!timeout) + timeout = this.defaultTimeout; + + if (timeout != -1){ + return new Promise((resolve,reject)=>{ + let to = setTimeout(()=>{ + delete this.callbacks[message.id]; + reject("timed out"); + },timeout); + + this.callbacks[message.id] = (msgtype,msg)=>{ + clearTimeout(to); + delete this.callbacks[message.id]; + if (msgtype == "error") + reject(msg); + else + resolve({type: msgtype,message: msg}); + }; + + this.websocket.send( + JSON.stringify(message) + ); + }); + } else { + new Promise((resolve,reject)=>{ + this.websocket.send( + JSON.stringify(message) + ); + resolve(); + }); + } + + } + + + _onclose(evt){ + this.websocket = null; + this.options.onclose && this.options.onclose(evt); + if (!this.closing) + { + this._retry = setTimeout(() => { + this._retry = null; + console.log("reconnect...") + this.open(); + }, 5000); + } + } + _onerror(evt){ + this.options.onerror && this.options.onerror(evt); + } + _onmessage(evt){ + try + { + let j = JSON.parse(evt.data); + let cb = this.callbacks[ j.id ]; + cb && cb(j.type,j.message); + } catch(exc){ + console.log(exc,evt.data); + } + + } + } class LNVue { @@ -13,17 +288,25 @@ this.data = Object.assign({}, options.data, { LNVue: this, msg: "Hello World" }); this.promises = []; + this.globals = { + currentPromises, + }; + + this.statusText = "LNVue preparing"; + Vue.prototype.$LNVue = this; LNVue.$_ = this; - console.log("LNVue: preparing"); - this.navigation = {}; + this.identity = { + uniqueID: null, + identityName: "", + }; Promise .all(LNVue.promises) .then(()=>{ - console.log("LNVue: starting"); + this.status("LNVue: starting"); LNVue.vueRouter.addRoutes([{ path: "*", @@ -31,7 +314,9 @@ template: `

404 Not Found

The URL you tried to reach is not existing.`, }, }]); - + }, + (cause)=>{ + this.status("LNVue: start failed: " + cause); }); this.vue = null; @@ -47,11 +332,31 @@ data: this.data, router: LNVue.vueRouter, }); - }); + }); }); + LNVue.onidle(()=>{ + this.socket = new LNVueWebSocket(this); + this.socket.open(); + }); + } + + Version(){ return "0.2alpha"; }; + getCurrentPromises() { + return this.globals.currentPromises; + } + + status(){ + if (arguments.length == 1){ + this.statusText = arguments[0]; + return this; + } else if (arguments.length == 0){ + return this.statusText; + } else + throw "LNVue.status(): too many arguments"; + } addModule(modSpec){ if (modSpec.navigation instanceof Object){ @@ -61,13 +366,16 @@ LNVue.$each(modSpec.routes,(key,route)=>{ if ((route instanceof Object) && route.url) { - let p = new Promise((resolve,reject)=>{ + let p = LNVue.LNPromise( "addModule()", (resolve,reject)=>{ LNVue .fetch(route.url) .then((src)=>{ this.addRoute(key,{ template: src, data: ()=>{ return this.data; }, }); resolve(); - }); + }, + (cause)=>{ + console.log("loading route.url failed: ",cause); + }); }); this.promises.push(p); } else if (route instanceof Object){ @@ -85,6 +393,45 @@ ]); } + /* Authentication API */ + + requestChallenges(identityName,secureAttributeTypeName){ + return new Promise((resolve,reject)=>{ + this.socket.request("AuthenticationRequest",{ + IdentityName: identityName, + SecureAttributeTypeName: secureAttributeTypeName, + }) + .then((challenges)=>{ + console.log("rx challenges",challenges); + resolve(challenges); + }, + (error)=>{ + console.log("Login challenges could not be retrieved", error); + } + ); + }); + } + + authenticate(identityName,secureAttributeID,challenge,prove){ + let authenticationProve = { + IdentityName: identityName, + SecureAttributeUniqueID: secureAttributeID, + Challenge: challenge, + Prove: prove, + }; + console.log("authenticate", authenticationProve); + this.socket.request("AuthenticationProve", authenticationProve) + .then((identity)=>{ + console.log("auth",identity); + }, + (error)=>{ + console.log("auth error",error); + }); + } + + + + } Object.defineProperty( LNVue, '$LNVue', { @@ -155,8 +502,7 @@ LNVue.promises = []; LNVue.fetch = function(url,cb){ - let self = this; - return new Promise(function(resolve,reject){ + return LNVue.LNPromise("loading",(resolve,reject)=>{ fetch(url) .then((response => { if (response.status.toString().startsWith("2")) @@ -182,5 +528,37 @@ }); }; + LNVue.LNPromise = function(label, action){ + console.log(label,action); + return new LNVuePromise(label, action); + } + + LNVue.encodeB64 = base64EncArr; + LNVue.decodeB64 = base64DecToArr; + + LNVue.encodeHex = (bytes) => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + LNVue.decodeHex = (hexString) => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); + + ArrayBuffer.combine = function(...args){ + let byteLength = 0; + args.forEach((arg,index)=>{ + byteLength = byteLength + arg.byteLength; + }); + + let result = new Uint8Array(byteLength); + let p = 0; + args.forEach((arg,index)=>{ + for (let n=0;n