mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
generate qrcode on frontend, added manual key copy
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
"vue-gettext": "^2.1.12",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.6.10",
|
||||
"vuex": "^3.4.0"
|
||||
"vuex": "^3.4.0",
|
||||
"qrcode.vue": "~1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
|
||||
@@ -16,8 +16,8 @@ export default {
|
||||
startTFASetup () {
|
||||
return repository.post(`${resource}/tfa/setup/`)
|
||||
},
|
||||
getQRCodeForTFASetup () {
|
||||
return repository.get(`${resource}/tfa/setup/qr_code/`)
|
||||
getKeyForTFASetup () {
|
||||
return repository.get(`${resource}/tfa/setup/key/`)
|
||||
},
|
||||
finalizeTFASetup (pinCode) {
|
||||
const payload = { pin_code: pinCode }
|
||||
|
||||
@@ -5,10 +5,29 @@
|
||||
<span class="text-subtitle-1"><translate>Two factor authentication</translate></span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<template v-if="qrCode">
|
||||
<template v-if="qrURL">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<div v-html="qrCode"></div>
|
||||
<v-row rows="2">
|
||||
<qrcode-vue
|
||||
:value="qrURL"
|
||||
:size="200"
|
||||
render-as="svg"
|
||||
level="H"
|
||||
class= "qrcode"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-btn color="primary" @click="copyKey" class="key">
|
||||
Click here to copy the key
|
||||
<v-icon
|
||||
v-if="clicked"
|
||||
color="success"
|
||||
>
|
||||
mdi-check-all
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-alert type="info">
|
||||
@@ -95,10 +114,12 @@
|
||||
import account from '@/api/account'
|
||||
import Cookies from 'js-cookie'
|
||||
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecoveryCodesResetDialog
|
||||
RecoveryCodesResetDialog,
|
||||
QrcodeVue
|
||||
},
|
||||
props: {
|
||||
account: Object
|
||||
@@ -107,20 +128,27 @@ export default {
|
||||
return {
|
||||
newTokens: [],
|
||||
pinCode: null,
|
||||
qrCode: null,
|
||||
key: null,
|
||||
qrURL: null,
|
||||
clicked: false,
|
||||
showCodesResetDialog: false,
|
||||
tokens: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQRCode () {
|
||||
account.getQRCodeForTFASetup().then(resp => {
|
||||
this.qrCode = resp.data
|
||||
getKey () {
|
||||
account.getKeyForTFASetup().then(resp => {
|
||||
this.key = resp.data.key
|
||||
this.qrURL = resp.data.url
|
||||
})
|
||||
},
|
||||
copyKey () {
|
||||
navigator.clipboard.writeText(this.key)
|
||||
this.clicked = true
|
||||
},
|
||||
startTFASetup () {
|
||||
account.startTFASetup().then(resp => {
|
||||
this.getQRCode()
|
||||
this.getKey()
|
||||
})
|
||||
},
|
||||
async finalizeTFASetup () {
|
||||
@@ -129,7 +157,8 @@ export default {
|
||||
return
|
||||
}
|
||||
const resp = await account.finalizeTFASetup(this.pinCode)
|
||||
this.qrCode = null
|
||||
this.key = null
|
||||
this.qrURL = null
|
||||
this.tokens = resp.data.tokens
|
||||
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
|
||||
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
|
||||
@@ -153,8 +182,18 @@ export default {
|
||||
},
|
||||
created () {
|
||||
if (!this.account.tfa_enabled) {
|
||||
this.getQRCode()
|
||||
this.getKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.qrcode {
|
||||
margin-left:20px;
|
||||
}
|
||||
.key {
|
||||
margin-left:10px;
|
||||
margin-top:10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6350,6 +6350,11 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qrcode.vue@~1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qrcode.vue/-/qrcode.vue-1.7.0.tgz#c54b2934f0650c10d92785d08aaad36c55e0fc56"
|
||||
integrity sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==
|
||||
|
||||
qs@6.10.3:
|
||||
version "6.10.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
"""Core API v2 viewsets."""
|
||||
|
||||
import io
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import filters, permissions, response, viewsets
|
||||
@@ -102,19 +97,16 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
|
||||
"access": str(refresh.access_token)
|
||||
})
|
||||
|
||||
@action(methods=["get"], detail=False, url_path="tfa/setup/qr_code")
|
||||
def tfa_setup_get_qr_code(self, request):
|
||||
@action(methods=["get"], detail=False, url_path="tfa/setup/key")
|
||||
def tfa_setup_get_key(self, request):
|
||||
"""Get a QR code to finalize the setup process."""
|
||||
if request.user.tfa_enabled:
|
||||
return response.Response(status=404)
|
||||
device = request.user.totpdevice_set.first()
|
||||
if not device:
|
||||
return response.Response(status=404)
|
||||
factory = qrcode.image.svg.SvgPathImage
|
||||
img = qrcode.make(device.config_url, image_factory=factory)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf)
|
||||
return response.Response(buf.getvalue(), content_type="application/xml")
|
||||
return response.Response({"key": device.key, "url":device.config_url},
|
||||
content_type="application/json")
|
||||
|
||||
@extend_schema(
|
||||
request=core_v1_serializers.CheckTFASetupSerializer
|
||||
|
||||
Reference in New Issue
Block a user