[#339] Frontend implementation of the new 2FA logic.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Closes: #339
This commit is contained in:
parent
7351a2685a
commit
6b5dbe7c1e
11
README.md
11
README.md
|
@ -67,6 +67,7 @@
|
|||
* [Other Web panels](#other-web-panels)
|
||||
* [Dashboards](#dashboards)
|
||||
* [PWA support](#pwa-support)
|
||||
- [Two-factor authentication](#two-factor-authentication)
|
||||
- [Mobile app](#mobile-app)
|
||||
- [Browser extension](#browser-extension)
|
||||
- [Tests](#tests)
|
||||
|
@ -1247,6 +1248,16 @@ PWA (progressive web app) to work. The Platypush PWA allows you to install a
|
|||
Platypush native-like client on your mobile devices if you don't want to use the
|
||||
full Android app.
|
||||
|
||||
## Two-factor authentication
|
||||
|
||||
Support for 2FA over OTP codes requires to enable the
|
||||
[`otp`](https://docs.platypush.tech/platypush/plugins/otp.html) and
|
||||
[`qrcode`](https://docs.platypush.tech/platypush/plugins/qrcode.html) plugins.
|
||||
|
||||
After installing the dependencies, you can enable it by navigating to
|
||||
_Settings_ -> _Users_ from the Web panel. Then select your user, choose _Set up
|
||||
2FA_ and proceed with the steps on screen to set up your authenticator.
|
||||
|
||||
## Mobile app
|
||||
|
||||
An [official Android
|
||||
|
|
|
@ -89,6 +89,10 @@ export default {
|
|||
this.isVisible = true
|
||||
},
|
||||
|
||||
open() {
|
||||
this.show()
|
||||
},
|
||||
|
||||
toggle() {
|
||||
if (this.isVisible)
|
||||
this.close()
|
||||
|
|
|
@ -0,0 +1,468 @@
|
|||
<template>
|
||||
<div class="otp-config-container">
|
||||
<Loading v-if="initializing" />
|
||||
|
||||
<div class="otp-config" v-else>
|
||||
<div class="title">
|
||||
<h3>Two-Factor Authentication {{ otpEnabled ? 'Enabled' : 'Disabled'}}</h3>
|
||||
<ToggleSwitch :value="toggleOn"
|
||||
:disabled="refreshing"
|
||||
@input="currentOtpConfig?.otp_secret?.length ? startOtpDisable() : startOtpSetup()" />
|
||||
</div>
|
||||
|
||||
<p class="description">
|
||||
Two-factor authentication adds an extra layer of security to your
|
||||
account. When enabled, you will need to enter a code from your
|
||||
authenticator app in addition to your password.
|
||||
</p>
|
||||
|
||||
<div class="current-otp-config" v-if="currentOtpConfig?.otp_secret?.length">
|
||||
<div class="header">
|
||||
<h4>2FA Configuration</h4>
|
||||
<button class="btn btn-primary"
|
||||
:disabled="refreshing"
|
||||
@click="$refs.confirmModal.open"
|
||||
v-if="hasChanges && temporaryOtpEnabled">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<p>Scan the QR code with your authenticator app to add this account.</p>
|
||||
<p>Alternatively, you can add either the secret or the provisioning
|
||||
URL to your password manager or authenticator app.</p>
|
||||
</div>
|
||||
|
||||
<div class="section qrcode-container" v-if="currentOtpConfig.qrcode">
|
||||
<img class="qrcode" :src="`data:image/png;base64,${currentOtpConfig.qrcode}`" alt="QR Code" />
|
||||
</div>
|
||||
|
||||
<div class="section secret-container" v-if="currentOtpConfig.otp_secret">
|
||||
<h4>Secret</h4>
|
||||
<input type="text"
|
||||
:value="currentOtpConfig.otp_secret"
|
||||
readonly
|
||||
@focus="copyToClipboard($event.target.value)" />
|
||||
</div>
|
||||
|
||||
<div class="section uri-container" v-if="currentOtpConfig.otp_uri">
|
||||
<h4>Provisioning URL</h4>
|
||||
<input type="text"
|
||||
:value="currentOtpConfig.otp_uri"
|
||||
readonly
|
||||
@focus="copyToClipboard($event.target.value)" />
|
||||
</div>
|
||||
|
||||
<div class="section backup-codes" v-if="otpEnabled">
|
||||
<div class="header">
|
||||
<h4>Backup Codes</h4>
|
||||
<button class="btn btn-primary"
|
||||
:disabled="refreshing"
|
||||
@click="$refs.confirmRefreshCodes.open">
|
||||
<i class="fas fa-sync"></i> Regenerate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="description" v-if="backupCodes?.length">
|
||||
<p>
|
||||
Backup Codes are one-time use codes that can be used to access
|
||||
your account in case you lose access to your authenticator app.
|
||||
</p>
|
||||
<p>Make sure to store them in a safe place.</p>
|
||||
<p><b>
|
||||
Take note of these codes NOW! You will not be able to see them again!
|
||||
</b></p>
|
||||
</div>
|
||||
|
||||
<textarea :value="backupCodes.join('\n')"
|
||||
readonly
|
||||
@focus="copyToClipboard($event.target.value)"
|
||||
v-if="backupCodes?.length" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog ref="confirmRefreshCodes" @input="refreshCodes" v-if="!refreshing">
|
||||
Are you sure you want to regenerate the backup codes?
|
||||
</ConfirmDialog>
|
||||
|
||||
<Modal title="Confirm 2FA Setup" ref="confirmModal" @open="onConfirmModalOpen">
|
||||
<div class="confirm-modal">
|
||||
<div class="dialog" v-if="temporaryOtpEnabled">
|
||||
<p>Are you sure you want to enable Two-Factor Authentication?</p>
|
||||
<p>Make sure to save the secret and backup codes in a safe place.</p>
|
||||
<p>
|
||||
In order to enable Two-Factor Authentication, you will need to enter
|
||||
your password and a code from your authenticator app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dialog" v-else>
|
||||
<p>Are you sure you want to disable Two-Factor Authentication?</p>
|
||||
<p>
|
||||
You will no longer need to enter a code from your authenticator app.
|
||||
You will still need to enter your password to log in, but your
|
||||
account may be less secure.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In order to disable Two-Factor Authentication, you will need to enter
|
||||
your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form :disabled="refreshing" @submit.prevent="otpEnabled ? disableOtp() : enableOtp()">
|
||||
<input type="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
:disabled="refreshing"
|
||||
ref="password" />
|
||||
|
||||
<input type="text"
|
||||
placeholder="Authenticator Code"
|
||||
required
|
||||
:disabled="refreshing"
|
||||
ref="code"
|
||||
v-if="temporaryOtpEnabled" />
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn btn-primary"
|
||||
:disabled="refreshing"
|
||||
type="submit">
|
||||
<i class="fas fa-check"></i> Confirm
|
||||
<Loading v-if="refreshing" />
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default"
|
||||
@click="$refs.confirmModal.close">
|
||||
<i class="fas fa-times"></i> Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
||||
import Loading from "@/components/Loading";
|
||||
import Modal from "@/components/Modal";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import Utils from '@/Utils'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
mixins: [Utils],
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
Loading,
|
||||
Modal,
|
||||
ToggleSwitch,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backupCodes: [],
|
||||
initializing: false,
|
||||
otpConfig: null,
|
||||
refreshing: false,
|
||||
temporaryOtpConfig: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentOtpConfig() {
|
||||
return this.otpEnabled ? this.otpConfig : this.temporaryOtpConfig
|
||||
},
|
||||
|
||||
hasChanges() {
|
||||
return (
|
||||
(!this.otpEnabled && this.temporaryOtpConfig != null) ||
|
||||
(this.otpEnabled && (this.temporaryOtpConfig == null || this.temporaryOtpConfig?.otp_secret != this.otpConfig?.otp_secret))
|
||||
)
|
||||
},
|
||||
|
||||
otpEnabled() {
|
||||
return !!this?.otpConfig?.otp_secret?.length
|
||||
},
|
||||
|
||||
temporaryOtpDisabled() {
|
||||
return this.hasChanges && this.temporaryOtpConfig?.otp_secret == null
|
||||
},
|
||||
|
||||
temporaryOtpEnabled() {
|
||||
return this.hasChanges && this.temporaryOtpConfig?.otp_secret != null
|
||||
},
|
||||
|
||||
toggleOn() {
|
||||
return this.otpEnabled || this.temporaryOtpEnabled
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getErrorMessage(error) {
|
||||
return (
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.error ||
|
||||
error.message ||
|
||||
error.response?.statusText ||
|
||||
error.toString()
|
||||
)
|
||||
},
|
||||
|
||||
onError(error) {
|
||||
console.error(error)
|
||||
error = this.getErrorMessage(error)
|
||||
this.notify({
|
||||
error: true,
|
||||
title: "Error while setting up Two-Factor Authentication",
|
||||
text: error,
|
||||
image: {
|
||||
iconClass: "fas fa-exclamation-triangle",
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async getOtpConfig() {
|
||||
this.initializing = true
|
||||
|
||||
try {
|
||||
this.otpConfig = (await axios.get("/otp/config")).data
|
||||
this.temporaryOtpConfig = this.otpConfig
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
this.initializing = false
|
||||
}
|
||||
},
|
||||
|
||||
async startOtpSetup() {
|
||||
this.refreshing = true
|
||||
|
||||
try {
|
||||
this.temporaryOtpConfig = (await axios.post("/otp/config", { dry_run: true })).data
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
async enableOtp() {
|
||||
this.refreshing = true
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"/otp/config",
|
||||
{
|
||||
otp_secret: this.temporaryOtpConfig.otp_secret,
|
||||
password: this.$refs.password.value,
|
||||
code: this.$refs.code.value,
|
||||
}
|
||||
)
|
||||
|
||||
this.backupCodes = response.data?.backup_codes || []
|
||||
await this.getOtpConfig()
|
||||
|
||||
this.$refs.confirmModal.close()
|
||||
this.notify({
|
||||
title: "Two-Factor Authentication enabled",
|
||||
text: "Two-Factor Authentication has been enabled for your account",
|
||||
image: {
|
||||
iconClass: "fas fa-shield-alt",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
async startOtpDisable() {
|
||||
this.temporaryOtpConfig = null
|
||||
this.$refs.confirmModal.open()
|
||||
},
|
||||
|
||||
async disableOtp() {
|
||||
this.refreshing = true
|
||||
|
||||
try {
|
||||
await axios.delete("/otp/config", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data: {
|
||||
password: this.$refs.password.value
|
||||
}
|
||||
})
|
||||
|
||||
await this.getOtpConfig()
|
||||
|
||||
this.$refs.confirmModal.close()
|
||||
this.notify({
|
||||
title: "Two-Factor Authentication disabled",
|
||||
text: "Two-Factor Authentication has been disabled for your account",
|
||||
image: {
|
||||
iconClass: "fas fa-shield-alt",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
async refreshCodes() {
|
||||
this.refreshing = true
|
||||
|
||||
try {
|
||||
const response = await axios.post("/otp/refresh-codes")
|
||||
this.backupCodes = response.data?.backup_codes || []
|
||||
this.notify({
|
||||
title: "Backup codes regenerated",
|
||||
text: "Take note of these codes NOW! You will not be able to see them again!",
|
||||
image: {
|
||||
iconClass: "fas fa-shield-alt",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
onConfirmModalOpen() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password.value = ""
|
||||
if (this.$refs.code)
|
||||
this.$refs.code.value = ""
|
||||
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.getOtpConfig()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.otp-config-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.description {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.otp-config {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 30em;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.backup-codes {
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 16em;
|
||||
padding: .5em;
|
||||
border: $default-border-2;
|
||||
border-radius: 1em;
|
||||
box-shadow: $border-shadow-bottom-right;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-otp-config {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
h4 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.modal) {
|
||||
.confirm-modal {
|
||||
width: 100%;
|
||||
max-width: 40em;
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
input[type="password"],
|
||||
input[type="text"],
|
||||
.buttons {
|
||||
width: 100%;
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
margin: 0 0.5em;
|
||||
|
||||
&[type="submit"] {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -36,7 +36,13 @@
|
|||
<input type="submit" class="btn btn-primary" value="Change Password" :disabled="commandRunning">
|
||||
</label>
|
||||
</form>
|
||||
</modal>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Two-factor Authentication"
|
||||
:visible="showOtpModal"
|
||||
@close="showOtpModal = false">
|
||||
<Otp v-if="showOtpModal" />
|
||||
</Modal>
|
||||
|
||||
<div class="body">
|
||||
<ul class="users-list">
|
||||
|
@ -46,6 +52,8 @@
|
|||
<Dropdown title="User Actions" icon-class="fa fa-ellipsis">
|
||||
<DropdownItem text="Change Password" :disabled="commandRunning" icon-class="fa fa-key"
|
||||
@input="showChangePasswordModal(user)" />
|
||||
<DropdownItem text="Set Up 2FA" :disabled="commandRunning || !supports2fa" icon-class="fa fa-lock"
|
||||
:title="mfaTitle" @input="showOtpModal = true" />
|
||||
<DropdownItem text="Delete User" :disabled="commandRunning"
|
||||
icon-class="fa fa-trash" item-class="text-danger"
|
||||
@input="selectedUser = user.username; $refs.deleteUserDialog.show()" />
|
||||
|
@ -67,13 +75,22 @@ import ConfirmDialog from "@/components/elements/ConfirmDialog";
|
|||
import Dropdown from "@/components/elements/Dropdown";
|
||||
import Modal from "@/components/Modal";
|
||||
import Loading from "@/components/Loading";
|
||||
import Otp from "./Otp";
|
||||
import Utils from "@/Utils";
|
||||
import DropdownItem from "@/components/elements/DropdownItem";
|
||||
import FloatingButton from "@/components/elements/FloatingButton";
|
||||
|
||||
export default {
|
||||
name: "Users",
|
||||
components: {ConfirmDialog, Dropdown, DropdownItem, FloatingButton, Loading, Modal},
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
FloatingButton,
|
||||
Loading,
|
||||
Modal,
|
||||
Otp,
|
||||
},
|
||||
mixins: [Utils],
|
||||
|
||||
props: {
|
||||
|
@ -94,10 +111,65 @@ export default {
|
|||
commandRunning: false,
|
||||
loading: false,
|
||||
selectedUser: null,
|
||||
hasOtpPlugin: false,
|
||||
hasQrcodePlugin: false,
|
||||
showOtpModal: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
supports2fa() {
|
||||
return this.hasOtpPlugin && this.hasQrcodePlugin
|
||||
},
|
||||
|
||||
mfaTitle() {
|
||||
if (this.supports2fa)
|
||||
return ''
|
||||
|
||||
const missing = []
|
||||
if (!this.hasOtpPlugin)
|
||||
missing.push('otp')
|
||||
if (!this.hasQrcodePlugin)
|
||||
missing.push('qrcode')
|
||||
|
||||
return 'The following plugin(s) are missing: ' + missing.join(', ')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async testOtp() {
|
||||
this.commandRunning = true
|
||||
this.hasOtpPlugin = false
|
||||
this.hasQrcodePlugin = false
|
||||
|
||||
try {
|
||||
this.hasOtpPlugin = true
|
||||
|
||||
// Test the OTP plugin
|
||||
const otp = await this.request('otp.generate_secret', {}, 10000, false)
|
||||
|
||||
if (typeof otp === 'string' && otp.length) {
|
||||
// Test the QR code plugin
|
||||
const output = await this.request('qrcode.generate', {
|
||||
content: 'test',
|
||||
}, 10000, false)
|
||||
|
||||
if (output?.data?.length)
|
||||
this.hasQrcodePlugin = true
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.hasOtpPlugin) {
|
||||
console.info('otp plugin not found. Enable/configure it to use 2FA')
|
||||
}
|
||||
|
||||
if (!this.hasQrcodePlugin) {
|
||||
console.info('qrcode plugin not found. Enable/configure it to use 2FA')
|
||||
}
|
||||
} finally {
|
||||
this.commandRunning = false
|
||||
}
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
try {
|
||||
|
@ -243,8 +315,20 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
async mounted() {
|
||||
await this.refresh()
|
||||
await this.testOtp()
|
||||
|
||||
if (!this.supports2fa) {
|
||||
this.notify({
|
||||
title: 'Two-factor Authentication not available',
|
||||
text: this.mfaTitle,
|
||||
error: true,
|
||||
image: {
|
||||
iconClass: 'fas fa-exclamation-triangle',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -343,4 +427,11 @@ export default {
|
|||
border-radius: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.otp-config-container) {
|
||||
@include from($tablet) {
|
||||
max-width: 50em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,19 +12,39 @@
|
|||
|
||||
<div class="row">
|
||||
<label>
|
||||
<input type="text" name="username" :disabled="authenticating" placeholder="Username" ref="username">
|
||||
<input :type="requires2fa ? 'hidden' : 'text'"
|
||||
name="username"
|
||||
:disabled="authenticating"
|
||||
placeholder="Username"
|
||||
ref="username">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
<input type="password" name="password" :disabled="authenticating" placeholder="Password">
|
||||
<input :type="requires2fa ? 'hidden' : 'password'"
|
||||
name="password"
|
||||
:disabled="authenticating"
|
||||
placeholder="Password">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="requires2fa">
|
||||
<label>
|
||||
<input type="text"
|
||||
name="code"
|
||||
:disabled="authenticating"
|
||||
placeholder="2FA code"
|
||||
ref="code">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="register">
|
||||
<label>
|
||||
<input type="password" name="confirm_password" :disabled="authenticating" placeholder="Confirm password">
|
||||
<input type="password"
|
||||
name="confirm_password"
|
||||
:disabled="authenticating"
|
||||
placeholder="Confirm password">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -85,6 +105,7 @@ export default {
|
|||
authenticating: false,
|
||||
isAuthenticated: false,
|
||||
initialized: false,
|
||||
requires2fa: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -116,16 +137,22 @@ export default {
|
|||
this.authError = "Invalid credentials"
|
||||
}
|
||||
} catch (e) {
|
||||
this.authError = e.response.data.message || e.response.data.error
|
||||
|
||||
if (e.response?.status === 401) {
|
||||
this.authError = this.authError || "Invalid credentials"
|
||||
if (e.response?.data?.error === 'MISSING_OTP_CODE') {
|
||||
this.requires2fa = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.code?.focus()
|
||||
})
|
||||
} else {
|
||||
this.authError = this.authError || "An error occurred while processing the request"
|
||||
if (e.response)
|
||||
console.error(e.response.status, e.response.data)
|
||||
else
|
||||
console.error(e)
|
||||
this.authError = e.response.data.message || e.response.data.error
|
||||
if (e.response?.status === 401) {
|
||||
this.authError = this.authError || "Invalid credentials"
|
||||
} else {
|
||||
this.authError = this.authError || "An error occurred while processing the request"
|
||||
if (e.response)
|
||||
console.error(e.response.status, e.response.data)
|
||||
else
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue