Fix Always On OTPs & Dots display and looping

This commit is contained in:
Bubka 2023-11-17 19:49:39 +01:00
parent a75e3c13f7
commit e8a3c441be
3 changed files with 96 additions and 75 deletions

View File

@ -3,12 +3,12 @@ import { httpClientFactory } from '@/services/httpClientFactory'
const apiClient = httpClientFactory('api') const apiClient = httpClientFactory('api')
export default { export default {
getAll(withOtp = false) { getAll(withOtp = false, config = {}) {
return apiClient.get('/twofaccounts' + (withOtp ? '?withOtp=1' : '')) return apiClient.get('/twofaccounts' + (withOtp ? '?withOtp=1' : ''), { ...config })
}, },
getByIds(ids, withOtp = false) { getByIds(ids, withOtp = false, config = {}) {
return apiClient.get('/twofaccounts?ids=' + ids + (withOtp ? '&withOtp=1' : '')) return apiClient.get('/twofaccounts?ids=' + ids + (withOtp ? '&withOtp=1' : ''), { ...config })
}, },
get(id, config = {}) { get(id, config = {}) {

View File

@ -79,31 +79,33 @@ export const useTwofaccounts = defineStore({
/** /**
* Refreshes the accounts collection using the backend * Refreshes the accounts collection using the backend
*/ */
async fetch() { async fetch(force = false) {
// We do not want to fetch fresh data multiple times in the same 2s timespan // We do not want to fetch fresh data multiple times in the same 2s timespan
const age = Math.floor(Date.now() - this.fetchedOn) const age = Math.floor(Date.now() - this.fetchedOn)
const isNotFresh = age > 2000 const isOutOfAge = age > 2000
if (isNotFresh) { if (isOutOfAge || force) {
this.fetchedOn = Date.now() this.fetchedOn = Date.now()
await twofaccountService.getAll(! useUserStore().preferences.getOtpOnRequest).then(response => { await twofaccountService.getAll(! useUserStore().preferences.getOtpOnRequest).then(response => {
// Defines if the store was up-to-date with the backend // Defines if the store was up-to-date with the backend
this.backendWasNewer = response.data.length !== this.items.length if (force) {
this.backendWasNewer = response.data.length !== this.items.length
this.items.forEach((item) => {
let matchingBackendItem = response.data.find(e => e.id === item.id) this.items.forEach((item) => {
if (matchingBackendItem == undefined) { let matchingBackendItem = response.data.find(e => e.id === item.id)
this.backendWasNewer = true if (matchingBackendItem == undefined) {
return;
}
for (const field in item) {
if (field !== 'otp' && item[field] != matchingBackendItem[field]) {
this.backendWasNewer = true this.backendWasNewer = true
return; return;
} }
} for (const field in item) {
}) if (field !== 'otp' && item[field] != matchingBackendItem[field]) {
this.backendWasNewer = true
return;
}
}
})
}
// Updates the state // Updates the state
this.items = response.data this.items = response.data

View File

@ -32,9 +32,8 @@
const showGroupSwitch = ref(false) const showGroupSwitch = ref(false)
const showDestinationGroupSelector = ref(false) const showDestinationGroupSelector = ref(false)
const isDragging = ref(false) const isDragging = ref(false)
const stepIndexesCache = ref({})
const isRenewingOTPs = ref(false) const isRenewingOTPs = ref(false)
const renewedOTPs = ref(null) const renewedPeriod = ref(null)
const otpDisplay = ref(null) const otpDisplay = ref(null)
const otpDisplayProps = ref({ const otpDisplayProps = ref({
@ -85,12 +84,17 @@
// This SFC is reached only if the user has some twofaccounts (see the starter middleware). // This SFC is reached only if the user has some twofaccounts (see the starter middleware).
// This allows to display accounts without latency. // This allows to display accounts without latency.
// //
// We sync the store with the backend again to // We sync the store with the backend again to
twofaccounts.fetch().then(() => { if (! user.preferences.getOtpOnRequest) {
if (twofaccounts.backendWasNewer) { updateTotps()
notify.info({ text: trans('commons.data_refreshed_to_reflect_server_changes'), duration: 10000 }) }
} else {
}) twofaccounts.fetch().then(() => {
if (twofaccounts.backendWasNewer) {
notify.info({ text: trans('commons.data_refreshed_to_reflect_server_changes'), duration: 10000 })
}
})
}
groups.fetch() groups.fetch()
}) })
@ -207,59 +211,69 @@
twofaccounts.saveOrder() twofaccounts.saveOrder()
} }
/**
* Turns dots On at the current step and caches the state
*/
function setCurrentStep(period, stepIndex) {
stepIndexesCache.value[period] = stepIndex
turnDotsOn(period, stepIndex)
}
/**
* Turns dots On at the cached step index
*/
function turnDotsOnFromCache(period, stepIndex) {
if (stepIndexesCache.value[period] != undefined) {
turnDotsOn(period, stepIndexesCache.value[period])
}
}
/** /**
* Turns dots On for all dots components that match the provided period * Turns dots On for all dots components that match the provided period
*/ */
function turnDotsOn(period, stepIndex) { function turnDotsOn(period, stepIndex) {
dotsRefs.value dotsRefs.value
.filter((dots) => dots.props.period == period) .filter((dots) => dots.props.period == period || period == undefined)
.forEach((dot) => { .forEach((dot) => {
dot.turnOn(stepIndex) dot.turnOn(stepIndex)
}) })
} }
/** /**
* Updates "Always On" OTPs for all TOTP accounts with the given period and restarts loopers * Turns dots Off for all dots components that match the provided period
*/
function turnDotsOff(period) {
dotsRefs.value
.filter((dots) => dots.props.period == period || period == undefined)
.forEach((dot) => {
dot.turnOff()
})
}
/**
* Updates "Always On" OTPs for all TOTP accounts and (re)starts loopers
*/ */
async function updateTotps(period) { async function updateTotps(period) {
isRenewingOTPs.value = true isRenewingOTPs.value = true
renewedOTPs.value = period turnDotsOff(period)
let fetchPromise
twofaccountService.getByIds(twofaccounts.accountIdsWithPeriod(period).join(','), true).then(response => {
if (period == undefined) {
renewedPeriod.value = -1
fetchPromise = twofaccountService.getAll(true)
} else {
renewedPeriod.value = period
fetchPromise = twofaccountService.getByIds(twofaccounts.accountIdsWithPeriod(period).join(','), true)
}
fetchPromise.then(response => {
let generatedAt = 0
// twofaccounts OTP updates
response.data.forEach((account) => { response.data.forEach((account) => {
const index = twofaccounts.items.findIndex(acc => acc.id === account.id) const index = twofaccounts.items.findIndex(acc => acc.id === account.id)
twofaccounts.items[index].otp = account.otp if (twofaccounts.items[index] == undefined) {
twofaccounts.items.push(account)
looperRefs.value.forEach((looper) => { }
if (looper.props.period == period) { else twofaccounts.items[index].otp = account.otp
nextTick().then(() => { generatedAt = account.otp.generated_at
looper.startLoop(account.otp.generated_at) })
})
} // Loopers restart at new timestamp
}) looperRefs.value.forEach((looper) => {
if (looper.props.period == period || period == undefined) {
nextTick().then(() => {
looper.startLoop(generatedAt)
})
}
}) })
}) })
.finally(() => { .finally(() => {
isRenewingOTPs.value = false isRenewingOTPs.value = false
renewedOTPs.value = null renewedPeriod.value = null
}) })
} }
@ -339,6 +353,20 @@
@please-close-me="showOtpInModal = false"> @please-close-me="showOtpInModal = false">
</OtpDisplay> </OtpDisplay>
</Modal> </Modal>
<!-- totp loopers -->
<span v-if="!user.preferences.getOtpOnRequest">
<TotpLooper
v-for="period in twofaccounts.periods"
:key="period.period"
:autostart="false"
:period="period.period"
:generated_at="period.generated_at"
v-on:loop-ended="updateTotps(period.period)"
v-on:loop-started="turnDotsOn(period.period, $event)"
v-on:stepped-up="turnDotsOn(period.period, $event)"
ref="looperRefs"
></TotpLooper>
</span>
<!-- show accounts list --> <!-- show accounts list -->
<div class="container" v-if="showAccounts" :class="bus.inManagementMode ? 'is-edit-mode' : ''"> <div class="container" v-if="showAccounts" :class="bus.inManagementMode ? 'is-edit-mode' : ''">
<!-- accounts --> <!-- accounts -->
@ -366,13 +394,17 @@
<transition name="popLater"> <transition name="popLater">
<div v-show="user.preferences.getOtpOnRequest == false && !bus.inManagementMode" class="has-text-right"> <div v-show="user.preferences.getOtpOnRequest == false && !bus.inManagementMode" class="has-text-right">
<span v-if="account.otp != undefined"> <span v-if="account.otp != undefined">
<span v-if="isRenewingOTPs == true && renewedOTPs == account.period" class="has-nowrap has-text-grey has-text-centered is-size-5"> <span v-if="isRenewingOTPs == true && (renewedPeriod == -1 || renewedPeriod == account.period)" class="has-nowrap has-text-grey has-text-centered is-size-5">
<FontAwesomeIcon :icon="['fas', 'circle-notch']" spin /> <FontAwesomeIcon :icon="['fas', 'circle-notch']" spin />
</span> </span>
<span v-else class="always-on-otp is-clickable has-nowrap has-text-grey is-size-5 ml-4" @click="copyToClipboard(account.otp.password)" @keyup.enter="copyToClipboard(account.otp.password)" :title="$t('commons.copy_to_clipboard')"> <span v-else class="always-on-otp is-clickable has-nowrap has-text-grey is-size-5 ml-4" @click="copyToClipboard(account.otp.password)" @keyup.enter="copyToClipboard(account.otp.password)" :title="$t('commons.copy_to_clipboard')">
{{ useDisplayablePassword(account.otp.password) }} {{ useDisplayablePassword(account.otp.password) }}
</span> </span>
<Dots v-if="account.otp_type.includes('totp')" @hook:mounted="turnDotsOnFromCache(account.period)" :class="'condensed'" ref="dotsRefs" :period="account.period" /> <Dots
v-if="account.otp_type.includes('totp')"
:class="'condensed'"
ref="dotsRefs"
:period="account.period" />
</span> </span>
<span v-else> <span v-else>
<!-- get hotp button --> <!-- get hotp button -->
@ -415,18 +447,5 @@
</ActionButtons> </ActionButtons>
</VueFooter> </VueFooter>
</div> </div>
<!-- totp loopers -->
<span v-if="!user.preferences.getOtpOnRequest">
<TotpLooper
v-for="period in twofaccounts.periods"
:key="period.period"
:period="period.period"
:generated_at="period.generated_at"
v-on:loop-ended="updateTotps(period.period)"
v-on:loop-started="setCurrentStep(period.period, $event)"
v-on:stepped-up="setCurrentStep(period.period, $event)"
ref="looperRefs"
></TotpLooper>
</span>
</div> </div>
</template> </template>