diff --git a/src/i18n/langs/zh.json b/src/i18n/langs/zh.json
index da7a751ff..c2b655a2d 100644
--- a/src/i18n/langs/zh.json
+++ b/src/i18n/langs/zh.json
@@ -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": "解锁",
diff --git a/src/icons/svg/passkey.svg b/src/icons/svg/passkey.svg
new file mode 100644
index 000000000..9aac2b07d
--- /dev/null
+++ b/src/icons/svg/passkey.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/router/profile/index.js b/src/router/profile/index.js
index b6e8687ea..fe2d4033c 100644
--- a/src/router/profile/index.js
+++ b/src/router/profile/index.js
@@ -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',
diff --git a/src/utils/passkey.js b/src/utils/passkey.js
new file mode 100644
index 000000000..f0c095b5a
--- /dev/null
+++ b/src/utils/passkey.js
@@ -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
+}
+
diff --git a/src/views/profile/PassKey.vue b/src/views/profile/PassKey.vue
new file mode 100644
index 000000000..43097e12e
--- /dev/null
+++ b/src/views/profile/PassKey.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
diff --git a/src/views/settings/Auth/Passkey.vue b/src/views/settings/Auth/Passkey.vue
new file mode 100644
index 000000000..e9b3f11ad
--- /dev/null
+++ b/src/views/settings/Auth/Passkey.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/src/views/settings/Auth/index.vue b/src/views/settings/Auth/index.vue
index 0b8d533bb..564071d39 100644
--- a/src/views/settings/Auth/index.vue
+++ b/src/views/settings/Auth/index.vue
@@ -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
]
}