/
githubmirror
/
photoprism
Обзор
Документация
Войти
/
githubmirror
/
photoprism
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/src/component/settings/passcode.vue
383 строки
13 KB
Michael Mayer
Frontend: Introduce reusable context constants across components & tests
27 ноя 2025, 18:34
27 ноя 2025, 18:34
9231bfc
Код
Авторство
О чём код?
<template> <v-dialog ref="dialog" :model-value="visible" persistent max-width="500" class="p-dialog modal-dialog p-settings-passcode" @keydown.esc.exact="close" @after-enter="afterEnter" @after-leave="afterLeave" > <v-form ref="form" validate-on="invalid-input" accept-charset="UTF-8" class="form-password" tabindex="-1" @submit.prevent> <v-card> <v-card-title class="d-flex justify-start align-center ga-3"> <v-icon v-if="page === 'setup'" size="28" color="primary">mdi-shield-alert</v-icon> <v-icon v-else-if="page === 'deactivate'" size="28" color="primary">mdi-shield-check</v-icon> <v-icon v-else size="28" color="primary">mdi-cog</v-icon> <h6 class="text-h6">{{ $gettext(`2-Factor Authentication`) }}</h6> </v-card-title> <!-- Setup --> <template v-if="page === 'setup'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-subtitle-2"> {{ $gettext( `After entering your password for confirmation, you can set up two-factor authentication with a compatible authenticator app or device:` ) }} </v-col> <v-col cols="12"> <v-text-field v-model="password" :disabled="busy" :type="showPassword ? 'text' : 'password'" :placeholder="$gettext('Password')" autofocus name="password" hide-details autocorrect="off" autocapitalize="none" autocomplete="current-password" prepend-inner-icon="mdi-lock" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'" class="input-password text-selectable" @click:append-inner="showPassword = !showPassword" @keyup.enter="onSetup" ></v-text-field> </v-col> <v-col cols="12" class="text-body-2"> {{ $gettext( `Enabling two-factor authentication means that you will need a randomly generated verification code to log in, so even if someone gains access to your password, they will not be able to access your account.` ) }} </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-close" @click.stop="close"> {{ $gettext(`Close`) }} </v-btn> <v-btn variant="flat" color="highlight" class="action-setup" :disabled="setupDisabled()" @click.stop="onSetup"> {{ $gettext(`Setup`) }} </v-btn> </v-card-actions> </template> <!-- Confirm --> <template v-else-if="page === 'confirm'"> <v-card-text class="dense"> <v-row dense> <v-col cols="12" class="text-body-2 text-center"> {{ $gettext(`Scan the QR code with your authenticator app or use the setup key shown below and then enter the generated verification code:`) }} </v-col> <v-col cols="12"> <v-img :src="key.QRCode" :max-height="340" alt="QR Code" class="my-1"></v-img> </v-col> <v-col cols="12" class="text-body-2 text-center"> <span class="text-monospace cursor-copy" @click.stop.prevent="$util.copyText(key.Secret)">{{ key.Secret }}</span> </v-col> <v-col cols="12" class="text-body-2 text-center pb-0"> {{ $gettext(`Enter the code generated by your authenticator app:`) }} </v-col> <v-col cols="12"> <v-otp-input ref="otp" v-model="code" :disabled="busy" :length="6" name="one-time-code" type="number" :label="$gettext('Verification Code')" class="input-code" @keyup.enter="onConfirm" ></v-otp-input> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-cancel" @click.stop="close"> {{ $gettext(`Cancel`) }} </v-btn> <v-btn variant="flat" color="highlight" class="action-confirm" :disabled="code.length !== 6" @click.stop="onConfirm"> {{ $gettext(`Confirm`) }} </v-btn> </v-card-actions> </template> <!-- Activate --> <template v-else-if="page === 'activate'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-body-2"> {{ $gettext( `Use the following recovery code to access your account when you are unable to generate a valid verification code with your authenticator app:` ) }} </v-col> <v-col cols="12"> <v-text-field :model-value="key.RecoveryCode" type="text" mask="nnn nnn nnn nnn" hide-details readonly autocorrect="off" autocapitalize="none" autocomplete="off" append-inner-icon="mdi-content-copy" class="input-recoverycode cursor-copy text-monospace" @click.stop="onCopyRecoveryCode" ></v-text-field> </v-col> <v-col cols="12" class="text-subtitle-2"> {{ $gettext(`To avoid being locked out of your account, please download, print or copy this recovery code now and keep it in a safe place.`) }} {{ $gettext(`It is a one-time use code that will disable 2FA for your account when you use it.`) }} </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-cancel" @click.stop="close"> {{ $gettext(`Cancel`) }} </v-btn> <v-btn v-if="recoveryCodeCopied" variant="flat" color="highlight" class="action-activate" @click.stop="onActivate"> {{ $gettext(`Activate`) }} </v-btn> <v-btn v-else variant="flat" color="highlight" class="action-copy" @click.stop="onCopyRecoveryCode"> {{ $gettext(`Copy`) }} </v-btn> </v-card-actions> </template> <!-- Deactivate --> <template v-else-if="page === 'deactivate'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-subtitle-2"> {{ $gettext(`Two-factor authentication has been enabled for your account.`) }} </v-col> <v-col cols="12" class="text-body-2"> {{ $gettext(`If you lose access to your authenticator app or device, you can use your recovery code to regain access to your account.`) }} {{ $gettext(`It is a one-time use code that will disable 2FA for your account when you use it.`) }} </v-col> <v-col cols="12" class="text-body-2"> {{ $gettext(`To switch to a new authenticator app or device, first deactivate two-factor authentication and then reactivate it:`) }} </v-col> <v-col cols="12"> <v-text-field v-model="password" :disabled="busy" :type="showPassword ? 'text' : 'password'" :placeholder="$gettext('Password')" autofocus name="password" hide-details autocorrect="off" autocapitalize="none" autocomplete="current-password" class="input-password text-selectable" prepend-inner-icon="mdi-lock" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'" @click:append-inner="showPassword = !showPassword" @keyup.enter="onDeactivate" ></v-text-field> </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="highlight" class="action-deactivate" :disabled="setupDisabled()" @click.stop="onDeactivate"> {{ $gettext(`Deactivate`) }} </v-btn> <v-btn variant="flat" color="button" class="action-close" @click.stop="close"> {{ $gettext(`Close`) }} </v-btn> </v-card-actions> </template> <!-- Not Available --> <template v-else-if="page === 'not_available'"> <v-card-text class="dense"> <v-row align="start" dense> <v-col cols="12" class="text-body-2"> {{ $gettext(`Only locally managed accounts can be set up for authentication with 2FA.`) }} </v-col> </v-row> </v-card-text> <v-card-actions class="action-buttons"> <v-btn variant="flat" color="button" class="action-close" @click.stop="close"> {{ $gettext(`Close`) }} </v-btn> </v-card-actions> </template> </v-card> </v-form> </v-dialog> </template> <script> export default { name: "PSettingsPasscode", props: { visible: { type: Boolean, default: false, }, model: { type: Object, default: () => this.$session.getUser(), }, }, emits: ["updateUser", "close"], data() { return { busy: false, isDemo: this.$config.get("demo"), isPublic: this.$config.get("public"), code: "", recoveryCodeCopied: false, password: "", showPassword: false, session: this.$session, minLength: this.$config.get("passwordLength"), maxLength: 72, rtl: this.$isRtl, key: {}, }; }, computed: { page() { if (!this.session.hasPassword()) { return "not_available"; } else if (this.model?.AuthMethod === "2fa") { return "deactivate"; } else if (this.key?.Type === "totp") { if (!this.key?.VerifiedAt) { return "confirm"; } else if (!this.key?.ActivatedAt) { return "activate"; } else { return "deactivate"; } } return "setup"; }, }, watch: { visible: function (show) { if (show) { this.reset(); } }, }, created() { if (this.isPublic && !this.isDemo) { this.$emit("close"); } }, methods: { afterEnter() { this.$view.enter(this, this.$refs.form, ".input-password input"); }, afterLeave() { this.$view.leave(this); }, reset() { this.code = ""; this.password = ""; this.showPassword = false; this.recoveryCodeCopied = false; this.updateUser(); }, updateUser() { this.$emit("updateUser"); }, setupDisabled() { return this.isDemo || this.busy || this.password.length < this.minLength; }, onSetup() { if (this.busy || this.password === "") { return; } this.busy = true; this.model .createPasscode(this.password) .then((resp) => { this.key = resp; this.busy = false; this.$nextTick(() => { this.$view.focus(this.$refs?.otp); }); }) .catch(() => { this.busy = false; }); }, onConfirm() { if (this.busy || this.code === "") { return; } this.busy = true; this.model .confirmPasscode(this.code) .then((resp) => { this.key = resp; this.$notify.success(this.$gettext("Successfully verified")); }) .finally(() => { this.busy = false; this.code = ""; this.password = ""; this.showPassword = false; this.recoveryCodeCopied = false; }); }, onCopyRecoveryCode() { // Use the browser API to copy the recovery code to the clipboard. this.$util.copyText(this.key.RecoveryCode); // Flag the code as copied to the clipboard even // if the copyText() function returns an error. this.recoveryCodeCopied = true; }, onActivate() { if (this.busy) { return; } this.busy = true; this.model .activatePasscode() .then((resp) => { this.key = resp; this.$notify.success(this.$gettext("Successfully activated")); }) .finally(() => { this.busy = false; this.reset(); }); }, onDeactivate() { if (this.busy || this.password === "") { return; } this.busy = true; this.model .deactivatePasscode(this.password) .then(() => { this.$notify.success(this.$gettext("Settings saved")); this.reset(); this.key = {}; }) .finally(() => { this.busy = false; }); }, close() { if (this.busy) { return; } this.$emit("close"); }, }, }; </script>