Просмотр исходного кода

Set up Always On OTPs on the main view

Bubka 1 год назад
Родитель
Сommit
4dbbae24dc

+ 6 - 1
resources/js_vue3/components/Dots.vue

@@ -8,6 +8,10 @@
             type: Number,
             default: null
         },
+        period: { // Used only to identify the dots component in Accounts.vue
+            type: Number,
+            default: null
+        },
     })
 
     const activeDot = ref(0)
@@ -37,7 +41,8 @@
 
     defineExpose({
         turnOn,
-        turnOff
+        turnOff,
+        props
     })
 
 </script>

+ 4 - 3
resources/js_vue3/components/TotpLooper.vue

@@ -58,9 +58,9 @@
     /**
      * Starts looping
      */
-    const startLoop = () => {
+    const startLoop = (generated_at = null) => {
         clearLooper()
-        generatedAt.value = props.generated_at
+        generatedAt.value = generated_at != null ? generated_at : props.generated_at
 
         emit('loop-started', initialStepIndex.value)
 
@@ -110,7 +110,8 @@
 
     defineExpose({
         startLoop,
-        clearLooper
+        clearLooper,
+        props
     })
 
 </script>

+ 4 - 0
resources/js_vue3/services/twofaccountService.js

@@ -7,6 +7,10 @@ export default {
         return apiClient.get('/twofaccounts' + (withOtp ? '?withOtp=1' : ''))
     },
 
+    getByIds(ids, withOtp = false) {
+        return apiClient.get('/twofaccounts?ids=' + ids + (withOtp ? '&withOtp=1' : ''))
+    },
+
     get(id, config = {}) {
         return apiClient.get('/twofaccounts/' + id, { ...config })
     },

+ 23 - 1
resources/js_vue3/stores/twofaccounts.js

@@ -34,6 +34,19 @@ export const useTwofaccounts = defineStore({
             )
         },
 
+        /**
+         * Lists unique periods used by twofaccounts in the collection
+         * ex: The items collection has 3 accounts with a period of 30s and 5 accounts with a period of 40s
+         *     => The method will return [30, 40]
+         */
+        periods(state) {
+            return state.items.filter(acc => acc.otp_type == 'totp').map(function(item) {
+                return { period: item.period, generated_at: item.otp?.generated_at }
+            }).filter((value, index, self) => index === self.findIndex((t) => (
+                t.period === value.period
+            ))).sort()
+        },
+
         orderedIds(state) {
             return state.items.map(a => a.id)
         },
@@ -141,11 +154,20 @@ export const useTwofaccounts = defineStore({
         },
 
         /**
-         *Sorts accounts descending
+         * Sorts accounts descending
         */
         sortDesc() {
             this.items.sort((a, b) => a.service < b.service ? 1 : -1)
             this.saveOrder()
         },
+        
+        /**
+         * Gets the IDs of all accounts that match the given period
+         * @param {*} period 
+         * @returns {Array<Number>} IDs of matching accounts
+         */
+        accountIdsWithPeriod(period) {
+            return this.items.filter(a => a.period == period).map(item => item.id)
+        },
     },
 })

+ 65 - 138
resources/js_vue3/views/Accounts.vue

@@ -1,6 +1,6 @@
 <script setup>
+
     import twofaccountService from '@/services/twofaccountService'
-    import groupService from '@/services/groupService'
     import Spinner from '@/components/Spinner.vue'
     import TotpLooper from '@/components/TotpLooper.vue'
     import GroupSwitch from '@/components/GroupSwitch.vue'
@@ -9,6 +9,7 @@
     import Toolbar from '@/components/Toolbar.vue'
     import OtpDisplay from '@/components/OtpDisplay.vue'
     import ActionButtons from '@/components/ActionButtons.vue'
+    import Dots from '@/components/Dots.vue'
     import { UseColorMode } from '@vueuse/components'
     import { useUserStore } from '@/stores/user'
     import { useNotifyStore } from '@/stores/notify'
@@ -33,8 +34,12 @@
     const showGroupSwitch = ref(false)
     const showDestinationGroupSelector = ref(false)
     const isDragging = ref(false)
+    const stepIndexesCache = ref({})
+    const isRenewingOTPs = ref(false)
 
     const otpDisplay = ref(null)
