From 0443c976fc143e87eb4bb2d1e4b93597042a7c9d Mon Sep 17 00:00:00 2001 From: 237899745 <237899745@workyai.cn> Date: Wed, 27 May 2026 22:32:42 +0800 Subject: [PATCH] refactor: remove passkey login --- admin-frontend/src/api/admin.js | 25 - admin-frontend/src/pages/SettingsPage.vue | 161 ------ admin-frontend/src/utils/passkey.js | 130 ----- app-frontend/src/api/auth.js | 10 - app-frontend/src/api/settings.js | 25 - app-frontend/src/layouts/AppLayout.vue | 163 +----- app-frontend/src/pages/LoginPage.vue | 128 ----- app-frontend/src/utils/passkey.js | 153 ----- app.py | 6 +- database.py | 9 - db/migrations.py | 26 +- db/passkeys.py | 173 ------ db/schema.py | 21 - requirements.txt | 1 - routes/admin_api/core.py | 357 ------------ routes/api_auth.py | 184 ------ routes/api_user.py | 205 ------- services/passkeys.py | 193 ------- static/admin/.vite/manifest.json | 76 +-- ...> AdminSocialBindCallbackPage-C0b0Vr3s.js} | 2 +- ...VG51R.js => AnnouncementsPage-CDXDBA1J.js} | 2 +- ...Page-B1uMhyWi.js => EmailPage-CPa_VcxD.js} | 2 +- ...-CG9FZytm.js => FeedbacksPage-Dfxcbeo8.js} | 2 +- ...sPage-Ct-BSxV6.js => LogsPage-Dyx_Pdm0.js} | 2 +- ...rid-kv-nSROj.js => MetricGrid-VHNT01i6.js} | 2 +- ...age-2jS10KoG.js => ReportPage-nz6X9nYS.js} | 2 +- ...e-93lfkhLF.js => SecurityPage-WZIr3v_6.js} | 2 +- static/admin/assets/SettingsPage-BbHyIZsy.js | 1 - static/admin/assets/SettingsPage-CjBdzgUX.js | 1 + static/admin/assets/SettingsPage-CjIQQfeg.css | 1 - static/admin/assets/SettingsPage-Wd9F5VCe.css | 1 + ...age-D9T-fhw-.js => SystemPage-6eR7PzW1.js} | 2 +- ...Page-2-Mno2hz.js => UsersPage-CFbejgdN.js} | 2 +- static/admin/assets/admin-DcqTfJCB.js | 1 + static/admin/assets/admin-VsbfHbbH.js | 1 - .../{email-CgUCpCe3.js => email-CoLcTI83.js} | 2 +- .../{index-6ynv0Z9Y.js => index-zJym-Cg7.js} | 4 +- ...{system-CeiBEEoE.js => system-CARqmgIY.js} | 2 +- .../{tasks-C6JkguA6.js => tasks-BFgZ7F4T.js} | 2 +- .../{users-D9XvGIoE.js => users-DuxohAiS.js} | 2 +- static/admin/index.html | 2 +- static/app/.vite/manifest.json | 523 +++++++++--------- ...e-B7MLZrfr.js => AccountsPage-DZM5eF8A.js} | 4 +- static/app/assets/AppLayout-8mkxrTVV.js | 1 + static/app/assets/AppLayout-C0FaVSZn.css | 1 + static/app/assets/AppLayout-CJKAa2WS.css | 1 - static/app/assets/AppLayout-D9A8Va7K.js | 1 - static/app/assets/LoginPage-BtooAZsk.js | 1 - static/app/assets/LoginPage-CSaMrhQm.css | 1 + static/app/assets/LoginPage-N6sdjwkY.js | 1 + static/app/assets/LoginPage-vCVLchWz.css | 1 - ...e-Cb1mme2j.js => RegisterPage-Bypz6ilN.js} | 2 +- ...K0fe1.js => ResetPasswordPage-Znm7wIOo.js} | 2 +- static/app/assets/SchedulesPage-0TKGPmUl.js | 1 - static/app/assets/SchedulesPage-vAAprGPM.js | 1 + static/app/assets/ScreenshotsPage-DrfiqfWk.js | 1 + static/app/assets/ScreenshotsPage-F6GpvKGW.js | 1 - ....js => SocialBindCallbackPage-BXLD-LiQ.js} | 2 +- ...r6Mm.js => SocialLoginButtons-BaFXslgf.js} | 2 +- ...SE4fL8.js => VerifyResultPage-Du3cLyZ2.js} | 2 +- static/app/assets/accounts-D_6SYB2i.css | 1 - ...ser-B7bO5p8k.css => accounts-DqlHDq0H.css} | 2 +- static/app/assets/accounts-DzntEHJR.js | 1 - static/app/assets/accounts-HALpNswY.js | 1 + static/app/assets/app-CV_JALyE.js | 2 - static/app/assets/app-D7SWy-KG.js | 2 + .../{auth-B5cl_nsV.js => auth-CuW_jyJD.js} | 2 +- static/app/assets/base-C_0HtztH.js | 1 + static/app/assets/base-xgxQQEpV.js | 1 - static/app/assets/el-alert-BgJljmz-.js | 12 + static/app/assets/el-alert-DTUOkrAB.js | 12 - static/app/assets/el-button-LKkD3jQh.js | 1 - static/app/assets/el-button-xGNUoXVX.js | 1 + ...l-card-CfK866jr.js => el-card-cnxuvbL3.js} | 2 +- static/app/assets/el-empty-B4_NEFfq.js | 1 - static/app/assets/el-empty-D4G4LZ50.css | 1 - static/app/assets/el-input-BaZNy9Kg.js | 1 - static/app/assets/el-input-nl0Ylqa_.js | 1 + ...lay-hge8bsIn.js => el-overlay-ckkTzDcK.js} | 2 +- static/app/assets/el-pagination-D16TMO1B.js | 1 + static/app/assets/el-pagination-kVJ2XlAP.js | 1 - static/app/assets/el-popper-BrfLRiIr.css | 1 + static/app/assets/el-popper-_4NhtSRX.js | 1 + static/app/assets/el-select-B0XIb2QK.css | 1 + static/app/assets/el-select-BADfKG7m.js | 1 + static/app/assets/el-select-CBs1QjJm.js | 1 - static/app/assets/el-select-D_oyzAZN.css | 1 - .../app/assets/el-skeleton-item-CD5Idavp.js | 1 - .../app/assets/el-skeleton-item-cWa5ANvD.js | 1 + static/app/assets/http-BDcxFXLM.js | 31 ++ static/app/assets/http-BoPYlvwK.js | 31 -- static/app/assets/index-CoYtSGUZ.js | 1 - static/app/assets/index-D04QrwME.js | 1 + .../app/assets/isArrayLikeObject-B5fs56rA.js | 1 - static/app/assets/login-C88J0b5r.js | 1 + static/app/assets/login-rQcRwu0T.js | 1 - static/app/assets/settings-C8OWd3zp.js | 1 + static/app/assets/settings-Db4PmPGC.js | 1 - static/app/assets/user-B5lTGWdM.css | 1 + static/app/assets/user-BlXB4Zbh.js | 1 - static/app/assets/user-DIrCtqzm.js | 1 + ...vue-WbiK4TmU.js => vendor-vue-Da_zwKNU.js} | 2 +- static/app/index.html | 5 +- static/app/login.html | 21 +- templates/admin_login.html | 123 ---- 105 files changed, 410 insertions(+), 2505 deletions(-) delete mode 100644 admin-frontend/src/utils/passkey.js delete mode 100644 app-frontend/src/utils/passkey.js delete mode 100644 db/passkeys.py delete mode 100644 services/passkeys.py rename static/admin/assets/{AdminSocialBindCallbackPage-BsLZg3f-.js => AdminSocialBindCallbackPage-C0b0Vr3s.js} (91%) rename static/admin/assets/{AnnouncementsPage-BcIVG51R.js => AnnouncementsPage-CDXDBA1J.js} (99%) rename static/admin/assets/{EmailPage-B1uMhyWi.js => EmailPage-CPa_VcxD.js} (99%) rename static/admin/assets/{FeedbacksPage-CG9FZytm.js => FeedbacksPage-Dfxcbeo8.js} (97%) rename static/admin/assets/{LogsPage-Ct-BSxV6.js => LogsPage-Dyx_Pdm0.js} (98%) rename static/admin/assets/{MetricGrid-kv-nSROj.js => MetricGrid-VHNT01i6.js} (94%) rename static/admin/assets/{ReportPage-2jS10KoG.js => ReportPage-nz6X9nYS.js} (98%) rename static/admin/assets/{SecurityPage-93lfkhLF.js => SecurityPage-WZIr3v_6.js} (99%) delete mode 100644 static/admin/assets/SettingsPage-BbHyIZsy.js create mode 100644 static/admin/assets/SettingsPage-CjBdzgUX.js delete mode 100644 static/admin/assets/SettingsPage-CjIQQfeg.css create mode 100644 static/admin/assets/SettingsPage-Wd9F5VCe.css rename static/admin/assets/{SystemPage-D9T-fhw-.js => SystemPage-6eR7PzW1.js} (98%) rename static/admin/assets/{UsersPage-2-Mno2hz.js => UsersPage-CFbejgdN.js} (99%) create mode 100644 static/admin/assets/admin-DcqTfJCB.js delete mode 100644 static/admin/assets/admin-VsbfHbbH.js rename static/admin/assets/{email-CgUCpCe3.js => email-CoLcTI83.js} (88%) rename static/admin/assets/{index-6ynv0Z9Y.js => index-zJym-Cg7.js} (90%) rename static/admin/assets/{system-CeiBEEoE.js => system-CARqmgIY.js} (88%) rename static/admin/assets/{tasks-C6JkguA6.js => tasks-BFgZ7F4T.js} (93%) rename static/admin/assets/{users-D9XvGIoE.js => users-DuxohAiS.js} (90%) rename static/app/assets/{AccountsPage-B7MLZrfr.js => AccountsPage-DZM5eF8A.js} (68%) create mode 100644 static/app/assets/AppLayout-8mkxrTVV.js create mode 100644 static/app/assets/AppLayout-C0FaVSZn.css delete mode 100644 static/app/assets/AppLayout-CJKAa2WS.css delete mode 100644 static/app/assets/AppLayout-D9A8Va7K.js delete mode 100644 static/app/assets/LoginPage-BtooAZsk.js create mode 100644 static/app/assets/LoginPage-CSaMrhQm.css create mode 100644 static/app/assets/LoginPage-N6sdjwkY.js delete mode 100644 static/app/assets/LoginPage-vCVLchWz.css rename static/app/assets/{RegisterPage-Cb1mme2j.js => RegisterPage-Bypz6ilN.js} (91%) rename static/app/assets/{ResetPasswordPage-CUOK0fe1.js => ResetPasswordPage-Znm7wIOo.js} (86%) delete mode 100644 static/app/assets/SchedulesPage-0TKGPmUl.js create mode 100644 static/app/assets/SchedulesPage-vAAprGPM.js create mode 100644 static/app/assets/ScreenshotsPage-DrfiqfWk.js delete mode 100644 static/app/assets/ScreenshotsPage-F6GpvKGW.js rename static/app/assets/{SocialBindCallbackPage-DraQ_mks.js => SocialBindCallbackPage-BXLD-LiQ.js} (81%) rename static/app/assets/{SocialLoginButtons-BlVSr6Mm.js => SocialLoginButtons-BaFXslgf.js} (98%) rename static/app/assets/{VerifyResultPage-BUSE4fL8.js => VerifyResultPage-Du3cLyZ2.js} (93%) delete mode 100644 static/app/assets/accounts-D_6SYB2i.css rename static/app/assets/{user-B7bO5p8k.css => accounts-DqlHDq0H.css} (62%) delete mode 100644 static/app/assets/accounts-DzntEHJR.js create mode 100644 static/app/assets/accounts-HALpNswY.js delete mode 100644 static/app/assets/app-CV_JALyE.js create mode 100644 static/app/assets/app-D7SWy-KG.js rename static/app/assets/{auth-B5cl_nsV.js => auth-CuW_jyJD.js} (91%) create mode 100644 static/app/assets/base-C_0HtztH.js delete mode 100644 static/app/assets/base-xgxQQEpV.js create mode 100644 static/app/assets/el-alert-BgJljmz-.js delete mode 100644 static/app/assets/el-alert-DTUOkrAB.js delete mode 100644 static/app/assets/el-button-LKkD3jQh.js create mode 100644 static/app/assets/el-button-xGNUoXVX.js rename static/app/assets/{el-card-CfK866jr.js => el-card-cnxuvbL3.js} (60%) delete mode 100644 static/app/assets/el-empty-B4_NEFfq.js delete mode 100644 static/app/assets/el-empty-D4G4LZ50.css delete mode 100644 static/app/assets/el-input-BaZNy9Kg.js create mode 100644 static/app/assets/el-input-nl0Ylqa_.js rename static/app/assets/{el-overlay-hge8bsIn.js => el-overlay-ckkTzDcK.js} (76%) create mode 100644 static/app/assets/el-pagination-D16TMO1B.js delete mode 100644 static/app/assets/el-pagination-kVJ2XlAP.js create mode 100644 static/app/assets/el-popper-BrfLRiIr.css create mode 100644 static/app/assets/el-popper-_4NhtSRX.js create mode 100644 static/app/assets/el-select-B0XIb2QK.css create mode 100644 static/app/assets/el-select-BADfKG7m.js delete mode 100644 static/app/assets/el-select-CBs1QjJm.js delete mode 100644 static/app/assets/el-select-D_oyzAZN.css delete mode 100644 static/app/assets/el-skeleton-item-CD5Idavp.js create mode 100644 static/app/assets/el-skeleton-item-cWa5ANvD.js create mode 100644 static/app/assets/http-BDcxFXLM.js delete mode 100644 static/app/assets/http-BoPYlvwK.js delete mode 100644 static/app/assets/index-CoYtSGUZ.js create mode 100644 static/app/assets/index-D04QrwME.js delete mode 100644 static/app/assets/isArrayLikeObject-B5fs56rA.js create mode 100644 static/app/assets/login-C88J0b5r.js delete mode 100644 static/app/assets/login-rQcRwu0T.js create mode 100644 static/app/assets/settings-C8OWd3zp.js delete mode 100644 static/app/assets/settings-Db4PmPGC.js create mode 100644 static/app/assets/user-B5lTGWdM.css delete mode 100644 static/app/assets/user-BlXB4Zbh.js create mode 100644 static/app/assets/user-DIrCtqzm.js rename static/app/assets/{vendor-vue-WbiK4TmU.js => vendor-vue-Da_zwKNU.js} (99%) diff --git a/admin-frontend/src/api/admin.js b/admin-frontend/src/api/admin.js index 97d7ff7..18405b7 100644 --- a/admin-frontend/src/api/admin.js +++ b/admin-frontend/src/api/admin.js @@ -20,31 +20,6 @@ export async function logout() { return data } -export async function fetchAdminPasskeys() { - const { data } = await api.get('/admin/passkeys') - return data -} - -export async function createAdminPasskeyOptions(payload = {}) { - const { data } = await api.post('/admin/passkeys/register/options', payload) - return data -} - -export async function createAdminPasskeyVerify(payload = {}) { - const { data } = await api.post('/admin/passkeys/register/verify', payload) - return data -} - -export async function deleteAdminPasskey(passkeyId) { - const { data } = await api.delete(`/admin/passkeys/${passkeyId}`) - return data -} - -export async function reportAdminPasskeyClientError(payload = {}) { - const { data } = await api.post('/admin/passkeys/client-error', payload) - return data -} - export async function fetchAdminSocialBindings() { const { data } = await api.get('/admin/social-bindings') return data diff --git a/admin-frontend/src/pages/SettingsPage.vue b/admin-frontend/src/pages/SettingsPage.vue index c2f4cec..ce1d105 100644 --- a/admin-frontend/src/pages/SettingsPage.vue +++ b/admin-frontend/src/pages/SettingsPage.vue @@ -5,32 +5,19 @@ import QrcodeVue from 'qrcode.vue' import { createAdminSocialLoginUrl, - createAdminPasskeyOptions, - createAdminPasskeyVerify, fetchAdminSocialBindings, - deleteAdminPasskey, - fetchAdminPasskeys, logout, pollAdminSocialLogin, - reportAdminPasskeyClientError, unbindAdminSocial, updateAdminPassword, updateAdminUsername, } from '../api/admin' -import { createPasskey, getPasskeyClientErrorMessage, isPasskeyAvailable } from '../utils/passkey' const username = ref('') const currentPassword = ref('') const password = ref('') const confirmPassword = ref('') const submitting = ref(false) -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 socialBindingsLoading = ref(false) const socialBindLoadingProvider = ref('') const socialBindings = ref([]) @@ -148,113 +135,6 @@ async function savePassword() { } } -async function loadPasskeys() { - passkeyLoading.value = true - try { - const data = await fetchAdminPasskeys() - 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 createAdminPasskeyOptions({}) - passkeyRegisterOptions.value = res - passkeyRegisterOptionsAt.value = Date.now() - } catch { - passkeyRegisterOptions.value = null - passkeyRegisterOptionsAt.value = 0 - } -} - -async function addPasskey() { - 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 createAdminPasskeyOptions({}) - } - const credential = await createPasskey(optionsRes?.publicKey || {}) - await createAdminPasskeyVerify({ credential, device_name: passkeyDeviceName.value.trim() }) - passkeyRegisterOptions.value = null - passkeyRegisterOptionsAt.value = 0 - passkeyDeviceName.value = '' - ElMessage.success('Passkey设备添加成功') - await loadPasskeys() - } catch (e) { - try { - await reportAdminPasskeyClientError({ - stage: 'register', - source: 'admin-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 removePasskey(item) { - try { - await ElMessageBox.confirm(`确定删除设备「${item?.device_name || '未命名设备'}」吗?`, '删除Passkey设备', { - confirmButtonText: '删除', - cancelButtonText: '取消', - type: 'warning', - }) - } catch { - return - } - - try { - await deleteAdminPasskey(item.id) - ElMessage.success('设备已删除') - await loadPasskeys() - } catch (e) { - const data = e?.response?.data - ElMessage.error(data?.error || '删除失败') - } -} - function socialBindRedirectUri() { const url = new URL(window.location.href) url.pathname = '/yuyx/admin-social-bind-callback' @@ -380,7 +260,6 @@ async function unbindSocial(item) { } onMounted(() => { - loadPasskeys() loadSocialBindings() }) @@ -443,46 +322,6 @@ onBeforeUnmount(() => {
建议使用更强密码(至少8位且包含字母与数字)。
- -

Passkey设备

- - - - - - - - 添加Passkey设备 - - - -
- - - - - - - - - - -
-
-

快捷登录绑定

diff --git a/admin-frontend/src/utils/passkey.js b/admin-frontend/src/utils/passkey.js deleted file mode 100644 index 6a0cd64..0000000 --- a/admin-frontend/src/utils/passkey.js +++ /dev/null @@ -1,130 +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 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) -} diff --git a/app-frontend/src/api/auth.js b/app-frontend/src/api/auth.js index 16b1757..43acd84 100644 --- a/app-frontend/src/api/auth.js +++ b/app-frontend/src/api/auth.js @@ -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 diff --git a/app-frontend/src/api/settings.js b/app-frontend/src/api/settings.js index fd05a65..9f93cd1 100644 --- a/app-frontend/src/api/settings.js +++ b/app-frontend/src/api/settings.js @@ -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 diff --git a/app-frontend/src/layouts/AppLayout.vue b/app-frontend/src/layouts/AppLayout.vue index daa386d..599416c 100644 --- a/app-frontend/src/layouts/AppLayout.vue +++ b/app-frontend/src/layouts/AppLayout.vue @@ -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() {
- -
- - - - - - - - - 添加Passkey设备 - - - - - - - - - - - - - - -
-
-
diff --git a/app-frontend/src/pages/LoginPage.vue b/app-frontend/src/pages/LoginPage.vue index 165e5bf..3216787 100644 --- a/app-frontend/src/pages/LoginPage.vue +++ b/app-frontend/src/pages/LoginPage.vue @@ -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 () => { - -