perf: 修改 passkey (#3382)

* perf: 修改 passkey

* perf: 完成 passkey

* perf: 修改 passkey icon

---------

Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
fit2bot 2023-09-11 18:34:44 +08:00 committed by GitHub
parent 95831c3ff7
commit ca3a99a5cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 301 additions and 1 deletions

View File

@ -465,6 +465,7 @@
"AfterChange": "变更后"
},
"auth": {
"AddPassKey": "添加 Passkey(通行密钥)",
"LoginRequiredMsg": "账号已退出,请重新登录",
"ReLogin": "重新登录",
"ReLoginTitle": "当前三方登录用户CAS/SAML未绑定 MFA 且不支持密码校验,请重新登录。",
@ -777,6 +778,7 @@
"nav": {
"TempPassword": "临时密码",
"ConnectionToken": "连接令牌",
"PassKey": "Passkey",
"APIKey": "API Key",
"Workbench": "工作台",
"Navigation": "导航",
@ -1525,6 +1527,7 @@
"PublishStatus": "发布状态"
},
"setting": {
"Passkey": "Passkey",
"BlockedIPS": "已锁定的 IP",
"ViewBlockedIPSHelpText": "查看已被锁定的 IP 列表",
"Unblock": "解锁",

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694426459089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8414" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M510.952965 0L78.178596 164.73347v286.887525a505.36878 505.36878 0 0 0 209.406953 408.343559l223.367416 164.035446 225.461486-164.035446a505.36878 505.36878 0 0 0 209.406953-408.343559V164.73347z m360.877983 451.620995a432.774369 432.774369 0 0 1-177.995911 349.011589L510.952965 934.653033l-182.184049-133.322426A431.378323 431.378323 0 0 1 147.980913 451.620995v-237.32788l360.877983-139.604635 360.877982 139.604635z" p-id="8415"></path><path d="M510.952965 258.966599a144.490798 144.490798 0 0 0-34.901159 284.095432V732.924335a32.109066 32.109066 0 0 0 34.901159 34.901159 32.109066 32.109066 0 0 0 36.297205-36.297205V677.082481h36.297206a32.109066 32.109066 0 0 0 36.297205-36.297205 32.109066 32.109066 0 0 0-36.297205-35.599182h-36.297206v-57.2379-4.886163a144.490798 144.490798 0 0 0-36.297205-284.095432z m69.802318 144.490797a69.802318 69.802318 0 1 1-69.802318-69.802318 69.802318 69.802318 0 0 1 72.594411 69.802318z" p-id="8416"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -76,6 +76,17 @@ export default {
permissions: ['authentication.view_connectiontoken']
}
},
{
path: '/profile/passkeys',
component: () => import('@/views/profile/PassKey.vue'),
name: 'Passkey',
meta: {
title: i18n.t('common.nav.PassKey'),
icon: 'passkey',
hidden: ({ settings }) => !settings['AUTH_PASSKEY'],
permissions: ['authentication.view_connectiontoken']
}
},
{
path: '/profile/user/setting',
name: 'UserSetting',

80
src/utils/passkey.js Normal file
View File

@ -0,0 +1,80 @@
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
// Use a lookup table to find the index.
const lookup = new Uint8Array(256)
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i
}
const encode = function(arraybuffer) {
const bytes = new Uint8Array(arraybuffer)
let i; const len = bytes.length; let base64url = ''
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2]
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]
base64url += chars[bytes[i + 2] & 63]
}
if ((len % 3) === 2) {
base64url = base64url.substring(0, base64url.length - 1)
} else if (len % 3 === 1) {
base64url = base64url.substring(0, base64url.length - 2)
}
return base64url
}
const decode = function(base64string) {
const bufferLength = base64string.length * 0.75
const len = base64string.length; let i; let p = 0
let encoded1; let encoded2; let encoded3; let encoded4
const bytes = new Uint8Array(bufferLength)
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64string.charCodeAt(i)]
encoded2 = lookup[base64string.charCodeAt(i + 1)]
encoded3 = lookup[base64string.charCodeAt(i + 2)]
encoded4 = lookup[base64string.charCodeAt(i + 3)]
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
}
return bytes.buffer
}
const publicKeyCredentialToJSON = (pubKeyCred) => {
if (pubKeyCred instanceof Array) {
const arr = []
for (const i of pubKeyCred) { arr.push(publicKeyCredentialToJSON(i)) }
return arr
}
if (pubKeyCred instanceof ArrayBuffer) {
return encode(pubKeyCred)
}
if (pubKeyCred instanceof Object) {
const obj = {}
for (const key in pubKeyCred) {
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
}
return obj
}
return pubKeyCred
}
export default {
'decode': decode,
'encode': encode,
'publicKeyCredentialToJSON': publicKeyCredentialToJSON
}

