refactor(twofactor_backupcodes): migrate Vue to script-setup and Typescript

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-10-28 18:26:50 +01:00
parent 95f2d5dbac
commit 11f4fa92cf
No known key found for this signature in database
GPG key ID: 45FAE7268762B400
2 changed files with 93 additions and 123 deletions

View file

@ -8,3 +8,4 @@ import { getLoggerBuilder } from '@nextcloud/logger'
export const logger = getLoggerBuilder()
.detectLogLevel()
.setApp('twofactor_backupcodes')
.build()

View file

@ -2,12 +2,71 @@
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { getCapabilities } from '@nextcloud/capabilities'
import { showError } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { confirmPassword } from '@nextcloud/password-confirmation'
import { computed, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import { logger } from '../service/logger.ts'
import { print } from '../service/PrintService.js'
import { useStore } from '../store/index.ts'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instanceName = (getCapabilities() as any).theming.name ?? 'Nextcloud'
const store = useStore()
const generatingCodes = ref(false)
const hasCodes = computed(() => {
return store.codes && store.codes.length > 0
})
const downloadFilename = instanceName + '-backup-codes.txt'
const downloadUrl = computed(() => {
if (!hasCodes.value) {
return ''
}
return 'data:text/plain,' + encodeURIComponent(store.codes.reduce((prev, code) => {
return prev + code + '\n'
}, ''))
})
/**
* Generate new backup codes
*/
async function generateBackupCodes() {
await confirmPassword()
// Hide old codes
generatingCodes.value = true
try {
await store.generate()
} catch (error) {
logger.error('Error generating backup codes', { error })
showError(t('twofactor_backupcodes', 'An error occurred while generating your backup codes'))
} finally {
generatingCodes.value = false
}
}
/**
* Print the backup codes
*/
function printCodes() {
print(!store.codes || store.codes.length === 0 ? [] : store.codes)
}
</script>
<template>
<div class="backupcodes-settings">
<div :class="$style.backupcodesSettings">
<NcButton
v-if="!enabled"
id="generate-backup-codes"
v-if="!store.enabled"
:disabled="generatingCodes"
variant="primary"
@click="generateBackupCodes">
<template #icon>
<NcLoadingIcon v-if="generatingCodes" />
@ -15,39 +74,40 @@
{{ t('twofactor_backupcodes', 'Generate backup codes') }}
</NcButton>
<template v-else>
<p class="backupcodes-settings__codes">
<template v-if="!haveCodes">
{{ t('twofactor_backupcodes', 'Backup codes have been generated. {used} of {total} codes have been used.', { used, total }) }}
<p>
<template v-if="!hasCodes">
{{ t('twofactor_backupcodes', 'Backup codes have been generated. {used} of {total} codes have been used.', { used: store.used, total: store.total }) }}
</template>
<template v-else>
{{ t('twofactor_backupcodes', 'These are your backup codes. Please save and/or print them as you will not be able to read the codes again later.') }}
<ul>
<ul :aria-label="t('twofactor_backupcodes', 'List of backup codes')">
<li
v-for="code in codes"
v-for="code in store.codes"
:key="code"
class="backupcodes-settings__codes__code">
:class="$style.backupcodesSettings__code">
{{ code }}
</li>
</ul>
</template>
</p>
<p class="backupcodes-settings__actions">
<template v-if="haveCodes">
<p :class="$style.backupcodesSettings__actions">
<NcButton
id="generate-backup-codes"
variant="error"
@click="generateBackupCodes">
{{ t('twofactor_backupcodes', 'Regenerate backup codes') }}
</NcButton>
<template v-if="hasCodes">
<NcButton @click="printCodes">
{{ t('twofactor_backupcodes', 'Print backup codes') }}
</NcButton>
<NcButton
:href="downloadUrl"
:download="downloadFilename"
variant="primary">
{{ t('twofactor_backupcodes', 'Save backup codes') }}
</NcButton>
<NcButton @click="printCodes">
{{ t('twofactor_backupcodes', 'Print backup codes') }}
</NcButton>
</template>
<NcButton
id="generate-backup-codes"
@click="generateBackupCodes">
{{ t('twofactor_backupcodes', 'Regenerate backup codes') }}
</NcButton>
</p>
<p>
<em>
@ -58,112 +118,21 @@
</div>
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import { confirmPassword } from '@nextcloud/password-confirmation'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import { logger } from '../service/logger.ts'
import { print } from '../service/PrintService.js'
import { useStore } from '../store/index.ts'
export default {
name: 'PersonalSettings',
components: {
NcButton,
NcLoadingIcon,
},
setup() {
return {
store: useStore(),
}
},
data() {
return {
generatingCodes: false,
}
},
computed: {
downloadUrl() {
if (!this.codes) {
return ''
}
return 'data:text/plain,' + encodeURIComponent(this.codes.reduce((prev, code) => {
return prev + code + '\r\n'
}, ''))
},
downloadFilename() {
const name = OC.theme.name || 'Nextcloud'
return name + '-backup-codes.txt'
},
enabled() {
return this.store.enabled
},
total() {
return this.store.total
},
used() {
return this.store.used
},
codes() {
return this.store.codes
},
name() {
return OC.theme.name || 'Nextcloud'
},
haveCodes() {
return this.codes && this.codes.length > 0
},
},
methods: {
async generateBackupCodes() {
await confirmPassword()
// Hide old codes
this.generatingCodes = true
try {
await this.store.generate()
} catch (error) {
logger.error('Error generating backup codes', { error })
showError(t('twofactor_backupcodes', 'An error occurred while generating your backup codes'))
} finally {
this.generatingCodes = false
}
},
printCodes() {
print(!this.codes || this.codes.length === 0 ? [] : this.codes)
},
},
<style module>
.backupcodesSettings {
display: flex;
flex-direction: column;
}
</script>
<style lang="scss" scoped>
.backupcodes-settings {
&__codes {
&__code {
font-family: monospace;
letter-spacing: 0.02em;
font-size: 1.2em;
}
}
.backupcodesSettings__code {
font-family: monospace;
letter-spacing: 0.02em;
font-size: 1.2em;
}
&__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--default-grid-baseline);
}
.backupcodesSettings__actions {
display: flex;
flex-wrap: wrap;
gap: var(--default-grid-baseline);
}
</style>