mirror of
https://github.com/Luzifer/vault-otp-ui.git
synced 2024-11-08 08:10:11 +00:00
Rewrite frontend on ES6 with Vue rendering
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
ff37285740
commit
ab4b20f015
3 changed files with 251 additions and 440 deletions
|
@ -1,154 +0,0 @@
|
|||
currentTimeout = 0
|
||||
clipboard = null
|
||||
preFetch = null
|
||||
|
||||
fetchInProgress = false
|
||||
serverConnectionError = false
|
||||
|
||||
iterationCurrent = 'current'
|
||||
iterationNext = 'next'
|
||||
|
||||
# document-ready function to start Javascript processing
|
||||
$ ->
|
||||
if $('body').hasClass 'state-signedin'
|
||||
initializeApplication()
|
||||
|
||||
# createOTPItem generates code entries from the JSON objects passed from the backend
|
||||
createOTPItem = (item) ->
|
||||
tpl = $('#tpl-otp-item').html()
|
||||
|
||||
otpItem = $(tpl)
|
||||
otpItem.find('.badge').text item.code.replace(/^([0-9]{3})([0-9]{3})$/, '$1 $2').replace(/^([0-9]{2})([0-9]{3})([0-9]{3})$/, '$1 $2 $3')
|
||||
otpItem.find('.title').text item.name
|
||||
otpItem.find('i.fa').addClass "fa-#{item.icon}"
|
||||
|
||||
otpItem.appendTo $('#keylist')
|
||||
|
||||
# createAlert adds a colored message at the top of the list
|
||||
# type = success / info / warning / danger
|
||||
createAlert = (type, keyword, message, timeout) ->
|
||||
tpl = $('#tpl-message').html()
|
||||
|
||||
alrt = $(tpl)
|
||||
alrt.find('.alert').addClass "alert-#{type}"
|
||||
alrt.find('.alert').find('.keyword').text keyword
|
||||
alrt.find('.alert').find('.message').text message
|
||||
|
||||
alrt.appendTo $('#messagecontainer')
|
||||
|
||||
if timeout > 0
|
||||
delay timeout, () ->
|
||||
alrt.remove()
|
||||
|
||||
# delay is a convenience wrapper to swap parameters of setTimeout
|
||||
delay = (delayMSecs, fkt) ->
|
||||
window.setTimeout fkt, delayMSecs
|
||||
|
||||
# fetchCodes contacts the backend to receive JSON containing current codes
|
||||
fetchCodes = (iteration) ->
|
||||
if fetchInProgress
|
||||
return
|
||||
|
||||
fetchInProgress = true
|
||||
|
||||
if iteration == iterationCurrent
|
||||
successFunc = updateCodes
|
||||
else
|
||||
successFunc = updatePreFetch
|
||||
|
||||
if iteration == iterationCurrent and preFetch != null
|
||||
data = preFetch
|
||||
preFetch = null
|
||||
successFunc data
|
||||
return
|
||||
|
||||
$.ajax
|
||||
url: "codes.json?it=#{iteration}",
|
||||
success: successFunc,
|
||||
dataType: 'json',
|
||||
error: () ->
|
||||
fetchInProgress = false
|
||||
createAlert 'danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0
|
||||
serverConnectionError = true
|
||||
statusCode:
|
||||
401: () ->
|
||||
window.location.reload()
|
||||
500: () ->
|
||||
fetchInProgress = false
|
||||
createAlert 'danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0
|
||||
serverConnectionError = true
|
||||
|
||||
# filterChange is called when changing the filter field and matches the
|
||||
# titles of all shown entries. Those not matching the given regular expression
|
||||
# will be hidden. The filterChange function is also called after a successful
|
||||
# refresh of the shown codes to re-apply
|
||||
filterChange = () ->
|
||||
filter = $('#filter').val().toLowerCase()
|
||||
$('.otp-item').each (idx, el) ->
|
||||
if $(el).find('.title').text().toLowerCase().match(filter) == null
|
||||
$(el).hide()
|
||||
else
|
||||
$(el).show()
|
||||
|
||||
# initializeApplication initializes some basic events and starts the first
|
||||
# polling for codes
|
||||
initializeApplication = () ->
|
||||
$('#keylist').empty()
|
||||
$('#filter').bind 'keyup', filterChange
|
||||
tick 500, refreshTimerProgress
|
||||
fetchCodes iterationCurrent
|
||||
|
||||
# refreshTimerProgress updates the top progressbar to display the
|
||||
# remaining time until the one-time-passwords changes
|
||||
refreshTimerProgress = () ->
|
||||
secondsLeft = timeLeft()
|
||||
$('#timer').css 'width', "#{secondsLeft / 30 * 100}%"
|
||||
|
||||
if secondsLeft < 10 and preFetch == null and not serverConnectionError
|
||||
# Do a pre-fetch to provide a seamless experience
|
||||
fetchCodes iterationNext
|
||||
|
||||
# tick is a convenience wrapper to swap parameters of setInterval
|
||||
tick = (delay, fkt) ->
|
||||
window.setInterval fkt, delay
|
||||
|
||||
# timeLeft calculates the remaining time until codes get invalid
|
||||
timeLeft = () ->
|
||||
now = new Date().getTime()
|
||||
(currentTimeout - now) / 1000
|
||||
|
||||
# updateCodes is being called when the backend delivered codes. The codes
|
||||
# are then rendered and the clipboard methods are re-bound. Afterwards the
|
||||
# next fetchCodes call is timed to that moment when the codes are getting
|
||||
# invalid
|
||||
updateCodes = (data) ->
|
||||
currentTimeout = new Date(data.next_wrap).getTime()
|
||||
|
||||
if clipboard
|
||||
clipboard.destroy()
|
||||
|
||||
$('#initLoader').hide()
|
||||
$('#keylist').empty()
|
||||
for token in data.tokens
|
||||
createOTPItem token
|
||||
|
||||
clipboard = new Clipboard '.otp-item',
|
||||
text: (trigger) ->
|
||||
$(trigger).find('.badge').text().replace(/ /g, '')
|
||||
|
||||
clipboard.on 'success', (e) ->
|
||||
createAlert 'success', 'Success:', 'Code copied to clipboard', 1000
|
||||
e.blur()
|
||||
|
||||
clipboard.on 'error', (e) ->
|
||||
createAlert 'danger', 'Oops.', 'Copy to clipboard failed', 2000
|
||||
|
||||
filterChange()
|
||||
|
||||
delay timeLeft()*1000, ->
|
||||
fetchCodes iterationCurrent
|
||||
fetchInProgress = false
|
||||
|
||||
updatePreFetch = (data) ->
|
||||
preFetch = data
|
||||
fetchInProgress = false
|
326
application.js
326
application.js
|
@ -1,189 +1,161 @@
|
|||
// Generated by CoffeeScript 2.3.1
|
||||
(function() {
|
||||
var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, fetchInProgress, filterChange, initializeApplication, iterationCurrent, iterationNext, preFetch, refreshTimerProgress, serverConnectionError, tick, timeLeft, updateCodes, updatePreFetch;
|
||||
const iterationCurrent = 'current'
|
||||
const iterationNext = 'next'
|
||||
|
||||
currentTimeout = 0;
|
||||
const app = new Vue({
|
||||
|
||||
clipboard = null;
|
||||
computed: {
|
||||
filteredItems() {
|
||||
if (this.filter === '') {
|
||||
return this.otpItems
|
||||
}
|
||||
|
||||
preFetch = null;
|
||||
const items = []
|
||||
|
||||
fetchInProgress = false;
|
||||
|
||||
serverConnectionError = false;
|
||||
|
||||
iterationCurrent = 'current';
|
||||
|
||||
iterationNext = 'next';
|
||||
|
||||
// document-ready function to start Javascript processing
|
||||
$(function() {
|
||||
if ($('body').hasClass('state-signedin')) {
|
||||
return initializeApplication();
|
||||
}
|
||||
});
|
||||
|
||||
// createOTPItem generates code entries from the JSON objects passed from the backend
|
||||
createOTPItem = function(item) {
|
||||
var otpItem, tpl;
|
||||
tpl = $('#tpl-otp-item').html();
|
||||
otpItem = $(tpl);
|
||||
otpItem.find('.badge').text(item.code.replace(/^([0-9]{3})([0-9]{3})$/, '$1 $2').replace(/^([0-9]{2})([0-9]{3})([0-9]{3})$/, '$1 $2 $3'));
|
||||
otpItem.find('.title').text(item.name);
|
||||
otpItem.find('i.fa').addClass(`fa-${item.icon}`);
|
||||
return otpItem.appendTo($('#keylist'));
|
||||
};
|
||||
|
||||
// createAlert adds a colored message at the top of the list
|
||||
// type = success / info / warning / danger
|
||||
createAlert = function(type, keyword, message, timeout) {
|
||||
var alrt, tpl;
|
||||
tpl = $('#tpl-message').html();
|
||||
alrt = $(tpl);
|
||||
alrt.find('.alert').addClass(`alert-${type}`);
|
||||
alrt.find('.alert').find('.keyword').text(keyword);
|
||||
alrt.find('.alert').find('.message').text(message);
|
||||
alrt.appendTo($('#messagecontainer'));
|
||||
if (timeout > 0) {
|
||||
return delay(timeout, function() {
|
||||
return alrt.remove();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// delay is a convenience wrapper to swap parameters of setTimeout
|
||||
delay = function(delayMSecs, fkt) {
|
||||
return window.setTimeout(fkt, delayMSecs);
|
||||
};
|
||||
|
||||
// fetchCodes contacts the backend to receive JSON containing current codes
|
||||
fetchCodes = function(iteration) {
|
||||
var data, successFunc;
|
||||
if (fetchInProgress) {
|
||||
return;
|
||||
}
|
||||
fetchInProgress = true;
|
||||
if (iteration === iterationCurrent) {
|
||||
successFunc = updateCodes;
|
||||
} else {
|
||||
successFunc = updatePreFetch;
|
||||
}
|
||||
if (iteration === iterationCurrent && preFetch !== null) {
|
||||
data = preFetch;
|
||||
preFetch = null;
|
||||
successFunc(data);
|
||||
return;
|
||||
}
|
||||
return $.ajax({
|
||||
url: `codes.json?it=${iteration}`,
|
||||
success: successFunc,
|
||||
dataType: 'json',
|
||||
error: function() {
|
||||
fetchInProgress = false;
|
||||
createAlert('danger', 'Oops.', 'Server could not be contacted. Maybe you (or the server) are offline? Reload to try again.', 0);
|
||||
return serverConnectionError = true;
|
||||
},
|
||||
statusCode: {
|
||||
401: function() {
|
||||
return window.location.reload();
|
||||
},
|
||||
500: function() {
|
||||
fetchInProgress = false;
|
||||
createAlert('danger', 'Oops.', 'The server responded with an internal error. Reload to try again.', 0);
|
||||
return serverConnectionError = true;
|
||||
for (let i of this.otpItems) {
|
||||
if (i.name.toLowerCase().match(this.filter.toLowerCase())) {
|
||||
items.push(i)
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// filterChange is called when changing the filter field and matches the
|
||||
// titles of all shown entries. Those not matching the given regular expression
|
||||
// will be hidden. The filterChange function is also called after a successful
|
||||
// refresh of the shown codes to re-apply
|
||||
filterChange = function() {
|
||||
var filter;
|
||||
filter = $('#filter').val().toLowerCase();
|
||||
return $('.otp-item').each(function(idx, el) {
|
||||
if ($(el).find('.title').text().toLowerCase().match(filter) === null) {
|
||||
return $(el).hide();
|
||||
} else {
|
||||
return $(el).show();
|
||||
return items
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
authUrl,
|
||||
backoff: 500,
|
||||
currentTimeout: null,
|
||||
fetchInProgress: false,
|
||||
filter: '',
|
||||
lastFetch: null,
|
||||
loading: true,
|
||||
preFetch: null,
|
||||
signedIn,
|
||||
otpItems: [],
|
||||
timeLeftPerc: 0.0,
|
||||
},
|
||||
|
||||
el: '#application',
|
||||
|
||||
methods: {
|
||||
|
||||
// Let user know whether the copy command was successful
|
||||
codeCopyResult(success) {
|
||||
this.createAlert(success ? 'success' : 'danger', 'Copy to clipboard...', 'Code copied to clipboard')
|
||||
},
|
||||
|
||||
// Wrapper around toast creation
|
||||
createAlert(variant, title, text, autoHideDelay=2000) {
|
||||
this.$bvToast.toast(text, {
|
||||
autoHideDelay,
|
||||
title,
|
||||
toaster: 'b-toaster-bottom-center',
|
||||
variant,
|
||||
})
|
||||
},
|
||||
|
||||
// Main functionality: Fetch codes, handle errors including backoff
|
||||
fetchCodes(iteration=iterationCurrent) {
|
||||
if (this.fetchInProgress || !this.signedIn) {
|
||||
return
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// initializeApplication initializes some basic events and starts the first
|
||||
// polling for codes
|
||||
initializeApplication = function() {
|
||||
$('#keylist').empty();
|
||||
$('#filter').bind('keyup', filterChange);
|
||||
tick(500, refreshTimerProgress);
|
||||
return fetchCodes(iterationCurrent);
|
||||
};
|
||||
|
||||
// refreshTimerProgress updates the top progressbar to display the
|
||||
// remaining time until the one-time-passwords changes
|
||||
refreshTimerProgress = function() {
|
||||
var secondsLeft;
|
||||
secondsLeft = timeLeft();
|
||||
$('#timer').css('width', `${secondsLeft / 30 * 100}%`);
|
||||
if (secondsLeft < 10 && preFetch === null && !serverConnectionError) {
|
||||
// Do a pre-fetch to provide a seamless experience
|
||||
return fetchCodes(iterationNext);
|
||||
}
|
||||
};
|
||||
|
||||
// tick is a convenience wrapper to swap parameters of setInterval
|
||||
tick = function(delay, fkt) {
|
||||
return window.setInterval(fkt, delay);
|
||||
};
|
||||
|
||||
// timeLeft calculates the remaining time until codes get invalid
|
||||
timeLeft = function() {
|
||||
var now;
|
||||
now = new Date().getTime();
|
||||
return (currentTimeout - now) / 1000;
|
||||
};
|
||||
|
||||
// updateCodes is being called when the backend delivered codes. The codes
|
||||
// are then rendered and the clipboard methods are re-bound. Afterwards the
|
||||
// next fetchCodes call is timed to that moment when the codes are getting
|
||||
// invalid
|
||||
updateCodes = function(data) {
|
||||
var i, len, ref, token;
|
||||
currentTimeout = new Date(data.next_wrap).getTime();
|
||||
if (clipboard) {
|
||||
clipboard.destroy();
|
||||
}
|
||||
$('#initLoader').hide();
|
||||
$('#keylist').empty();
|
||||
ref = data.tokens;
|
||||
for (i = 0, len = ref.length; i < len; i++) {
|
||||
token = ref[i];
|
||||
createOTPItem(token);
|
||||
}
|
||||
clipboard = new Clipboard('.otp-item', {
|
||||
text: function(trigger) {
|
||||
return $(trigger).find('.badge').text().replace(/ /g, '');
|
||||
if (this.lastFetch && this.lastFetch.getTime() + this.backoff > new Date().getTime()) {
|
||||
// slow down spammy requests
|
||||
return
|
||||
}
|
||||
});
|
||||
clipboard.on('success', function(e) {
|
||||
createAlert('success', 'Success:', 'Code copied to clipboard', 1000);
|
||||
return e.blur();
|
||||
});
|
||||
clipboard.on('error', function(e) {
|
||||
return createAlert('danger', 'Oops.', 'Copy to clipboard failed', 2000);
|
||||
});
|
||||
filterChange();
|
||||
delay(timeLeft() * 1000, function() {
|
||||
return fetchCodes(iterationCurrent);
|
||||
});
|
||||
return fetchInProgress = false;
|
||||
};
|
||||
this.lastFetch = new Date()
|
||||
|
||||
updatePreFetch = function(data) {
|
||||
preFetch = data;
|
||||
return fetchInProgress = false;
|
||||
};
|
||||
this.fetchInProgress=true
|
||||
|
||||
let successFunc= iteration == iterationCurrent ? this.updateCodes : this.updatePreFetch
|
||||
if (iteration == iterationCurrent && this.preFetch !== null) {
|
||||
successFunc(this.preFetch)
|
||||
this.fetchInProgress = false
|
||||
return
|
||||
}
|
||||
|
||||
axios.get(`codes.json?it=${iteration}`)
|
||||
.then(resp => {
|
||||
successFunc(resp.data)
|
||||
this.backoff = 500 // Reset backoff to 500ms
|
||||
})
|
||||
.catch(err => {
|
||||
this.backoff = this.backoff * 1.5 > 30000 ? 30000 : this.backoff * 1.5
|
||||
|
||||
if (err.response && err.response.status) {
|
||||
switch (err.response.status) {
|
||||
case 401:
|
||||
this.createAlert('danger', 'Logged out...', 'Server has no valid token for you: You need to re-login.')
|
||||
this.signedIn = false
|
||||
this.otpItems = []
|
||||
break
|
||||
|
||||
case 500:
|
||||
this.createAlert('danger', 'Oops.', `Something went wrong when fetching your codes, will try again in ${Math.round(this.backoff / 1000)}s...`, this.backoff)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.error(err)
|
||||
this.createAlert('danger', 'Oops.', `The request went wrong, will try again in ${Math.round(this.backoff / 1000)}s...`, this.backoff)
|
||||
}
|
||||
|
||||
if (iteration === iterationCurrent) {
|
||||
this.otpItems = []
|
||||
this.loading = true
|
||||
}
|
||||
})
|
||||
.finally(() => { this.fetchInProgress=false })
|
||||
},
|
||||
|
||||
// Format code for better readability: 000 000 or 00 000 000
|
||||
formatCode(code) {
|
||||
return code.replace(/^([0-9]{3})([0-9]{3})$/, '$1 $2').replace(/^([0-9]{2})([0-9]{3})([0-9]{3})$/, '$1 $2 $3')
|
||||
},
|
||||
|
||||
// Update timer bar and trigger re-fetch of codes by time remaining
|
||||
refreshTimerProgress() {
|
||||
const secondsLeft = this.timeLeft()
|
||||
this.timeLeftPerc = secondsLeft / 30 * 100
|
||||
|
||||
if (secondsLeft < 3 && !this.preFetch && this.signedIn) {
|
||||
// Do a pre-fetch to provide a seamless experience
|
||||
this.fetchCodes(secondsLeft < 0 ? iterationCurrent : iterationNext)
|
||||
}
|
||||
},
|
||||
|
||||
// Calculate remaining time for the current batch of codes
|
||||
timeLeft() {
|
||||
if (!this.currentTimeout) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const now = new Date().getTime()
|
||||
return (this.currentTimeout.getTime() - now) / 1000
|
||||
},
|
||||
|
||||
// Update displayed codes
|
||||
updateCodes(data) {
|
||||
this.currentTimeout = new Date(data.next_wrap)
|
||||
this.otpItems = data.tokens
|
||||
this.loading = false
|
||||
this.preFetch = null
|
||||
|
||||
window.setTimeout(this.fetchCodes, this.timeLeft()*1000)
|
||||
},
|
||||
|
||||
// Store received data for later usage
|
||||
updatePreFetch(data) {
|
||||
this.preFetch = data
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
// Initialize application
|
||||
mounted() {
|
||||
window.setInterval(this.refreshTimerProgress, 500)
|
||||
this.fetchCodes(iterationCurrent)
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
}).call(this);
|
||||
|
|
211
index.html
211
index.html
|
@ -9,142 +9,135 @@
|
|||
|
||||
<link rel="manifest" href="static/manifest.json">
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css"
|
||||
integrity="sha256-LA89z+k9fjgMKQ/kq4OO2Mrf8VltYml/VES+Rg0fh20=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/4.0.0/flatly/bootstrap.min.css"
|
||||
integrity="sha256-uR+4waFpGD6COqOrSkm2HiJwc668qfgW3kidhhaOyYk=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css"
|
||||
integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@4.3.1/dist/flatly/bootstrap.min.css"
|
||||
integrity="sha256-0mhswpc4tUm8b+EHmWyk817AlGI+X5NmVsKbJkQ342c=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-vue@2.0.0/dist/bootstrap-vue.min.css"
|
||||
integrity="sha256-98fnCXYEILg6wOwaFWPVePJcizsYZG2U+N95WSWsG3g=" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
|
||||
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous">
|
||||
|
||||
<style>
|
||||
body { font-size: 16px; padding-top: 90px; }
|
||||
i { margin-right: 0.4em; }
|
||||
#initLoader i { margin-right: unset; }
|
||||
#templates { display: none; }
|
||||
.alert { background-image: none; }
|
||||
.initLoader i { margin-right: unset; }
|
||||
.alert { background-image: none; }
|
||||
.badge { background-color: #2980b9; color: #ddd; font-size: 15px; font-weight: bold; margin-top: 3px; }
|
||||
.center { text-align: center; }
|
||||
.fixed { bottom: 0; position: fixed; width: 100%; z-index: 999; }
|
||||
.jumbotron h2 { text-align: center; }
|
||||
.otp-item { cursor: pointer; }
|
||||
.otp-item i { width: 1.1em; }
|
||||
.pbar { background-color: #18BC9C; height: 100%; }
|
||||
.pcontainer { background-color: #E74C3C; border-width: 1px 0 1px 0; border-color: #333; height: 3px; position: absolute; bottom: 0; left: 0; width: 100%; z-index: 999; }
|
||||
.state-signedin #login { display: none; }
|
||||
.state-signedout #application { display: none; }
|
||||
</style>
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"
|
||||
integrity="sha256-3Jy/GbSLrg0o9y5Z5n1uw0qxZECH7C6OQpVBgNFYa0g=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"
|
||||
integrity="sha256-g6iAfvZp+nDQ2TdTR/VVKJf3bGro4ub5fvWSWVRi2NE=" crossorigin="anonymous"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body class="state-{{ if .isloggedin }}signedin{{ else }}signedout{{ end }}">
|
||||
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand mr-5" href="#">Vault OTP-UI</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<form class="form-inline mr-auto">
|
||||
<input type="text" class="form-control" placeholder="Filter" id="filter">
|
||||
</form>
|
||||
<ul class="navbar-nav my-2 my-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://github.com/Luzifer/vault-otp-ui"><i class="fa fa-github" aria-hidden="true"></i> Source on Github</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</div><!-- /.container-fluid -->
|
||||
|
||||
<div class="pcontainer">
|
||||
<div class="pbar" style="width: 0%;" id="timer"></div>
|
||||
</div>
|
||||
</nav>
|
||||
<body>
|
||||
|
||||
<div id="application">
|
||||
<div class="container-fluid fixed">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 col-lg-4" id="messagecontainer"></div>
|
||||
|
||||
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand mr-5" href="#">Vault OTP-UI</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<form class="form-inline mr-auto">
|
||||
<input type="text" class="form-control" placeholder="Filter" v-model="filter">
|
||||
</form>
|
||||
<ul class="navbar-nav my-2 my-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://github.com/Luzifer/vault-otp-ui"><i class="fa fa-github" aria-hidden="true"></i> Source on Github</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</div><!-- /.container-fluid -->
|
||||
|
||||
<div class="pcontainer">
|
||||
<div class="pbar" :style="{ width: `${timeLeftPerc}%` }" id="timer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-xs-10 col-md-4 center" id="initLoader">
|
||||
<i class="fa fa-refresh fa-spin fa-5x"></i><br>
|
||||
</div>
|
||||
<div class="w-100"></div>
|
||||
<div class="col-xs-12 col-sm-8 col-md-6 col-lg-6">
|
||||
<div class="list-group" id="keylist">
|
||||
<!-- FIXME: Remove this -->
|
||||
<div v-if="signedIn">
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-xs-10 col-md-4 center initLoader" v-if="loading">
|
||||
<i class="fa fa-refresh fa-spin fa-5x"></i><br>
|
||||
</div>
|
||||
<div class="w-100"></div>
|
||||
<div class="col-xs-12 col-sm-8 col-md-6 col-lg-6">
|
||||
<div class="list-group" id="keylist">
|
||||
|
||||
<a
|
||||
class="list-group-item d-flex justify-content-between align-items-center otp-item"
|
||||
v-for="item in filteredItems"
|
||||
:key="item.name"
|
||||
v-clipboard:copy="item.code"
|
||||
v-clipboard:success="() => codeCopyResult(true)"
|
||||
v-clipboard:error="() => codeCopyResult(false)"
|
||||
>
|
||||
<span>
|
||||
<i :class="`fa fa-fw fa-${item.icon}`"></i>
|
||||
<span class="title">{{ item.name }}</span>
|
||||
</span>
|
||||
<span class="badge">{{ formatCode(item.code) }}</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Please sign in!</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
Use Github authentication to sign into your Vault instance and get access to your one-time passwords:
|
||||
</p>
|
||||
<p class="center">
|
||||
<a :href="authurl" class="btn btn-primary"><i class="fa fa-github" aria-hidden="true"></i> Sign-in with Github</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /#login -->
|
||||
|
||||
</div> <!-- /#application -->
|
||||
|
||||
<div id="login">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"
|
||||
integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-vue@2.0.0/dist/bootstrap-vue.min.js"
|
||||
integrity="sha256-Hv63vpX6fRHvM0UYK/NJMbAZ81/6IHQfkkq5BSHYXP8="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue-clipboard2@0.3.1/dist/vue-clipboard.min.js"
|
||||
integrity="sha256-XvHL1mhvDUwfYL9UgYaEG0TBKZg3J9uScjUDG6oCS6k="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js"
|
||||
integrity="sha256-S1J4GVHHDMiirir9qsXWc8ZWw74PHHafpsHp5PXtjTs="
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Please sign in!</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
Use Github authentication to sign into your Vault instance and get access to your one-time passwords:
|
||||
</p>
|
||||
<p class="center">
|
||||
<a href="{{ .authurl }}" class="btn btn-primary"><i class="fa fa-github" aria-hidden="true"></i> Sign-in with Github</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /#login -->
|
||||
|
||||
<div id="templates">
|
||||
<div id="tpl-otp-item">
|
||||
<a class="list-group-item d-flex justify-content-between align-items-center otp-item">
|
||||
<span>
|
||||
<i class="fa fa-fw"></i>
|
||||
<span class="title">Some Site</span>
|
||||
</span>
|
||||
<span class="badge">145 369</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="tpl-message">
|
||||
<div class="row">
|
||||
<div class="alert alert-dismissible col-sm-12 col-xs-12 col-md-12 col-lg-12" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<strong class="keyword">Warning!</strong> <span class="message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
|
||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"
|
||||
integrity="sha256-5+02zu5UULQkO7w1GIr6vftCgMfFdZcAHeDtFnKZsBs=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"
|
||||
integrity="sha256-Daf8GuI2eLKHJlOWLRR/zRy9Clqcj4TUSumbxYH9kGI=" crossorigin="anonymous"></script>
|
||||
<!-- Application specific JavaScript -->
|
||||
<script src="vars.js"></script>
|
||||
<script src="application.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue