mirror of
https://github.com/Luzifer/vault-otp-ui.git
synced 2024-11-08 16:20:06 +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
|
const iterationCurrent = 'current'
|
||||||
(function() {
|
const iterationNext = 'next'
|
||||||
var clipboard, createAlert, createOTPItem, currentTimeout, delay, fetchCodes, fetchInProgress, filterChange, initializeApplication, iterationCurrent, iterationNext, preFetch, refreshTimerProgress, serverConnectionError, tick, timeLeft, updateCodes, updatePreFetch;
|
|
||||||
|
|
||||||
currentTimeout = 0;
|
const app = new Vue({
|
||||||
|
|
||||||
clipboard = null;
|
computed: {
|
||||||
|
filteredItems() {
|
||||||
|
if (this.filter === '') {
|
||||||
|
return this.otpItems
|
||||||
|
}
|
||||||
|
|
||||||
preFetch = null;
|
const items = []
|
||||||
|
|
||||||
fetchInProgress = false;
|
for (let i of this.otpItems) {
|
||||||
|
if (i.name.toLowerCase().match(this.filter.toLowerCase())) {
|
||||||
serverConnectionError = false;
|
items.push(i)
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// filterChange is called when changing the filter field and matches the
|
return items
|
||||||
// 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() {
|
data: {
|
||||||
var filter;
|
authUrl,
|
||||||
filter = $('#filter').val().toLowerCase();
|
backoff: 500,
|
||||||
return $('.otp-item').each(function(idx, el) {
|
currentTimeout: null,
|
||||||
if ($(el).find('.title').text().toLowerCase().match(filter) === null) {
|
fetchInProgress: false,
|
||||||
return $(el).hide();
|
filter: '',
|
||||||
} else {
|
lastFetch: null,
|
||||||
return $(el).show();
|
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
|
if (this.lastFetch && this.lastFetch.getTime() + this.backoff > new Date().getTime()) {
|
||||||
// polling for codes
|
// slow down spammy requests
|
||||||
initializeApplication = function() {
|
return
|
||||||
$('#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, '');
|
|
||||||
}
|
}
|
||||||
});
|
this.lastFetch = new Date()
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePreFetch = function(data) {
|
this.fetchInProgress=true
|
||||||
preFetch = data;
|
|
||||||
return fetchInProgress = false;
|
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">
|
<link rel="manifest" href="static/manifest.json">
|
||||||
|
|
||||||
<!-- Bootstrap -->
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css"
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css"
|
integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous">
|
||||||
integrity="sha256-LA89z+k9fjgMKQ/kq4OO2Mrf8VltYml/VES+Rg0fh20=" crossorigin="anonymous" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@4.3.1/dist/flatly/bootstrap.min.css"
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/4.0.0/flatly/bootstrap.min.css"
|
integrity="sha256-0mhswpc4tUm8b+EHmWyk817AlGI+X5NmVsKbJkQ342c=" crossorigin="anonymous">
|
||||||
integrity="sha256-uR+4waFpGD6COqOrSkm2HiJwc668qfgW3kidhhaOyYk=" crossorigin="anonymous" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-vue@2.0.0/dist/bootstrap-vue.min.css"
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
|
integrity="sha256-98fnCXYEILg6wOwaFWPVePJcizsYZG2U+N95WSWsG3g=" crossorigin="anonymous">
|
||||||
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" 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>
|
<style>
|
||||||
body { font-size: 16px; padding-top: 90px; }
|
body { font-size: 16px; padding-top: 90px; }
|
||||||
i { margin-right: 0.4em; }
|
i { margin-right: 0.4em; }
|
||||||
#initLoader i { margin-right: unset; }
|
.initLoader i { margin-right: unset; }
|
||||||
#templates { display: none; }
|
.alert { background-image: none; }
|
||||||
.alert { background-image: none; }
|
|
||||||
.badge { background-color: #2980b9; color: #ddd; font-size: 15px; font-weight: bold; margin-top: 3px; }
|
.badge { background-color: #2980b9; color: #ddd; font-size: 15px; font-weight: bold; margin-top: 3px; }
|
||||||
.center { text-align: center; }
|
.center { text-align: center; }
|
||||||
.fixed { bottom: 0; position: fixed; width: 100%; z-index: 999; }
|
.fixed { bottom: 0; position: fixed; width: 100%; z-index: 999; }
|
||||||
.jumbotron h2 { text-align: center; }
|
.jumbotron h2 { text-align: center; }
|
||||||
|
.otp-item { cursor: pointer; }
|
||||||
.otp-item i { width: 1.1em; }
|
.otp-item i { width: 1.1em; }
|
||||||
.pbar { background-color: #18BC9C; height: 100%; }
|
.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; }
|
.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>
|
</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>
|
</head>
|
||||||
<body class="state-{{ if .isloggedin }}signedin{{ else }}signedout{{ end }}">
|
<body>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div id="application">
|
<div id="application">
|
||||||
<div class="container-fluid fixed">
|
|
||||||
<div class="row justify-content-md-center">
|
<nav class="navbar fixed-top navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div class="col-xs-10 col-sm-10 col-md-8 col-lg-4" id="messagecontainer"></div>
|
<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>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="row justify-content-md-center">
|
<div v-if="signedIn">
|
||||||
<div class="col-xs-10 col-md-4 center" id="initLoader">
|
|
||||||
<i class="fa fa-refresh fa-spin fa-5x"></i><br>
|
<div class="container">
|
||||||
</div>
|
|
||||||
<div class="w-100"></div>
|
<div class="row justify-content-md-center">
|
||||||
<div class="col-xs-12 col-sm-8 col-md-6 col-lg-6">
|
<div class="col-xs-10 col-md-4 center initLoader" v-if="loading">
|
||||||
<div class="list-group" id="keylist">
|
<i class="fa fa-refresh fa-spin fa-5x"></i><br>
|
||||||
<!-- FIXME: Remove this -->
|
</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>
|
</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> <!-- /#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">
|
<script src="vars.js"></script>
|
||||||
<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="application.js"></script>
|
<script src="application.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue