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
+
+
+
{{ 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