mirror of
https://github.com/jumpserver/lina.git
synced 2025-07-23 03:24:02 +00:00
perf: 修改 passkey (#3382)
* perf: 修改 passkey * perf: 完成 passkey * perf: 修改 passkey icon --------- Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
parent
95831c3ff7
commit
ca3a99a5cf
@ -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": "解锁",
|
||||
|
1
src/icons/svg/passkey.svg
Normal file
1
src/icons/svg/passkey.svg
Normal 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 |
@ -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
80
src/utils/passkey.js
Normal 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
|
||||
}
|
||||
|
153
src/views/profile/PassKey.vue
Normal file
153
src/views/profile/PassKey.vue
Normal 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>
|
45
src/views/settings/Auth/Passkey.vue
Normal file
45
src/views/settings/Auth/Passkey.vue
Normal 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>
|
@ -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
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user