Compare commits

...

13 Commits

Author SHA1 Message Date
Harald Wolff de0219f509 WIP 2020-01-13 11:14:39 +01:00
Harald Wolff 63f02b047e WIP 2020-01-13 09:52:44 +01:00
Harald Wolff 6ff597c106 ln.identities 2019-11-29 09:29:30 +01:00
Harald Wolff 4f67439042 Implement WSHello 2019-11-26 12:22:27 +01:00
Harald Wolff b870fe214c WIP 2019-11-24 15:10:36 +01:00
Harald Wolff db1f67805b Use history mode as default for vue-router 2019-11-19 11:38:16 +01:00
Harald Wolff caff9e19d5 Authentication Alpha 2019-11-18 08:54:22 +01:00
Harald Wolff f001840670 Authentication Alpha 2019-11-15 14:07:09 +01:00
Harald Wolff d1663cbbbe WIP 2019-11-04 09:57:00 +01:00
Harald Wolff 0de72abc3d #2: further fix first transition, fix wrong position on scrolled document 2019-10-17 11:45:01 +02:00
Harald Wolff 9bb9a877af Added VS Code workspace 2019-10-17 11:29:46 +02:00
Harald Wolff 1f3608bc21 #1 fixed 2019-10-17 11:29:36 +02:00
Harald Wolff 77724600d1 #2 fixed 2019-10-17 11:29:18 +02:00
16 changed files with 1320 additions and 238 deletions

View File

