refactor: remove passkey login
This commit is contained in:
@@ -15,16 +15,6 @@ export async function login(payload) {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function passkeyLoginOptions(payload) {
|
||||
const { data } = await publicApi.post('/passkeys/login/options', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function passkeyLoginVerify(payload) {
|
||||
const { data } = await publicApi.post('/passkeys/login/verify', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function register(payload) {
|
||||
const { data } = await publicApi.post('/register', payload)
|
||||
return data
|
||||
|
||||
@@ -45,31 +45,6 @@ export async function fetchKdocsStatus() {
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchUserPasskeys() {
|
||||
const { data } = await publicApi.get('/user/passkeys')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createUserPasskeyOptions(payload) {
|
||||
const { data } = await publicApi.post('/user/passkeys/register/options', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createUserPasskeyVerify(payload) {
|
||||
const { data } = await publicApi.post('/user/passkeys/register/verify', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function deleteUserPasskey(passkeyId) {
|
||||
const { data } = await publicApi.delete(`/user/passkeys/${passkeyId}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function reportUserPasskeyClientError(payload) {
|
||||
const { data } = await publicApi.post('/user/passkeys/client-error', payload || {})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchSocialBindings() {
|
||||
const { data } = await publicApi.get('/user/social-bindings')
|
||||
return data
|
||||
|
||||
@@ -13,15 +13,10 @@ import {
|
||||
bindSocial,
|
||||
bindEmail,
|
||||
changePassword,
|
||||
createUserPasskeyOptions,
|
||||
createUserPasskeyVerify,
|
||||
deleteUserPasskey,
|
||||
fetchEmailNotify,
|
||||
fetchUserPasskeys,
|
||||
fetchUserEmail,
|
||||
fetchKdocsSettings,
|
||||
fetchSocialBindings,
|
||||
reportUserPasskeyClientError,
|
||||
unbindSocial,
|
||||
unbindEmail,
|
||||
updateKdocsSettings,
|
||||
@@ -29,7 +24,6 @@ import {
|
||||
} from '../api/settings'
|
||||
import SocialLoginButtons from '../components/SocialLoginButtons.vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { createPasskey, getPasskeyClientErrorMessage, isPasskeyAvailable } from '../utils/passkey'
|
||||
import { validateStrongPassword } from '../utils/password'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -129,13 +123,6 @@ const passwordForm = reactive({
|
||||
const kdocsLoading = ref(false)
|
||||
const kdocsSaving = ref(false)
|
||||
const kdocsUnitValue = ref('')
|
||||
const passkeyLoading = ref(false)
|
||||
const passkeyAddLoading = ref(false)
|
||||
const passkeyDeviceName = ref('')
|
||||
const passkeyItems = ref([])
|
||||
const passkeyRegisterOptions = ref(null)
|
||||
const passkeyRegisterOptionsAt = ref(0)
|
||||
const PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS = 240000
|
||||
|
||||
const socialConfig = ref({ enabled: false, providers: [] })
|
||||
const socialBindings = ref([])
|
||||
@@ -273,7 +260,7 @@ async function openSettings() {
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings(), loadPasskeys(), loadSocialBindings()])
|
||||
await Promise.all([loadEmailInfo(), loadEmailNotify(), loadKdocsSettings(), loadSocialBindings()])
|
||||
}
|
||||
|
||||
function socialBindRedirectUri() {
|
||||
@@ -397,113 +384,6 @@ async function saveKdocsSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPasskeys() {
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
const data = await fetchUserPasskeys()
|
||||
passkeyItems.value = Array.isArray(data?.items) ? data.items : []
|
||||
if (passkeyItems.value.length < 3) {
|
||||
await prefetchPasskeyRegisterOptions()
|
||||
} else {
|
||||
passkeyRegisterOptions.value = null
|
||||
passkeyRegisterOptionsAt.value = 0
|
||||
}
|
||||
} catch {
|
||||
passkeyItems.value = []
|
||||
passkeyRegisterOptions.value = null
|
||||
passkeyRegisterOptionsAt.value = 0
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCachedPasskeyRegisterOptions() {
|
||||
if (!passkeyRegisterOptions.value) return null
|
||||
if (Date.now() - Number(passkeyRegisterOptionsAt.value || 0) > PASSKEY_OPTIONS_PREFETCH_MAX_AGE_MS) return null
|
||||
return passkeyRegisterOptions.value
|
||||
}
|
||||
|
||||
async function prefetchPasskeyRegisterOptions() {
|
||||
try {
|
||||
const res = await createUserPasskeyOptions({})
|
||||
passkeyRegisterOptions.value = res
|
||||
passkeyRegisterOptionsAt.value = Date.now()
|
||||
} catch {
|
||||
passkeyRegisterOptions.value = null
|
||||
passkeyRegisterOptionsAt.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddPasskey() {
|
||||
if (!isPasskeyAvailable()) {
|
||||
ElMessage.error('当前浏览器或环境不支持Passkey(需 HTTPS)')
|
||||
return
|
||||
}
|
||||
if (passkeyItems.value.length >= 3) {
|
||||
ElMessage.error('最多可绑定3台设备')
|
||||
return
|
||||
}
|
||||
|
||||
passkeyAddLoading.value = true
|
||||
try {
|
||||
let optionsRes = getCachedPasskeyRegisterOptions()
|
||||
if (!optionsRes) {
|
||||
optionsRes = await createUserPasskeyOptions({})
|
||||
}
|
||||
const credential = await createPasskey(optionsRes?.publicKey || {})
|
||||
await createUserPasskeyVerify({ credential, device_name: passkeyDeviceName.value.trim() })
|
||||
passkeyRegisterOptions.value = null
|
||||
passkeyRegisterOptionsAt.value = 0
|
||||
passkeyDeviceName.value = ''
|
||||
ElMessage.success('Passkey设备添加成功')
|
||||
await loadPasskeys()
|
||||
} catch (e) {
|
||||
try {
|
||||
await reportUserPasskeyClientError({
|
||||
stage: 'register',
|
||||
source: 'user-settings',
|
||||
name: e?.name || '',
|
||||
message: e?.message || '',
|
||||
code: e?.code || '',
|
||||
user_agent: navigator.userAgent || '',
|
||||
})
|
||||
} catch {
|
||||
// ignore report failure
|
||||
}
|
||||
passkeyRegisterOptions.value = null
|
||||
passkeyRegisterOptionsAt.value = 0
|
||||
await prefetchPasskeyRegisterOptions()
|
||||
const data = e?.response?.data
|
||||
const message =
|
||||
data?.error ||
|
||||
getPasskeyClientErrorMessage(e, 'Passkey注册')
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
passkeyAddLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeletePasskey(item) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除设备「${item?.device_name || '未命名设备'}」吗?`, '删除Passkey设备', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteUserPasskey(item.id)
|
||||
ElMessage.success('设备已删除')
|
||||
await loadPasskeys()
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
ElMessage.error(data?.error || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function onBindEmail() {
|
||||
const email = bindEmailValue.value.trim().toLowerCase()
|
||||
if (!email) {
|
||||
@@ -877,47 +757,6 @@ async function dismissAnnouncementPermanently() {
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Passkey设备" name="passkeys">
|
||||
<div class="settings-section" v-loading="passkeyLoading">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="最多可绑定3台设备,用于无密码登录。"
|
||||
show-icon
|
||||
class="settings-alert"
|
||||
/>
|
||||
|
||||
<el-form inline>
|
||||
<el-form-item label="设备备注">
|
||||
<el-input
|
||||
v-model="passkeyDeviceName"
|
||||
placeholder="例如:我的iPhone / 办公Mac"
|
||||
maxlength="40"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="passkeyAddLoading" @click="onAddPasskey">
|
||||
添加Passkey设备
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-empty v-if="passkeyItems.length === 0" description="暂无Passkey设备" />
|
||||
<el-table v-else :data="passkeyItems" size="small" style="width: 100%">
|
||||
<el-table-column prop="device_name" label="设备备注" min-width="160" />
|
||||
<el-table-column prop="credential_id_preview" label="凭据ID" min-width="180" />
|
||||
<el-table-column prop="last_used_at" label="最近使用" min-width="140" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="140" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" text @click="onDeletePasskey(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="快捷登录" name="social">
|
||||
<div class="settings-section" v-loading="socialBindingsLoading || socialBindLoading">
|
||||
<el-empty v-if="!socialConfig.enabled" description="暂未启用快捷登录" />
|
||||
|
||||
@@ -13,7 +13,6 @@ const needCaptcha = ref(false)
|
||||
const captchaImage = ref('')
|
||||
const captchaSession = ref('')
|
||||
const loading = ref(false)
|
||||
const passkeyLoading = ref(false)
|
||||
|
||||
const emailEnabled = ref(false)
|
||||
const registerVerifyEnabled = ref(false)
|
||||
@@ -111,90 +110,11 @@ async function apiRequest(path, options = {}) {
|
||||
const fetchEmailVerifyStatus = () => apiRequest('/email/verify-status')
|
||||
const generateCaptcha = () => apiRequest('/generate_captcha', { method: 'POST', body: {} })
|
||||
const loginRequest = (payload) => apiRequest('/login', { method: 'POST', body: payload || {} })
|
||||
const passkeyLoginOptions = (payload) => apiRequest('/passkeys/login/options', { method: 'POST', body: payload || {} })
|
||||
const passkeyLoginVerify = (payload) => apiRequest('/passkeys/login/verify', { method: 'POST', body: payload || {} })
|
||||
const resendVerifyEmail = (payload) => apiRequest('/resend-verify-email', { method: 'POST', body: payload || {} })
|
||||
const forgotPassword = (payload) => apiRequest('/forgot-password', { method: 'POST', body: payload || {} })
|
||||
const fetchSocialConfig = () => apiRequest('/auth/social/config')
|
||||
const socialCallbackRequest = (payload) => apiRequest('/auth/social/callback', { method: 'POST', body: payload || {} })
|
||||
|
||||
function base64UrlToUint8Array(base64url) {
|
||||
const value = String(base64url || '')
|
||||
const padding = '='.repeat((4 - (value.length % 4)) % 4)
|
||||
const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const raw = window.atob(base64)
|
||||
const bytes = new Uint8Array(raw.length)
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
bytes[i] = raw.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function uint8ArrayToBase64Url(input) {
|
||||
const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input || [])
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return window
|
||||
.btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '')
|
||||
}
|
||||
|
||||
function normalizePublicKeyOptions(options) {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new Error('Passkey参数无效')
|
||||
}
|
||||
return options.publicKey && typeof options.publicKey === 'object' ? options.publicKey : options
|
||||
}
|
||||
|
||||
function toRequestOptions(rawOptions) {
|
||||
const options = normalizePublicKeyOptions(rawOptions)
|
||||
const normalized = {
|
||||
...options,
|
||||
challenge: base64UrlToUint8Array(options.challenge),
|
||||
}
|
||||
if (Array.isArray(options.allowCredentials)) {
|
||||
normalized.allowCredentials = options.allowCredentials.map((item) => ({
|
||||
...item,
|
||||
id: base64UrlToUint8Array(item.id),
|
||||
}))
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function serializeCredential(credential) {
|
||||
const response = credential?.response || {}
|
||||
const output = {
|
||||
id: credential?.id,
|
||||
rawId: uint8ArrayToBase64Url(credential?.rawId),
|
||||
type: credential?.type,
|
||||
authenticatorAttachment: credential?.authenticatorAttachment || undefined,
|
||||
response: {},
|
||||
}
|
||||
if (response.clientDataJSON) output.response.clientDataJSON = uint8ArrayToBase64Url(response.clientDataJSON)
|
||||
if (response.authenticatorData) output.response.authenticatorData = uint8ArrayToBase64Url(response.authenticatorData)
|
||||
if (response.signature) output.response.signature = uint8ArrayToBase64Url(response.signature)
|
||||
if (response.userHandle) {
|
||||
output.response.userHandle = uint8ArrayToBase64Url(response.userHandle)
|
||||
} else {
|
||||
output.response.userHandle = null
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function isPasskeyAvailable() {
|
||||
return typeof window !== 'undefined' && window.isSecureContext && !!window.PublicKeyCredential && !!navigator.credentials
|
||||
}
|
||||
|
||||
async function authenticateWithPasskey(rawOptions) {
|
||||
const publicKey = toRequestOptions(rawOptions)
|
||||
const credential = await navigator.credentials.get({ publicKey })
|
||||
return serializeCredential(credential)
|
||||
}
|
||||
|
||||
async function loadVerifyStatus() {
|
||||
if (verifyStatusLoaded.value) return
|
||||
try {
|
||||
@@ -370,33 +290,6 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onPasskeyLogin() {
|
||||
clearNotice()
|
||||
|
||||
const username = form.username.trim()
|
||||
if (!isPasskeyAvailable()) {
|
||||
setNotice('error', '当前浏览器或环境不支持Passkey(需 HTTPS)')
|
||||
return
|
||||
}
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
const optionsRes = await passkeyLoginOptions(username ? { username } : {})
|
||||
const credential = await authenticateWithPasskey(optionsRes?.publicKey || {})
|
||||
await passkeyLoginVerify(username ? { username, credential } : { credential })
|
||||
setNotice('success', 'Passkey 登录成功,正在跳转...')
|
||||
redirectAfterLogin()
|
||||
} catch (e) {
|
||||
const data = e?.response?.data
|
||||
const message =
|
||||
data?.error ||
|
||||
(e?.name === 'NotAllowedError' ? 'Passkey验证未完成(可能取消、超时或设备未响应)' : e?.message || 'Passkey登录失败')
|
||||
setNotice('error', message)
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openForgot() {
|
||||
await loadVerifyStatus()
|
||||
forgotOpen.value = true
|
||||
@@ -568,9 +461,6 @@ onMounted(async () => {
|
||||
<button type="button" class="btn-login" :disabled="loading || socialCallbackLoading" @click="onSubmit">
|
||||
{{ loading || socialCallbackLoading ? '登录中...' : '登录系统' }}
|
||||
</button>
|
||||
<button type="button" class="btn-passkey" :disabled="passkeyLoading" @click="onPasskeyLogin">
|
||||
{{ passkeyLoading ? 'Passkey验证中...' : '使用 Passkey 登录' }}
|
||||
</button>
|
||||
|
||||
<div v-if="socialConfig.enabled" class="social-login-area">
|
||||
<div class="divider"><span>快捷登录</span></div>
|
||||
@@ -820,23 +710,6 @@ onMounted(async () => {
|
||||
transition: transform 0.15s, filter 0.15s;
|
||||
}
|
||||
|
||||
.btn-passkey {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
margin-top: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(17, 24, 39, 0.14);
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-passkey:hover:not(:disabled) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.social-login-area {
|
||||
margin-top: 14px;
|
||||
}
|
||||
@@ -859,7 +732,6 @@ onMounted(async () => {
|
||||
background: rgba(17, 24, 39, 0.12);
|
||||
}
|
||||
|
||||
.btn-passkey:disabled,
|
||||
.btn-login:disabled,
|
||||
.btn-ghost:disabled,
|
||||
.captcha-refresh:disabled {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
function ensurePublicKeyOptions(options) {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new Error('Passkey参数无效')
|
||||
}
|
||||
return options.publicKey && typeof options.publicKey === 'object' ? options.publicKey : options
|
||||
}
|
||||
|
||||
function base64UrlToUint8Array(base64url) {
|
||||
const value = String(base64url || '')
|
||||
const padding = '='.repeat((4 - (value.length % 4)) % 4)
|
||||
const base64 = (value + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const raw = window.atob(base64)
|
||||
const bytes = new Uint8Array(raw.length)
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
bytes[i] = raw.charCodeAt(i)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function uint8ArrayToBase64Url(input) {
|
||||
const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input || [])
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return window
|
||||
.btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/g, '')
|
||||
}
|
||||
|
||||
function toCreationOptions(rawOptions) {
|
||||
const options = ensurePublicKeyOptions(rawOptions)
|
||||
const normalized = {
|
||||
...options,
|
||||
challenge: base64UrlToUint8Array(options.challenge),
|
||||
user: {
|
||||
...options.user,
|
||||
id: base64UrlToUint8Array(options.user?.id),
|
||||
},
|
||||
}
|
||||
|
||||
if (Array.isArray(options.excludeCredentials)) {
|
||||
normalized.excludeCredentials = options.excludeCredentials.map((item) => ({
|
||||
...item,
|
||||
id: base64UrlToUint8Array(item.id),
|
||||
}))
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function toRequestOptions(rawOptions) {
|
||||
const options = ensurePublicKeyOptions(rawOptions)
|
||||
const normalized = {
|
||||
...options,
|
||||
challenge: base64UrlToUint8Array(options.challenge),
|
||||
}
|
||||
|
||||
if (Array.isArray(options.allowCredentials)) {
|
||||
normalized.allowCredentials = options.allowCredentials.map((item) => ({
|
||||
...item,
|
||||
id: base64UrlToUint8Array(item.id),
|
||||
}))
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function serializeCredential(credential) {
|
||||
if (!credential) return null
|
||||
|
||||
const response = credential.response || {}
|
||||
const output = {
|
||||
id: credential.id,
|
||||
rawId: uint8ArrayToBase64Url(credential.rawId),
|
||||
type: credential.type,
|
||||
authenticatorAttachment: credential.authenticatorAttachment || undefined,
|
||||
response: {},
|
||||
}
|
||||
|
||||
if (response.clientDataJSON) {
|
||||
output.response.clientDataJSON = uint8ArrayToBase64Url(response.clientDataJSON)
|
||||
}
|
||||
if (response.attestationObject) {
|
||||
output.response.attestationObject = uint8ArrayToBase64Url(response.attestationObject)
|
||||
}
|
||||
if (response.authenticatorData) {
|
||||
output.response.authenticatorData = uint8ArrayToBase64Url(response.authenticatorData)
|
||||
}
|
||||
if (response.signature) {
|
||||
output.response.signature = uint8ArrayToBase64Url(response.signature)
|
||||
}
|
||||
|
||||
if (response.userHandle) {
|
||||
output.response.userHandle = uint8ArrayToBase64Url(response.userHandle)
|
||||
} else {
|
||||
output.response.userHandle = null
|
||||
}
|
||||
|
||||
if (typeof response.getTransports === 'function') {
|
||||
output.response.transports = response.getTransports() || []
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function isPasskeyAvailable() {
|
||||
return typeof window !== 'undefined' && window.isSecureContext && !!window.PublicKeyCredential && !!navigator.credentials
|
||||
}
|
||||
|
||||
function isMiuiBrowser() {
|
||||
const ua = String(window?.navigator?.userAgent || '')
|
||||
return /MiuiBrowser|XiaoMi\/MiuiBrowser/i.test(ua)
|
||||
}
|
||||
|
||||
export function getPasskeyClientErrorMessage(error, actionLabel = 'Passkey操作') {
|
||||
const name = String(error?.name || '').trim()
|
||||
const message = String(error?.message || '').trim()
|
||||
|
||||
if (name === 'NotAllowedError') {
|
||||
return `${actionLabel}未完成(可能已取消、超时或设备未响应)`
|
||||
}
|
||||
|
||||
if (name === 'NotReadableError') {
|
||||
if (/credential manager/i.test(message) && isMiuiBrowser()) {
|
||||
return '当前小米浏览器与系统凭据管理器兼容性较差,请改用系统 Chrome 或 Edge 后重试。'
|
||||
}
|
||||
if (/credential manager/i.test(message)) {
|
||||
return '系统凭据管理器返回异常,请确认已设置系统锁屏并改用系统 Chrome/Edge 后重试。'
|
||||
}
|
||||
return message || `${actionLabel}失败(设备读取异常)`
|
||||
}
|
||||
|
||||
if (name === 'SecurityError') {
|
||||
return '当前环境安全策略不满足 Passkey 要求,请确认使用 HTTPS 且证书有效。'
|
||||
}
|
||||
|
||||
return message || `${actionLabel}失败`
|
||||
}
|
||||
|
||||
export async function createPasskey(rawOptions) {
|
||||
const publicKey = toCreationOptions(rawOptions)
|
||||
const credential = await navigator.credentials.create({ publicKey })
|
||||
return serializeCredential(credential)
|
||||
}
|
||||
|
||||
export async function authenticateWithPasskey(rawOptions) {
|
||||
const publicKey = toRequestOptions(rawOptions)
|
||||
const credential = await navigator.credentials.get({ publicKey })
|
||||
return serializeCredential(credential)
|
||||
}
|
||||
Reference in New Issue
Block a user