diff --git a/application.coffee b/application.coffee deleted file mode 100644 index 21ea401..0000000 --- a/application.coffee +++ /dev/null @@ -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 diff --git a/application.js b/application.js index c67a0a6..eaeae5a 100644 --- a/application.js +++ b/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); diff --git a/index.html b/index.html index f8076e3..fad844b 100644 --- a/index.html +++ b/index.html @@ -9,142 +9,135 @@ - - - - + + + + - - - -
- ++ Use Github authentication to sign into your Vault instance and get access to your one-time passwords: +
+ +- Use Github authentication to sign into your Vault instance and get access to your one-time passwords: -
- -