mirror of
https://github.com/Luzifer/vault-otp-ui.git
synced 2024-11-08 16:20:06 +00:00
207 lines
5.7 KiB
JavaScript
207 lines
5.7 KiB
JavaScript
const iterationCurrent = 'current'
|
|
const iterationNext = 'next'
|
|
|
|
const app = new Vue({
|
|
|
|
computed: {
|
|
filteredItems() {
|
|
if (this.filter === '') {
|
|
return this.otpItems
|
|
}
|
|
|
|
const items = []
|
|
|
|
for (let i of this.otpItems) {
|
|
if (i.name.toLowerCase().match(this.filter.toLowerCase())) {
|
|
items.push(i)
|
|
}
|
|
}
|
|
|
|
return items
|
|
},
|
|
|
|
minPeriod() {
|
|
let min = 99999
|
|
|
|
for (let i of this.otpItems) {
|
|
if (i.period > 0 && i.period < min) {
|
|
min = i.period
|
|
}
|
|
}
|
|
|
|
if (min === 99999) {
|
|
min = 30
|
|
}
|
|
|
|
return min
|
|
},
|
|
},
|
|
|
|
data: {
|
|
authUrl,
|
|
backoff: 500,
|
|
currentTimeout: null,
|
|
fetchInProgress: false,
|
|
filter: '',
|
|
inactivityTimeout: null,
|
|
lastFetch: null,
|
|
loading: true,
|
|
preFetch: null,
|
|
signedIn,
|
|
otpItems: [],
|
|
refreshTimerProgressTicker: null,
|
|
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
|
|
}
|
|
|
|
if (this.lastFetch && this.lastFetch.getTime() + this.backoff > new Date().getTime()) {
|
|
// slow down spammy requests
|
|
return
|
|
}
|
|
this.lastFetch = new Date()
|
|
|
|
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') // 6 digits
|
|
.replace(/^([0-9]{2})([0-9]{3})([0-9]{2})$/, '$1 $2 $3') // 7 digits
|
|
.replace(/^([0-9]{2})([0-9]{3})([0-9]{3})$/, '$1 $2 $3') // 8 digits
|
|
},
|
|
|
|
// Update timer bar and trigger re-fetch of codes by time remaining
|
|
refreshTimerProgress() {
|
|
const secondsLeft = this.timeLeft()
|
|
this.timeLeftPerc = secondsLeft / this.minPeriod * 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
|
|
},
|
|
|
|
windowBlur(evt) {
|
|
// Prepare shutdown of background refresh as long as page has no focus
|
|
this.inactivityTimeout = window.setTimeout(() => {
|
|
window.clearInterval(this.refreshTimerProgressTicker)
|
|
this.refreshTimerProgressTicker = null
|
|
this.inactivityTimeout = null
|
|
}, 30000)
|
|
},
|
|
|
|
windowFocus(evt) {
|
|
if (this.inactivityTimeout) {
|
|
// Page is in focus again, do not suspend
|
|
window.clearTimeout(this.inactivityTimeout)
|
|
this.inactivityTimeout = null
|
|
}
|
|
|
|
if (!this.refreshTimerProgressTicker) {
|
|
// In case the refresh ticker was stopped, revive it
|
|
this.refreshTimerProgressTicker = window.setInterval(this.refreshTimerProgress, 500)
|
|
}
|
|
},
|
|
|
|
},
|
|
|
|
// Initialize application
|
|
mounted() {
|
|
this.refreshTimerProgressTicker = window.setInterval(this.refreshTimerProgress, 500)
|
|
this.fetchCodes(iterationCurrent)
|
|
|
|
window.addEventListener('blur', (evt) => this.windowBlur(evt))
|
|
window.addEventListener('focus', (evt) => this.windowFocus(evt))
|
|
},
|
|
|
|
})
|
|
|