View File

@ -0,0 +1,153 @@
<template>
<div>
<GenericListPage
ref="GenericListTable"
:header-actions="headerActions"
:table-config="tableConfig"
/>
<Dialog
:show-buttons="false"
:title="$tc('auth.AddPassKey')"
:visible.sync="dialogVisible"
>
<AutoDataForm v-bind="form" @submit="onAddConfirm" />
</Dialog>
</div>
</template>
<script>
import { GenericListPage } from '@/layout/components'
import { AutoDataForm, Dialog } from '@/components'
import passkey from '@/utils/passkey'
export default {
components: {
GenericListPage,
Dialog,
AutoDataForm
},
data() {
const ajaxUrl = '/api/v1/authentication/passkeys/'
return {
dialogVisible: false,
form: {
url: '',
fields: [
{
id: 'name',
label: this.$t('common.Name'),
type: 'input',
required: true,
el: {
placeholder: 'Laptop, Phone, etc.'
}
}
],
hasSaveContinue: false,
hasReset: false
},
tableConfig: {
hasSelection: true,
url: ajaxUrl,
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'is_active', 'date_last_used', 'date_created', 'actions']
},
columnsMeta: {
actions: {
formatterArgs: {
hasUpdate: false,
hasClone: false,
onDelete: function({ row }) {
this.$axios.delete(`${ajaxUrl}${row.id}/`).then(res => {
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this),
extraActions: [
{
name: 'Enabled',
title: ({ row }) => {
return row.is_active ? this.$t('common.Disable') : this.$t('common.Enable')
},
type: 'info',
can: () => this.$hasPerm('authentication.change_passkey'),
callback: function({ row }) {
this.$axios.patch(`${ajaxUrl}${row.id}/`,
{ is_active: !row.is_active }
).then(res => {
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + error))
})
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasSearch: true,
hasRightActions: true,
hasRefresh: true,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasCreate: false,
extraActions: [
{
name: this.$t('setting.Create'),
title: this.$t('setting.Create'),
type: 'primary',
can: () => this.$hasPerm('authentication.add_passkey'),
callback: function() {
this.dialogVisible = true
}.bind(this)
}
]
}
}
},
computed: {
getRefsListTable() {
return this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable || {}
}
},
methods: {
onAddConfirm(form) {
const url = '/api/v1/authentication/passkeys/register/?name=' + form.name
this.$axios.get(url).then(res => {
return this.makeCredReq(res)
}).then((options) => {
return navigator.credentials.create(options)
}).then((attestation) => {
attestation['key_name'] = form.name
const data = passkey.publicKeyCredentialToJSON(attestation)
return this.$axios.post('/api/v1/authentication/passkeys/register/', data)
}).then((res) => {
this.dialogVisible = false
this.getRefsListTable.reloadTable()
this.$message.success(this.$tc('common.createSuccessMsg'))
}).catch((error) => {
console.log('Error = ', error)
})
},
makeCredReq(makeCredReq) {
makeCredReq.publicKey.challenge = passkey.decode(makeCredReq.publicKey.challenge)
makeCredReq.publicKey.user.id = passkey.decode(makeCredReq.publicKey.user.id)
for (const excludeCred of makeCredReq.publicKey.excludeCredentials) {
excludeCred.id = passkey.decode(excludeCred.id)
}
return makeCredReq
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,45 @@
<template>
<BaseAuth
:config="settings"
enable-field="AUTH_PASSKEY"
v-on="$listeners"
/>
</template>
<script>
import BaseAuth from './Base'
export default {
name: 'Passkey',
components: {
BaseAuth
},
data() {
return {
settings: {
url: '/api/v1/settings/setting/?category=passkey',
hasDetailInMsg: false,
fields: [
[
this.$t('common.BasicInfo'),
[
'AUTH_PASSKEY', 'FIDO_SERVER_ID', 'FIDO_SERVER_NAME'
]
]
],
fieldsMeta: {
},
submitMethod() {
return 'patch'
}
}
}
},
methods: {
}
}
</script>
<style scoped>
</style>

View File

@ -20,6 +20,7 @@ import WeCom from './WeCom'
import SSO from './SSO'
import SAML2 from './SAML2'
import OAuth2 from './OAuth2'
import Passkey from './Passkey.vue'
export default {
components: {
@ -35,7 +36,8 @@ export default {
Radius,
SSO,
SAML2,
OAuth2
OAuth2,
Passkey
},
data() {
let extraBackends = []
@ -96,6 +98,11 @@ export default {
name: 'CAS',
key: 'AUTH_CAS'
},
{
title: this.$t('setting.Passkey'),
name: 'Passkey',
key: 'AUTH_PASSKEY'
},
...extraBackends
]
}