@ -36,6 +36,58 @@
font-style: normal;
}
* {
flex-grow: 1;
}
div {
display: inline-block;
}
h1, h2, h3, h4, h5 {
display: block;
}
.h1, .h2, .h3, .h4, .h5 {
display: inline-block;
}
h1,.h1 {
font-size: 200%;
padding: 8px;
}
h2,.h2 {
font-size: 150%;
padding: 8px;
}
h3,.h3 {
font-size: 110%;
padding: 8px;
}
.flex {
display: flex;
flex-direction: row;
}
.flex.column {
flex-direction: column;
}
.popup {
position: absolute;
top: 100%;
right: 0%;
width: 100%;
border: 1px solid black;
background-color: white;
padding: 8px;
}
sym {
display: inline-block;
width: 24px;
@ -49,6 +101,18 @@ sym.trash::before {
color: red;
}
.no-border {
border: none;
display: inline-block;
padding: 0px;
margin: 0px;
}
.fa-solid {
font-family: 'fa-solid';
font-size: 20px;
}
div.ln-tooltip {
position: absolute;
@ -65,6 +129,7 @@ div.ln-tooltip {
}
div.ln-tooltip[VISIBLE] {
opacity: 1.0;
transition: opacity 300ms linear;
}
body {
@ -72,6 +137,37 @@ body {
font-size: 12px;
}
.block {
display: block;
}
.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;
@ -142,6 +238,20 @@ div.ln-select::after {
pointer-events: none;
}
.ln-identity {
position: relative;
display: inline-block;
flex-grow: 0;
flex-shrink: 0;
width: 200px;
text-align: center;
padding: 8px;
font-size: 150%;
}
.ln-navbar {
display: block;
width: 100%;
@ -152,7 +262,6 @@ div.ln-select::after {
padding-top: 4px;
padding-bottom: 4px;
margin-bottom: 2em;
}
.ln-navitem {
@ -181,18 +290,22 @@ div.ln-navitem > div.ln-nav-children {
top: 100%;
opacity: 0.0;
transition: opacity 200ms;
transition: opacity 300ms;
border: 1px solid black;
border-top: none;
background-color: white;
padding: 8px;
pointer-events: none;
}
div.ln-navitem:hover > div.ln-nav-children {
opacity: 1.0;
transition: opacity 200ms;
transition: opacity 300ms;
pointer-events: visible;
}
div.ln-nav-children > .ln-navitem {
@ -206,6 +319,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;

103
demo.html
View File

@ -5,30 +5,41 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="css/ln.vue.css"/>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="js/vue-router.js"></script>
<script type="text/javascript" src="js/ln.vue.js"></script>
<script type="text/javascript" src="js/ln.vue.components.js"></script>
<script type="text/javascript" src="js/ln.vue.table.js"></script>
<script type="text/javascript" src="js"></script>
<style>
table#controls > tbody > tr > td:nth-child(3) {
padding-left: 24px;
}
div#frame {
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div id="frame">
<h1>ln.vue Demo Application</h1>
<div v-if="false">
<h2>Please wait for application to be loaded...</h2>
</div>
<ln-navbar></ln-navbar>
<router-view></router-view>
<div id="frame" class="ln-view">
<section id="header">
<div class="flex">
<div class="h1">ln.vue Demo Application</div>
<ln-identity></ln-identity>
</div>
<ln-navbar></ln-navbar>
</section>
<section id="body">
<div v-if="false">
<h2>Please wait for application to be loaded...</h2>
</div>
<div id="viewPane" class="ln-viewpane">
<router-view></router-view>
</div>
</section>
<section id="footer">
<ln-statusbar></ln-statusbar>
</section>
</div>
<script type="text/javascript">
@ -41,7 +52,14 @@
},
routes: {
'/': {
template: `<div>This is the ln.vue demo application. Feel free to test all the features, you can find.<br>Here should be a message:<br><i>{{ message.value }}</i></div>`,
template: `<div>This is the ln.vue demo application.<br>
Feel free to test all the features, you can find.<br>
Here should be a message:<br>
<i>{{ message.value }}</i><br>
<br>
<div v-if="LN.Vue.$_.identity.hasRole(LN.Identity.VIEW,'demo')">You are allowed to see this!</div>
<div v-if="!LN.Vue.$_.identity.hasRole(LN.Identity.VIEW,'demo')">Login using demo/demopass to see the hidden message!</div>
</div>`,
},
}
};
@ -71,19 +89,9 @@
}
};
/* LNVue.addRouteTemplate(
"/state",
`<div>Current User Input object:<br><br>
<span class="json">{{ JSON.stringify(controls,null,4) }}</span>
</div>`
);
LNVue.addRoute("/controls","/controls.html");
LNVue.addRoute("/table","/table.html");
*/
let message = { value: "" };
app = new LNVue("#frame",{
app = new LN.Vue("#frame",{
data: {
message: message,
columns: {
@ -177,15 +185,54 @@
}
});
app.addModule({
navigation: {
protocols: {
label: 'Protocols',
navigation: {
webauthn: {
label: 'webauthn',
path: '/protocols/webauthn',
}
}
}
},
routes: {
'/protocols/webauthn': {
url: '/webauthn.html',
}
},
});
app.addModule({
navigation: {
login: {
label: "Login Pane",
path: "/login",
}
},
routes: {
'/login': `<div>Login Pane<br><ln-login-pane style="border: 1px solid black;"></ln-login-pane></div>`,
}
});
app.Start();
LNVue.onidle(()=>{
LN.$idle(()=>{
message.value = "idle time reached. Message written.";
setTimeout(()=>{
message.value = "fired from the idle job: new message after 1000ms.";
},1000);
});
for (let n=5;n<15;n++){
new LN.Promise((resolve,reject)=>{
setTimeout(()=>{
resolve();
},n * 1000);
},n + " sec promise test");
}
</script>
</body>

View File

@ -0,0 +1,84 @@
(function(){
/*\
|*|
|*| Base64 / binary data / UTF-8 strings utilities (#1)
|*|
|*| https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*| Author: madmurphy
|*|
|*| Harald Wolff-Thobaben: Small adaptions to create a static class
|*|
\*/
class Base64
{
constructor(){
}
static 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;
}
static uint6ToB64(nUint6){
return nUint6 < 26 ?
nUint6 + 65
: nUint6 < 52 ?
nUint6 + 71
: nUint6 < 62 ?
nUint6 - 4
: nUint6 === 62 ?
43
: nUint6 === 63 ?
47
:
65;
}
static encode(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;
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode(Base64.uint6ToB64(nUint24 >>> 18 & 63), Base64.uint6ToB64(nUint24 >>> 12 & 63), Base64.uint6ToB64(nUint24 >>> 6 & 63), Base64.uint6ToB64(nUint24 & 63));
nUint24 = 0;
}
}
return eqLen === 0 ?
sB64Enc
:
sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "==");
}
static decode(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 |= Base64.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;
}
}
LN.$add("LN.Base64",Base64);
})();

View File

@ -0,0 +1,51 @@
(function(){
class LNIdentity
{
constructor(src){
if (!src)
src = {};
this.IdentityName = src.IdentityName || "";
this.UniqueID = src.UniqueID || null;
this.Roles = src.AssignedRoles || [];
}
findRolesByName(identityName){
let roleMask = 0;
this.Roles.forEach(role => {
if (role.IdentityName == identityName)
roleMask = role.Roles;
});
return roleMask;
}
findRolesByID(identityUniqueID){
let roleMask = 0;
this.Roles.forEach(role => {
if (role.UniqueID == identityUniqueID)
roleMask = role.Roles;
});
return roleMask;;
}
hasRole(role,identityName){
let roles = this.findRolesByName(identityName);
return (roles & role) == role;
}
}
LNIdentity.VIEW = (1<<0);
LNIdentity.USE = LNIdentity.VIEW | (1<<1);
LNIdentity.CONTROL = LNIdentity.VIEW | (1<<2);
LNIdentity.MANAGE = LNIdentity.CONTROL | (1<<3);
LNIdentity.ADMIN = 0x0000FFFF;
LNIdentity.MANAGEROLES = (1<<16);
LNIdentity.IMPERSONATE = (1<<24);
LNIdentity.OWN = 0x0FFFFFFF;
LNIdentity.BE = 0x0000FFFF;
LNIdentity.SUPER = 0x7FFFFFFF;
LN.$add("LN.Identity",LNIdentity);
})();

View File

@ -0,0 +1,59 @@
(function(){
class LNPromise
{
constructor(executor, label){
this.promise = new Promise(
(resolve,reject) => {
this.resolve = (v) => {
this._s.state = "ready";
resolve(v);
this.release();
},
this.reject = (e) => {
this._s.state = "failed";
reject(e);
this.release();
}
executor && executor(
this.resolve,
this.reject
);
}
);
if (!label)
label = "N.D.";
this._s = {
label,
state: "waiting"
};
this.idx = LN.$unique();
LNPromise.$current[this.idx] = this._s;
}
label(){
return this._s.label;
}
state(){
return this._s.state;
}
release(){
setTimeout(()=>{
Vue.delete(LNPromise.$current, this.idx);
},1000);
}
then(){
return this.promise.then.apply(this.promise, arguments);
}
static getCurrentPromises(){ return LNPromise.$current; }
}
LNPromise.$current = {};
LN.$add("LN.Promise",LNPromise);
})();

View File

@ -4,7 +4,8 @@ let nextTooltipKey = 0;
var tooltipData = {};
var tootlipVisible = false;
var tooltipEl = LNVue.$(`<div class="ln-tooltip"></div>`);
var tooltipEl = LN.$(`<div class="ln-tooltip"></div>`);
function findTooltipData(el){
@ -18,40 +19,42 @@ function findTooltipData(el){
}
function tooltipShow(ev,tooltip){
if (tooltipEl.parentElement)
document.body.appendChild(tooltipEl);
tooltipEl.innerText = tooltip.value;
tooltipEl.setAttribute("VISIBLE","");
tootlipVisible = true;
}
function tooltipHide(ev,tooltip){
tooltipEl.removeAttribute("VISIBLE");
setTimeout(()=>{
//document.body.removeChild(tooltipEl);
}, 600);
tootlipVisible = false;
}
function tooltipMouseOver(ev){
if (!document.body.contains(tooltipEl)){
document.body.appendChild(tooltipEl);
LN.$idle(()=>{ tooltipMouseOver(ev);});
return;
}
let tooltip = findTooltipData(ev.target);
if (!tooltip)
console.log(ev);
if (tooltipEl){
tooltipEl.style.left = `${ev.pageX+3}px`;
tooltipEl.style.top = `${ev.pageY+3}px`;
}
if (tooltip.timeout)
clearTimeout(tooltip.timeout);
if (tootlipVisible)
tooltipHide(ev,tooltip);
else
if (tootlipVisible){
tooltip.timeout = setTimeout(() => {
tooltipHide(ev,tooltip);
}, (tooltip.delay ? (tooltip.delay / 4) : 200));
} else {
tooltip.timeout = setTimeout(() => {
tooltipShow(ev,tooltip);
}, tooltip.delay || 800);
if (tooltipEl){
tooltipEl.style.left = `${ev.x+3}px`;
tooltipEl.style.top = `${ev.y+3}px`;
}
}
@ -87,6 +90,50 @@ Vue.directive('tooltip',{
},
});
Vue.component('ln-statusbar',{
data: function(){
return {
current: LN.Promise.getCurrentPromises(),
};
},
computed: {
CurrentPromises: function(){
return this.current;
}
},
template: `
<div
class="ln-statusbar"
>
<div>{{ LNVue.$_.statusText }}</div>
<div style="flex-grow: 0;">
<span
v-if="!LNVue.$_.identity.UniqueID">NOT LOGGED IN
</span>
<span
v-if="LNVue.$_.identity.UniqueID">{{ LNVue.$_.identity.IdentityName }}
</span>
</div>
<div style="flex-grow: 0;">{{ LNVue.$_.socket && LNVue.$_.socket._state }}</div>
<div
class="ln-background-tasks"
>{{ Object.keys(CurrentPromises).length || "No" }} Background Tasks
<div
v-if="Object.keys(CurrentPromises).length"
>
<div
class="block"
v-for="promise in CurrentPromises"
:state="promise.state"
>{{ promise.label }}</div>
</div>
</div>
<div style="flex-grow: 0;">{{ LNVue.$_.Version() }}</div>
</div>
`,
});
Vue.component('ln-navitem',{
props: {
value: {
@ -164,6 +211,19 @@ 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: `<input
type="password"
v-bind:value="value"
autocomplete="current-password"
v-on:input="$emit('input', $event.target.value)">`,
});
Vue.component('ln-textarea',{
props: {
@ -428,12 +488,24 @@ Vue.component('ln-select',{
type: Boolean,
default: true,
},
render: {
type: Function,
default: function(value){
return value;
}
},
key: {
type: Function,
default: function(key,value){
return key;
}
}
},
computed: {
prepared: {
get: function(){
let prepared = {};
LNVue.each(this.items,(element,index) => {
LN.$each(this.items,(element,index) => {
prepared[index] = element;
});
return prepared;
@ -453,14 +525,200 @@ Vue.component('ln-select',{
value=""
></option>
<option
v-for="(item,key) in prepared"
:value="key"
:selected="key == value"
>{{ item }}</option>
v-for="(item,_key) in prepared"
v-bind:value="key(_key,item)"
:selected="_key == value"
>{{ render(item) }}</option>
</select>
</div>
`,
});
Vue.component('ln-identity',{
data: function(){
return {
popupVisible: false,
};
},
methods: {
logout(){
LNVue.$_.authenticate("",null,"","");
this.popupVisible = false;
},
popup(){
this.popupVisible = !this.popupVisible;
},
},
template: `<div class="ln-identity">
<div
v-if="LNVue.$_.identity.UniqueID"
v-tooltip="LNVue.$_.identity.UniqueID"
>
{{ LNVue.$_.identity.IdentityName }}
</div>
<div
v-if="!LNVue.$_.identity.UniqueID"
@click="popup()"
>Not logged in
</div>
<button
class="no-border fa-solid"
@click="popup();"
>&#xf107</button>
<div
class="popup"
v-if="popupVisible"
>
<div
v-if="LNVue.$_.identity.UniqueID"
><button
@click="logout()"
>Logout</button>
</div>
<div
v-if="!LNVue.$_.identity.UniqueID"
is="ln-login-pane"
>
</div>
</div>
</div>`,
});
Vue.component('ln-popup',{
props: {
title: String,
visible: {
type: Boolean,
default: false
}
},
methods: {
show: function(){
this.visible = true;
},
hide: function(){
this.visible = false;
}
},
template: `<div class="ln-popup">
<div
class="ln-popup-frame"
><div
class="ln-popup-title"
v-if="title"
>{{ title }}</div>
<div class="ln-popup-content">
<slot></slot>
</div>
<div
class="ln-popup-footer"
>
<button
@click="hide()"
>schliessen</button>
</div>
</div>
</div>
`,
});
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;
}
});
},
logout(){
LNVue.$_.authenticate("",null,"","");
},
authenticateSeededPassword(challenge){
let encoder = new TextEncoder();
let seed = LNVue.decodeHex(challenge.AuthenticationParameters);
let password = encoder.encode(challenge.prove);
let secretSource = ArrayBuffer.combine(seed, password, seed);
crypto.subtle.digest("SHA-256",secretSource)
.then((secret)=>{
let challengebytes = LN.Base64.decode(challenge.Challenge);
secret = new Uint8Array(secret);
let proveSource = ArrayBuffer.combine(challengebytes, secret, challengebytes);
crypto.subtle.digest("SHA-256",proveSource)
.then((prove)=>{
prove = LN.Base64.encode(new Uint8Array(prove));
LNVue.$_.authenticate(this.identityName,challenge.SecureAttributeID,challenge.Challenge,prove);
});
});
}
},
data: ()=>{
return {
identityName: "",
};
},
template: `<div>
<div
v-if="LNVue.$_.identity.UniqueID"
>
<div>Logged in as</div>
{{ LNVue.$_.identity.IdentityName }}
<button @click="logout()">Logout</button>
</div>
<div
v-if="!LNVue.$_.identity.UniqueID">
<div
v-if="LNVue.$_.identity.IdentityName == ''"
><form @submit.prevent="stage1();">
<label for="username">Username</label>
<ln-textfield
name="username"
autocomplete="username"
v-model="identityName"></ln-textfield>
<button
:disabled="identityName == ''"
type="submit"
>continue</button>
</form>
</div>
<div
v-if="LNVue.$_.identity.IdentityName != ''"
>
<div
v-for="challenge in LNVue.$_.identity.challenges"
>
<form @submit.prevent="authenticateSeededPassword(challenge); return false;">
<input style="display: none;" type="text" name="username" autocomplete="username" :value="LNVue.$_.identity.IdentityName">
<div
v-if="challenge.SecureAttributeTypeName == 'SeededPassword'"
>{{ challenge.SecureAttributeLabel }}
<ln-password
v-model="challenge.prove"
></ln-password>
<button
type="submit"
:disabled="challenge.prove==''"
>login</button>
</form>
</div>
</div>
</div>
</div>`,
});
})();

274
js/lib/ln.vue.js 100644
View File

@ -0,0 +1,274 @@
(function (){
let exModule = {
label: 'Example Module',
routes: [
{
path: '/about',
template: `This is about this example!`,
}
],
navigation: {
'99': {
label: 'About',
href: '/about'
}
}
};
let defaultOptions = {
element: 'body',
modules: [ exModule, ],
};
class LNVue
{
constructor(el,options = {}){
this.options = Object.assign({
routes: [],
data: {},
}, options );
this._el = el;
this.data = Object.assign({}, options.data, { LNVue: this, msg: "Hello World" });
this.promises = [];
this.statusText = "LNVue preparing";
Vue.prototype.$LNVue = this;
LNVue.$_ = this;
this.navigation = {};
this.identity = new LN.Identity();
Promise
.all(LNVue.promises)
.then(()=>{
this.status("LNVue: starting");
LNVue.vueRouter.addRoutes([{
path: "*",
component: {
template: `<h2>404 Not Found</h2>The URL you tried to reach is not existing.`,
},
}]);
},
(cause)=>{
this.status("LNVue: start failed: " + cause);
});
this.vue = null;
LNVue.$instance.resolve(this);
}
Start(){
Promise
.all(this.promises)
.then(()=>{
LN.$idle(()=>{
this.vue = new Vue({
el: this._el,
data: this.data,
router: LNVue.vueRouter,
});
});
});
LN.$idle(()=>{
this.socket = new LN.Vue.WebSocket(this);
this.socket.open();
});
LN.$idle(()=>{
LNVue.$start.resolve(this);
});
}
storage(){
return window.localStorage;
}
sessionID(){
if (arguments.length == 1){
this.storage().setItem("LNVueSessionID",arguments[0]);
console.log("LNVue.SID <= " + arguments[0]);
return this;
} else
{
let sid = this.storage().getItem("LNVueSessionID");
console.log("LNVue.SID == " + sid);
if (!sid)
{
sid = "00000000-0000-0000-0000-000000000000";
}
return sid;
}
}
Version(){ return "0.2alpha"; };
getCurrentPromises() {
return LN.Promise.getCurrentPromises();
}
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){
LNVue.deepAssign(modSpec.navigation,this.navigation);
}
LN.$each(modSpec.routes,(key,route)=>{
if ((route instanceof Object) && route.url)
{
let p = new LN.Promise((resolve,reject)=>{
LN.$fetch(route.url)
.then((src)=>{
this.addRoute(key,{ template: src, data: ()=>{ return this.data; }, });
resolve();
},
(cause)=>{
console.log("loading route.url failed: ",cause);
});
},`addModule(${route.url})`);
this.promises.push(p);
} else if (route instanceof Object){
this.addRoute(key,{ template: route.template, data: ()=>{ return this.data; }, } );
} else {
this.addRoute(key,{ template: route, data: ()=>{ return this.data; }, } );
}
});
}
addRoute(path,component){
LNVue.vueRouter.addRoutes([
{ path, component, },
]);
if (this.vue){
let route = this.vue.$route;
LNVue.vueRouter.replace("/");
LNVue.vueRouter.replace(route);
}
}
/* Authentication API */
requestChallenges(identityName,secureAttributeTypeName){
return new Promise((resolve,reject)=>{
this.socket.request("AuthenticationRequest",{
IdentityName: identityName,
SecureAttributeTypeName: secureAttributeTypeName,
})
.then((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,
};
this.socket.request("AuthenticationProve", authenticationProve)
.then((identity)=>{
this.identity = new LN.Identity(identity.message);
},
(error)=>{
this.identity = new LN.Identity();
});
}
rpc(moduleName,methodName,parameters){
return new Promise((resolve,reject)=>{
let rpcCall = {
module: moduleName,
method: methodName,
parameters: parameters
};
this.socket.request("RPCCall",rpcCall)
.then(
(result)=>{
if (result.message.error)
{
console.log("rpc call failed", result.message.error);
reject(result.message.error);
}
else
resolve(result.message.Result);
},
(error)=>{
console.log("rpc failed", error);
reject(error);
}
);
});
}
static $LNVue(){
return LNVue.$_;
}
}
LNVue.$instance = new LN.Promise(()=>{},'LN.Vue Instance Promise');
LNVue.$start = new LN.Promise(()=>{},'LN Vue Startup Promise');
LNVue.vueRouter = new VueRouter({
mode: 'history',
routes: [],
});
LNVue.deepAssign = function(source,target){
LN.$each(source,function(key){
if (target[key] instanceof Object){
LNVue.deepAssign(src[key],target[key]);
} else {
target[key] = source[key];
}
});
}
LNVue.routes = [];
LNVue.promises = [];
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<arg.byteLength;n++)
{
result[p++] = arg[n];
}
});
console.log("combine",new Uint8Array(result));
return result.buffer
};
LN.$add("LN.Vue",LNVue);
LN.$add("LNVue",LNVue);
})();

View File

@ -0,0 +1,45 @@
(function(){
// LNVue.prototype.createCredentials = function(_challenge,rp,userId,crossplatform,attestation){
// crossplatform = crossplatform ? true : false;
// if (!attestation)
// attestation = "none";
// let credCreateOptions = {
// challenge: Uint8Array.from(_challenge, c => c.charCodeAt(0)),
// rp: {
// id: rp,
// name: 'LN.Vue Demo Application',
// },
// user: {
// id: Int8Array.from(userId, c => c.charCodeAt(0)),
// name: 'SomeUser',
// displayName: "Some user for now...",
// },
// pubKeyCredParams: [
// {
// type: 'public-key',
// alg: -7,
// }
// ],
// authenticatorSelection: {
// authenticatorAttachment: crossplatform ? 'cross-platform' : 'platform',
// },
// timeout: 60000,
// attestation: attestation,
// };
// console.log(credCreateOptions);
// navigator.credentials.create({
// publicKey: credCreateOptions
// });
// };
// LNVue.prototype.createAuthToken = function(){
// };
})();

View File

@ -0,0 +1,137 @@
(function(){
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";
let WSHello = {
ApplicationSessionID: this.LNVue.sessionID(),
};
console.log("WSHello request",WSHello);
this.request("WSHello",WSHello)
.then((wsh)=>{
console.log("WSHello response",wsh);
this.LNVue.sessionID(wsh.message.ApplicationSessionID);
this.LNVue.identity = new LN.Identity(wsh.message.SessionIdentity);
});
};
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);
}
}
}
LN.$add("LN.Vue.WebSocket",LNVueWebSocket);
})();

107
js/ln.js 100644
View File

@ -0,0 +1,107 @@
(function(globals){
let _unique = new Date().getTime();
let __idles__ = [];
class LN
{
constructor(opts){
}
static $each(oa,cb){
if (oa instanceof Array){
let result = [];
oa.forEach((value,index)=>{
result.push(cb.call(value,index,value));
});
return result;
} else if (oa instanceof Object){
let result = {};
Object.keys(oa).forEach((key)=>{
if (oa.hasOwnProperty(key)){
result[key] = cb.call(oa[key],key,oa[key]);
}
});
return result;
}
}
static $idle(cb,thisval = null){
let scheduled = __idles__.length > 0;
let n=0;
for (;n<__idles__.length;n++){
let idle = __idles__[n];
if ((idle[0] == cb) && (idle[1] == thisval))
break;
}
if (n == __idles__.length)
__idles__.push([cb,thisval]);
if (!scheduled)
setTimeout(()=>{
while (__idles__.length > 0){
let idle = __idles__.pop();
idle[0].call(idle[1]);
}
},0);
}
static $unique(){
return _unique++;
}
static $fetch(url,cb){
return new LN.Promise((resolve,reject)=>{
fetch(url)
.then((response => {
if (response.status.toString().startsWith("2"))
{
let t = response.text();
cb && cb(t,null);
resolve(t);
} else {
cb && cb(null, response.statusText);
reject(response.statusText);
}
}));
}, `fetch(${url})`);
}
static $resolve(v){
if (v instanceof Function)
return v();
return v;
}
static $add(classpath,c){
let p = classpath.split(".");
if (p.length < 1)
throw "invalid classpath";
let container = globals;
while (p.length > 1){
let next = p.shift();
if (!container[next])
container[next] = {}
container = container[next];
}
let prev = container[p[0]];
container[p[0]] = c;
if (prev)
{
LN.$each(prev,(key,value)=>{
c[key] = value;
});
}
};
static $(selector){
let el = document.createElement("parse");
el.innerHTML = selector;
return el.firstChild;
}
}
LN.prototypes = {};
LN.$add("LN", LN);
})(window);

View File

@ -1,186 +0,0 @@
var LNVue = (function (){
class LNVue
{
constructor(el,options = {}){
this.options = Object.assign({
routes: [],
data: {},
}, options );
this._el = el;
this.data = Object.assign({}, options.data, { LNVue: this, msg: "Hello World" });
this.promises = [];
Vue.prototype.$LNVue = this;
LNVue.$_ = this;
console.log("LNVue: preparing");
this.navigation = {};
Promise
.all(LNVue.promises)
.then(()=>{
console.log("LNVue: starting");
LNVue.vueRouter.addRoutes([{
path: "*",
component: {
template: `<h2>404 Not Found</h2>The URL you tried to reach is not existing.`,
},
}]);
});
this.vue = null;
}
Start(){
Promise
.all(this.promises)
.then(()=>{
LNVue.onidle(()=>{
this.vue = new Vue({
el: this._el,
data: this.data,
router: LNVue.vueRouter,
});
});
});
}
Version(){ return "0.2alpha"; };
addModule(modSpec){
if (modSpec.navigation instanceof Object){
LNVue.deepAssign(modSpec.navigation,this.navigation);
}
LNVue.$each(modSpec.routes,(key,route)=>{
if ((route instanceof Object) && route.url)
{
let p = new Promise((resolve,reject)=>{
LNVue
.fetch(route.url)
.then((src)=>{
this.addRoute(key,{ template: src, data: ()=>{ return this.data; }, });
resolve();
});
});
this.promises.push(p);
} else if (route instanceof Object){
this.addRoute(key,{ template: route.template, data: ()=>{ return this.data; }, } );
} else {
this.addRoute(key,{ template: route, data: ()=>{ return this.data; }, } );
}
});
}
addRoute(path,component){
let self = this;
LNVue.vueRouter.addRoutes([
{ path, component, },
]);
}
}
Object.defineProperty( LNVue, '$LNVue', {
get: ()=>{ return LNVue.$_; },
});
LNVue.vueRouter = new VueRouter({
routes: [],
});
LNVue.$idles = [];
LNVue.onidle = function(cb,thisval = null){
let scheduled = LNVue.$idles.length > 0;
let n=0;
for (;n<LNVue.$idles.length;n++){
let idle = LNVue.$idles[n];
if ((idle[0] == cb) && (idle[1] == thisval))
break;
}
if (n == LNVue.$idles.length)
LNVue.$idles.push([cb,thisval]);
if (!scheduled)
setTimeout(()=>{
while (LNVue.$idles.length > 0){
let idle = LNVue.$idles.pop();
idle[0].call(idle[1]);
}
},0);
}
LNVue.$each = function(oa,cb){
if (oa instanceof Array){
let result = [];
oa.forEach((value,index)=>{
result.push(cb.call(value,index,value));
});
return result;
} else if (oa instanceof Object){
let result = {};
Object.keys(oa).forEach((key)=>{
if (oa.hasOwnProperty(key)){
result[key] = cb.call(oa[key],key,oa[key]);
}
});
return result;
}
}
LNVue.deepAssign = function(source,target){
LNVue.$each(source,function(key){
if (target[key] instanceof Object){
LNVue.deepAssign(src[key],target[key]);
} else {
target[key] = source[key];
}
});
}
LNVue.$ = function(src){
let el = document.createElement("parse");
el.innerHTML = src;
return el.firstChild;
}
LNVue.routes = [];
LNVue.promises = [];
LNVue.fetch = function(url,cb){
let self = this;
return new Promise(function(resolve,reject){
fetch(url)
.then((response => {
if (response.status.toString().startsWith("2"))
{
let t = response.text();
cb && cb(t,null);
resolve(t);
} else {
cb && cb(null, response.statusText);
reject(response.statusText);
}
}));
});
}
LNVue.each = function(o,cb){
if (o instanceof Array)
o.forEach((value,index)=> cb(value,index) );
else
Object.keys(o).forEach((key)=>{
if (o.hasOwnProperty(key))
cb(o[key],key);
});
};
return LNVue;
})();

View File

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}

9
webauthn.html 100644
View File

@ -0,0 +1,9 @@
<div>
<h1>webauthn - demo</h1>
<div>Use the buttons to start demo action...</div>
<button
@click="LNVue.createCredentials('ABCDEFGHIJKLMNOPQRSTUVWXYZ','LNVue Demo Application','1234567890',true);"
>Register</button>
</div>