+    const looperRefs = ref([])
+    const dotsRefs = ref([])
 
     watch(showOtpInModal, (val) => {
         if (val == false) {
@@ -158,6 +163,60 @@
         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
+     */
+     function turnDotsOn(period, stepIndex) {
+        dotsRefs.value.forEach((dots) => {
+            if (dots.props.period == period) {
+                dots.turnOn(stepIndex)
+            }
+        })
+    }
+
+    /**
+     * Updates "Always On" OTPs for all TOTP accounts with the given period and restarts loopers
+     */
+    async function updateTotps(period) {
+        isRenewingOTPs.value = true
+        
+        twofaccountService.getByIds(twofaccounts.accountIdsWithPeriod(period).join(','), true).then(response => {
+            response.data.forEach((account) => {
+                const index = twofaccounts.items.findIndex(acc => acc.id === account.id)
+                twofaccounts.items[index].otp = account.otp
+                
+                looperRefs.value.forEach((looper) => {
+                    if (looper.props.period == period) {
+                        nextTick().then(() => {
+                            looper.startLoop(account.otp.generated_at)
+                        })
+                    }
+                })
+            })
+        })
+        .finally(() => {
+            isRenewingOTPs.value = false
+        })
+    }
+
 </script>
 
 <template>
@@ -260,7 +319,7 @@
                                             </button>
                                         </UseColorMode>
                                     </span>
-                                    <!-- <dots v-if="account.otp_type.includes('totp')" @hook:mounted="turnDotsOnFromCache(account.period)" :class="'condensed'" :ref="'dots_' + account.period"></dots> -->
+                                    <Dots v-if="account.otp_type.includes('totp')" @hook:mounted="turnDotsOnFromCache(account.period)" :class="'condensed'" ref="dotsRefs" :period="account.period" />
                                 </div>
                             </transition>
                             <transition name="fadeInOut">
@@ -295,153 +354,21 @@
             </VueFooter>
         </div>
         <!-- totp loopers -->
-        <!-- <span v-if="!user.preferences.getOtpOnRequest">
+        <span v-if="!user.preferences.getOtpOnRequest">
             <TotpLooper
-                v-for="period in periods"
+                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="loopers"
+                ref="looperRefs"
             ></TotpLooper>
-        </span> -->
+        </span>
     </div>
 </template>
 
-<script>
-
-    /**
-     *  Accounts view
-     *
-     *  route: '/account' (alias: '/')
-     *
-     *  The main view of 2FAuth that list all existing account recorded in DB.
-     *  Available feature in this view :
-     *  - {{OTP}} generation
-     *  - Account fetching :
-     *    ~ Search
-     *    ~ Filtering (by group)
-     *  - Accounts management :
-     *    ~ Sorting
-     *    ~ QR code recovering
-     *    ~ Mass association to group
-     *    ~ Mass account deletion
-     *    ~ Access to account editing
-     *
-     *  Behavior :
-     *  - The view has 2 modes (toggle is done with the 'manage' button) :
-     *    ~ The View mode (the default one)
-     *    ~ The Edit mode
-     *  - User are automatically pushed to the start view if there is no account to list.
-     *  - The view is affected by :
-     *    ~ 'userPreferences.showAccountsIcons' toggle the icon visibility
-     *    ~ 'userPreferences.displayMode' change the account appearance
-     *
-     *
-     */
-
-
-    // export default {
-    //     data(){
-    //         return {
-    //             stepIndexes: {},
-    //             isRenewingOTPs: false
-    //         }
-    //     },
-
-        // computed: {
-            /**
-             * Returns an array of all totp periods present in the twofaccounts list
-             */
-            // periods() {
-            //     return !user.preferences.getOtpOnRequest ?
-            //         this.accounts.filter(acc => acc.otp_type == 'totp').map(function(item) {
-            //             return {period: item.period, generated_at: item.otp.generated_at}
-            //             // return item.period
-            //         }).filter((value, index, self) => index === self.findIndex((t) => (
-            //             t.period === value.period
-            //         ))).sort()
-            //         : null
-            // },
-        // },
-
-        // props: ['toRefresh'],
-     
-        // methods: {
-            /**
-             * 
-             */
-            // setCurrentStep(period, stepIndex) {
-            //     this.stepIndexes[period] = stepIndex
-            //     this.turnDotsOn(period, stepIndex)
-            // },
-
-            /**
-             * 
-             */
-            // turnDotsOnFromCache(period, stepIndex) {
-            //     if (this.stepIndexes[period] != undefined) {
-            //         this.turnDotsOn(period, this.stepIndexes[period])
-            //     }
-            // },
-
-            /**
-             * 
-             */
-            // turnDotsOn(period, stepIndex) {
-            //     this.$refs['dots_' + period].forEach((dots) => {
-            //         dots.turnOn(stepIndex)
-            //     })
-            // },
-
-            /**
-             * Fetch all accounts set with the given period to get fresh OTPs
-             */
-            // async updateTotps(period) {
-            //     this.isRenewingOTPs = true
-            //     this.axios.get('api/v1/twofaccounts?withOtp=1&ids=' + this.accountIdsWithPeriod(period).join(',')).then(response => {
-            //         response.data.forEach((account) => {
-            //             const index = this.accounts.findIndex(acc => acc.id === account.id)
-            //             this.accounts[index].otp = account.otp
-                        
-            //             this.$refs.loopers.forEach((looper) => {
-            //                 if (looper.period == period) {
-            //                     looper.generatedAt = account.otp.generated_at
-            //                     this.$nextTick(() => {
-            //                         looper.startLoop()
-            //                     })
-            //                 }
-            //             })
-            //         })
-            //     })
-            //     .finally(() => {
-            //         this.isRenewingOTPs = false
-            //     })
-            // },
-
-            /**
-             * Return an array of all accounts (ids) set with the given period
-             */
-            // accountIdsWithPeriod(period) {
-            //     return this.accounts.filter(a => a.period == period).map(item => item.id)
-            // },
-
-            /**
-             * Get a fresh OTP for the provided account
-             */
-            // getOTP(accountId) {
-            //     this.axios.get('api/v1/twofaccounts/' + accountId + '/otp').then(response => {
-            //         this.$notify({ type: 'is-success', text: this.$t('commons.copied_to_clipboard')+ ' '+response.data })
-            //     })
-            // },
-
-    //     }
-    // };
-
-</script>
-
 <style scoped>
     .ghost {
         opacity: 1;