feat: 个人中心 2FA的更新 绑定的添加
This commit is contained in:
@@ -79,7 +79,7 @@
|
|||||||
3. Enter the 6-digit code provided by your app and then verify.
|
3. Enter the 6-digit code provided by your app and then verify.
|
||||||
</div>
|
</div>
|
||||||
<div class="enter-code">Enter Code</div>
|
<div class="enter-code">Enter Code</div>
|
||||||
<el-input v-model="bindAuthCode" size="small" style="width: 50%"></el-input>
|
<el-input v-model="bindAuthCode" size="small" style="width: 50%" @keydown.enter="bindCode"></el-input>
|
||||||
</div>
|
</div>
|
||||||
<div slot="footer" class="footer">
|
<div slot="footer" class="footer">
|
||||||
<button id="asset-edit-cancel" @click="closeDialog" class="footer__btn footer__btn--light">
|
<button id="asset-edit-cancel" @click="closeDialog" class="footer__btn footer__btn--light">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="login-dialog-title2">
|
<div class="login-dialog-title2">
|
||||||
<div class="login-dialog-recover">
|
<div class="login-dialog-recover">
|
||||||
<div v-for="(item, index) in this.recoveryCode" :key="index" style="color: #999999;">
|
<div v-for="(item, index) in recoveryCode" :key="index" style="color: #999999;">
|
||||||
<span class="circle"></span>
|
<span class="circle"></span>
|
||||||
{{item}}
|
{{item}}
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +183,6 @@ export default {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.authBindShow = false
|
this.authBindShow = false
|
||||||
this.dialogLoading = false
|
|
||||||
this.$message.error(res.message)
|
this.$message.error(res.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<span>{{username.substr(0, 1)}}</span>
|
<span>{{username.substr(0, 1)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-left__header-username">
|
<div class="profile-left__header-username">
|
||||||
<div>{{userList.username}}<span class="profile-left__header-username__span">2FA</span></div>
|
<div>{{userList.username}}<span class="profile-left__header-username__span" v-show="mfaEnable == '1' || mfaLevel > 0">2FA</span></div>
|
||||||
<div>@{{userList.name}}</div>
|
<div>@{{userList.name}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
<!-- <el-button @click="profileMfaLevel" size="small" v-show="this.userList.mfaLevel == 0 && this.userList.mfaAuthEnable == 0 ? false : true" :class="{'table-operation-item--disable': this.userList.mfaLevel == 2 || this.userList.mfaAuthEnable ==1}" class="profile-left__button-footer__btn footer__btn table-operation-item">-->
|
<!-- <el-button @click="profileMfaLevel" size="small" v-show="this.userList.mfaLevel == 0 && this.userList.mfaAuthEnable == 0 ? false : true" :class="{'table-operation-item--disable': this.userList.mfaLevel == 2 || this.userList.mfaAuthEnable ==1}" class="profile-left__button-footer__btn footer__btn table-operation-item">-->
|
||||||
<!-- <span>{{$t('profile.update')}}</span>-->
|
<!-- <span>{{$t('profile.update')}}</span>-->
|
||||||
<!-- </el-button>-->
|
<!-- </el-button>-->
|
||||||
<el-button @click="profileEnable" size="small" class="profile-left__button-footer__btn footer__btns">
|
<el-button @click="profileEnable" size="small" class="profile-left__button-footer__btn footer__btn table-operation-item" v-if="mfaEnable != '1' && mfaLevel == 0">
|
||||||
<span>{{$t('profile.enable')}}</span>
|
<span>{{$t('profile.enable')}}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="profileDisable" size="small" class="profile-left__button-footer__btn footer__btns table-operation-item">
|
<el-button @click="profileDisable" size="small" class="profile-left__button-footer__btn footer__btns table-operation-item" v-if="mfaEnable == '1' || mfaLevel > 0" :disabled="mfaEnable == 1 || mfaLevel == 2 ">
|
||||||
<span>{{$t('profile.close')}}</span>
|
<span>{{$t('profile.close')}}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="profileMfaLevel" size="small" class="profile-left__button-footer__btn footer__btn table-operation-item">
|
<el-button @click="profileMfaLevel" size="small" class="profile-left__button-footer__btn footer__btn table-operation-item" v-if="mfaEnable == '1' || mfaLevel > 0" >
|
||||||
<span>{{$t('profile.update')}}</span>
|
<span>{{$t('profile.update')}}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,12 +57,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<operation-record />
|
<operation-record />
|
||||||
|
<el-dialog :visible.sync="authBindShow" :title="$t('login.verifyDialogTitle')" :modal-append-to-body='false'
|
||||||
|
:show-close="true" width="620px" class="nz-dialog" @closed="closeDialog">
|
||||||
|
|
||||||
|
<div v-loading="dialogLoading">
|
||||||
|
<div class="login-dialog-title">
|
||||||
|
1 Download your preferred authenticator app to your phone (any will work). If you don't
|
||||||
|
have a preferred app, we recommend using <span @click="jumpDlw" class="verify-link">Google Authenticator.</span>
|
||||||
|
</div>
|
||||||
|
<div class="login-dialog-title">
|
||||||
|
2 Use your app to take a photo of the QR code.
|
||||||
|
</div>
|
||||||
|
<div class="qrCode-box">
|
||||||
|
<div id="qrCode" ref="qrCodeDiv" class="qrCode-content"></div>
|
||||||
|
<div class="qrCode-text">
|
||||||
|
<div>Type this code down if you can't take a photo.</div>
|
||||||
|
<div class="qrCode-authKey">{{authKey}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-dialog-title">
|
||||||
|
3. Enter the 6-digit code provided by your app and then verify.
|
||||||
|
</div>
|
||||||
|
<div class="enter-code">Enter Code</div>
|
||||||
|
<el-input v-model="bindAuthCode" size="small" style="width: 50%" @keydown.enter="bindCode"></el-input>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="footer">
|
||||||
|
<button id="asset-edit-cancel" @click="closeDialog" class="footer__btn footer__btn--light">
|
||||||
|
<span>{{$t('overall.cancel')}}</span>
|
||||||
|
</button>
|
||||||
|
<button id="asset-edit-save" :class="{'footer__btn--disabled': prevent_opt.save}" :disabled="prevent_opt.save" class="footer__btn" @click="bindCode">
|
||||||
|
<span style="text-transform:capitalize">{{$t('login.verify')}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog :visible.sync="fileShow" :title="$t('login.verifyDialogTitle')" :modal-append-to-body='false'
|
||||||
|
:show-close="true" width="620px" class="nz-dialog" @closed="fileClosed">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="login-dialog-title">
|
||||||
|
Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account.
|
||||||
|
Please save them in a safe place, or you will lose access to your account.
|
||||||
|
</div>
|
||||||
|
<div class="login-dialog-title2">
|
||||||
|
<div class="login-dialog-recover">
|
||||||
|
<div v-for="(item, index) in recoveryCode" :key="index" style="color: #999999;">
|
||||||
|
<span class="circle"></span>
|
||||||
|
{{item}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="copy-value-content"> <i class="nz-icon nz-icon-override" @click="copyValue"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="footer">
|
||||||
|
<button id="asset-edit-download" :class="{'footer__btn--disabled': prevent_opt.save}" :disabled="prevent_opt.save" class="footer__btn" @click="downloadTxt">
|
||||||
|
<span style="text-transform:capitalize">{{$t('overall.download')}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import operationRecord from './operationRecord'
|
import operationRecord from './operationRecord'
|
||||||
import bus from '@/libs/bus.js'
|
import bus from '@/libs/bus.js'
|
||||||
|
import QRCode from "qrcodejs2";
|
||||||
|
import MessageBox from "element-ui/packages/message-box/src/main";
|
||||||
|
import i18n from "@/components/common/i18n";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
@@ -72,6 +133,15 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
username: sessionStorage.getItem('nz-username'),
|
username: sessionStorage.getItem('nz-username'),
|
||||||
|
mfaEnable: localStorage.getItem('nz-mfa-enable'),
|
||||||
|
authBindShow: false,
|
||||||
|
fileShow: false,
|
||||||
|
dialogLoading: false,
|
||||||
|
recoveryCode: '',
|
||||||
|
bindAuthCode: '',
|
||||||
|
mfaLevel: 0,
|
||||||
|
authKey: '',
|
||||||
|
authToken: '',
|
||||||
// 右侧列表
|
// 右侧列表
|
||||||
tableProfile: [
|
tableProfile: [
|
||||||
{
|
{
|
||||||
@@ -124,6 +194,7 @@ export default {
|
|||||||
this.$get('/sys/user/profile').then(response => {
|
this.$get('/sys/user/profile').then(response => {
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
this.userList = response.user
|
this.userList = response.user
|
||||||
|
this.mfaLevel = response.user.mfaLevel
|
||||||
this.tableProfile[0].prop = this.userList.roles[0].i18n
|
this.tableProfile[0].prop = this.userList.roles[0].i18n
|
||||||
this.tableProfile[1].prop = this.userList.email
|
this.tableProfile[1].prop = this.userList.email
|
||||||
this.tableProfile[2].prop = this.userList.mobile
|
this.tableProfile[2].prop = this.userList.mobile
|
||||||
@@ -135,38 +206,116 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
profileMfaLevel () {
|
profileMfaLevel () {
|
||||||
return new Promise(resolve => {
|
this.dialogLoading = true
|
||||||
this.$post('/mfa/genkey').then(response => {
|
this.authBindShow = true
|
||||||
if (response.code === 200) {
|
const params = {}
|
||||||
console.log(bus.$emit('profile-dialog'))
|
this.$post('/mfa/genkey', params).then(res => {
|
||||||
}
|
this.dialogLoading = false
|
||||||
resolve()
|
if (res.code === 200) {
|
||||||
})
|
this.$nextTick(() => {
|
||||||
|
this.authToken = res.data.authToken
|
||||||
|
this.authKey = res.data.authKey
|
||||||
|
this.bindQRCode(res.data.otpauth)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.authBindShow = false
|
||||||
|
this.$message.error(res.message)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
profileEnable () {
|
profileEnable () {
|
||||||
return new Promise(resolve => {
|
this.dialogLoading = true
|
||||||
const mfaLevelList = {
|
this.authBindShow = true
|
||||||
mfaLevel: 1
|
const params = {
|
||||||
|
mfaLevel: 1
|
||||||
|
}
|
||||||
|
this.$post('/mfa/genkey', params).then(res => {
|
||||||
|
this.dialogLoading = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.authToken = res.data.authToken
|
||||||
|
this.authKey = res.data.authKey
|
||||||
|
this.bindQRCode(res.data.otpauth)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.authBindShow = false
|
||||||
|
this.$message.error(res.message)
|
||||||
}
|
}
|
||||||
this.$post('/mfa/genkey', mfaLevelList).then(response => {
|
|
||||||
if (response.code === 200) {
|
|
||||||
bus.$emit('profile-dialog')
|
|
||||||
console.log(bus.$emit('profile-dialog'))
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
profileDisable () {
|
profileDisable () {
|
||||||
return new Promise(resolve => {
|
MessageBox.confirm(i18n.t('tip.resetMfa'), {
|
||||||
this.$get('/sys/user/disableMfa').then(response => {
|
confirmButtonText: i18n.t('tip.yes'),
|
||||||
if (response.code === 200) {
|
cancelButtonText: i18n.t('tip.no'),
|
||||||
console.log(response)
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.$delete('/sys/user/disableMfa').then(res => {
|
||||||
|
if (res.code == 200) {
|
||||||
|
this.personalCenter()
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg)
|
||||||
}
|
}
|
||||||
resolve()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
closeDialog () {
|
||||||
|
this.authBindShow = false
|
||||||
|
this.bindAuthCode = ''
|
||||||
|
},
|
||||||
|
jumpDlw () {
|
||||||
|
window.open('https://google-authenticator.en.softonic.com/android')
|
||||||
|
},
|
||||||
|
bindCode () {
|
||||||
|
this.prevent_opt.save = true
|
||||||
|
this.$post('/mfa/bind', { authToken: this.authToken, authCode: this.bindAuthCode }).then(res => {
|
||||||
|
this.prevent_opt.save = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.authBindShow = false
|
||||||
|
this.bindAuthCode = ''
|
||||||
|
this.recoveryCode = res.data.recoveryCode
|
||||||
|
this.fileContent = this.recoveryCode.join(' \n ')
|
||||||
|
this.fileShow = true
|
||||||
|
} else {
|
||||||
|
this.authToken = res.data.authToken
|
||||||
|
this.$message.error(this.$t('login.bindFail'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
bindQRCode: function (text) {
|
||||||
|
text = text || 'https://www.baidu.com'
|
||||||
|
if (!this.QRCode) {
|
||||||
|
this.QRCode = new QRCode(this.$refs.qrCodeDiv, {
|
||||||
|
text: text,
|
||||||
|
width: 124,
|
||||||
|
height: 124,
|
||||||
|
colorDark: '#333333', // 二维码颜色
|
||||||
|
colorLight: '#ffffff', // 二维码背景色
|
||||||
|
correctLevel: QRCode.CorrectLevel.L// 容错率,L/M/H
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.QRCode.makeCode(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadTxt () {
|
||||||
|
const element = document.createElement('a')
|
||||||
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(this.fileContent))
|
||||||
|
element.setAttribute('download', 'Nezha recovery codes')
|
||||||
|
element.style.display = 'none'
|
||||||
|
element.click()
|
||||||
|
},
|
||||||
|
copyValue () {
|
||||||
|
const domUrl = document.createElement('input')
|
||||||
|
domUrl.value = this.fileContent
|
||||||
|
domUrl.id = 'creatDom'
|
||||||
|
document.body.appendChild(domUrl)
|
||||||
|
domUrl.select() // 选择对象
|
||||||
|
document.execCommand('Copy') // 执行浏览器复制命令
|
||||||
|
const creatDom = document.getElementById('creatDom')
|
||||||
|
creatDom.parentNode.removeChild(creatDom)
|
||||||
|
this.$message.success(this.$t('overall.copySuccess'))
|
||||||
|
},
|
||||||
|
fileClosed () {
|
||||||
|
this.personalCenter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,4 +493,123 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.login-dialog-title{
|
||||||
|
background: #F9F9F9;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
.login-dialog-title2{
|
||||||
|
background: rgba(246,248,250, 0.9);
|
||||||
|
border: 1px solid #E7EAED;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 248px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
.copy-value-content{
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.login-dialog-recover{
|
||||||
|
height: calc(100% - 30px);overflow-y: auto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.enter-code{
|
||||||
|
ont-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.qrCode-box{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.qrCode-content{
|
||||||
|
padding: 4px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.qrCode-text{
|
||||||
|
width: 60%;
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.qrCode-authKey{
|
||||||
|
background: rgba(246,248,250,09);
|
||||||
|
border: 1px solid #E7EAED;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
/deep/ .nz-dialog .el-dialog__body{
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.verify-link{
|
||||||
|
font-family: Roboto-Black;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3C92F1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.verify-link:hover{
|
||||||
|
border-bottom: 1px solid #3C92F1;
|
||||||
|
}
|
||||||
|
.circle{
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #CECECE;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
.footer__btn {
|
||||||
|
margin: 0 15px;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 74px;
|
||||||
|
padding: 0 15px;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--theme-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color linear .2s, color linear .1s;
|
||||||
|
}
|
||||||
|
.footer__btn:hover:not(.footer__btn--disabled) {
|
||||||
|
background-color: var(--theme-color-light-20);
|
||||||
|
}
|
||||||
|
.footer__btn--light {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid $--primary-border-color;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.footer__btn.footer__btn--light:hover:not(.footer__btn--disabled) {
|
||||||
|
background-color: white;
|
||||||
|
border-color: var(--theme-color-light-50);
|
||||||
|
color: var(--theme-color);
|
||||||
|
}
|
||||||
|
.footer__btn--disabled {
|
||||||
|
opacity: .6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user