diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 6ee8e2441..2674a42ab 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1942,7 +1942,7 @@ const message = { passkey: 'Passkey', passkeyManage: 'Manage', passkeyHelper: 'For quick login, up to 5 passkeys can be bound', - passkeyRequireSSL: 'Enable HTTPS to use passkeys', + passkeyRequireSSL: 'Enable SSL with a trusted certificate and bind a domain name before using passkeys', passkeyNotSupported: 'Current browser or environment does not support passkeys', passkeyCount: 'Bound {0}/{1}', passkeyName: 'Name', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index c4b7b4dd3..92cf68fa3 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -1955,8 +1955,8 @@ const message = { passkey: 'Passkey', passkeyManage: 'Administrar', passkeyHelper: 'Para inicio rápido, se pueden vincular hasta 5 passkeys', - passkeyRequireSSL: 'Habilita HTTPS para usar passkeys', - passkeyNotSupported: 'El navegador o entorno actual no admite Passkey', + passkeyRequireSSL: 'Activa SSL con un certificado de confianza y vincula un dominio para usar passkeys', + passkeyNotSupported: 'El navegador o entorno actual no admite passkeys', passkeyCount: 'Vinculadas {0}/{1}', passkeyName: 'Nombre', passkeyNameHelper: 'Introduce un nombre para distinguir dispositivos', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index cfbd3ebc3..d1cebad83 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1867,7 +1867,7 @@ const message = { passkey: 'パスキー', passkeyManage: '管理', passkeyHelper: '素早くログインするため、最大 5 個のパスキーを登録できます', - passkeyRequireSSL: 'HTTPS を有効にするとパスキーを使用できます', + passkeyRequireSSL: '信頼された証明書で SSL を有効化し、ドメインをバインドしてからパスキーを使用できます', passkeyNotSupported: '現在のブラウザまたは環境はパスキーに対応していません', passkeyCount: '登録済み {0}/{1}', passkeyName: '名称', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 4867841b7..cb6617042 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -1843,7 +1843,7 @@ const message = { passkey: '패스키', passkeyManage: '관리', passkeyHelper: '빠른 로그인을 위해 최대 5개의 패스키를 등록할 수 있습니다', - passkeyRequireSSL: 'HTTPS를 활성화하면 패스키를 사용할 수 있습니다', + passkeyRequireSSL: '신뢰할 수 있는 인증서로 SSL을 활성화하고 도메인을 바인딩한 후 패스키를 사용할 수 있습니다', passkeyNotSupported: '현재 브라우저 또는 환경에서 패스키를 지원하지 않습니다', passkeyCount: '등록됨 {0}/{1}', passkeyName: '이름', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 128c9a4ff..3cd7a3405 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1921,7 +1921,7 @@ const message = { passkey: 'Passkey', passkeyManage: 'Urus', passkeyHelper: 'Untuk log masuk pantas, maksimum 5 passkey boleh dipautkan', - passkeyRequireSSL: 'Aktifkan HTTPS untuk menggunakan passkey', + passkeyRequireSSL: 'Aktifkan SSL dengan sijil dipercayai dan ikat nama domain untuk menggunakan passkey', passkeyNotSupported: 'Pelayar atau persekitaran semasa tidak menyokong passkey', passkeyCount: 'Dipaut {0}/{1}', passkeyName: 'Nama', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index b32ef47f2..0be45ae1b 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1909,7 +1909,7 @@ const message = { passkey: 'Passkey', passkeyManage: 'Gerenciar', passkeyHelper: 'Para login rápido, é possível vincular até 5 passkeys', - passkeyRequireSSL: 'Ative o HTTPS para usar passkeys', + passkeyRequireSSL: 'Ative o SSL com um certificado confiável e vincule um domínio para usar passkeys', passkeyNotSupported: 'O navegador ou ambiente atual não suporta passkeys', passkeyCount: 'Vinculadas {0}/{1}', passkeyName: 'Nome', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index fea549664..a7554a035 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1910,7 +1910,7 @@ const message = { passkey: 'Passkey', passkeyManage: 'Управление', passkeyHelper: 'Для быстрого входа можно привязать до 5 passkey', - passkeyRequireSSL: 'Включите HTTPS, чтобы использовать passkey', + passkeyRequireSSL: 'Включите SSL с доверенным сертификатом и привяжите домен, чтобы использовать passkey', passkeyNotSupported: 'Текущий браузер или среда не поддерживает passkey', passkeyCount: 'Привязано {0}/{1}', passkeyName: 'Название', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 2301a3103..d6f8d3c5e 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1962,7 +1962,7 @@ const message = { passkey: 'Passkey', passkeyManage: 'Yönet', passkeyHelper: 'Hızlı giriş için en fazla 5 passkey bağlanabilir', - passkeyRequireSSL: 'Passkey kullanmak için HTTPS’i etkinleştirin', + passkeyRequireSSL: 'Passkey kullanmak için güvenilir sertifika ile SSL’yi etkinleştirin ve alan adını bağlayın', passkeyNotSupported: 'Mevcut tarayıcı veya ortam passkey desteklemiyor', passkeyCount: 'Bağlı {0}/{1}', passkeyName: 'Ad', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 387672e55..20372dd9f 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1903,9 +1903,8 @@ const message = { passkey: '通行密鑰', passkeyManage: '管理', passkeyHelper: '用於快速登入,最多可綁定 5 個通行密鑰', - passkeyRequireSSL: '啟用 HTTPS 後可使用通行密鑰', - passkeyNotSupported: - '目前瀏覽器或環境不支援通行密鑰 \n 請確認您已綁定域名並啟用面板 SSL, 並且訪問時使用了可信的證書', + passkeyRequireSSL: '使用可信證書啟用 SSL 並且設定網域綁定後才可使用通行密鑰', + passkeyNotSupported: '目前瀏覽器或環境不支援通行密鑰', passkeyCount: '已綁定 {0}/{1}', passkeyName: '名稱', passkeyNameHelper: '請輸入用於區分裝置的名稱', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index df9581a72..36f24665e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1950,9 +1950,8 @@ const message = { passkey: '通行密钥', passkeyManage: '管理', passkeyHelper: '用于快速登录,最多可绑定 5 个通行密钥', - passkeyRequireSSL: '开启 HTTPS 后可使用通行密钥', - passkeyNotSupported: - '当前浏览器或环境不支持通行密钥 \n 请确认您已经设置域名绑定并启用了面板 SSL, 并且访问时使用了可信的证书', + passkeyRequireSSL: '使用可信证书开启 SSL 并且设置域名绑定后才可使用通行密钥', + passkeyNotSupported: '当前浏览器或环境不支持通行密钥', passkeyCount: '已绑定 {0}/{1}', passkeyName: '名称', passkeyNameHelper: '请输入用于区分设备的名称', @@ -1961,7 +1960,7 @@ const message = { passkeyLastUsedAt: '最近使用', passkeyDeleteConfirm: '删除后将无法使用该通行密钥登录,是否继续?', passkeyLimit: '最多可绑定 5 个通行密钥', - passkeyFailed: '通行密钥注册失败,请确认面板SSL证书是否为可信证书', + passkeyFailed: '通行密钥注册失败,请确认面板 SSL 证书是否为可信证书', sslChangeHelper: 'https 设置修改需要重启服务,是否继续?', sslDisable: '禁用', sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续?', diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index 19e77b832..62ba1d9f3 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -191,38 +191,7 @@ - - -
- - - - -
- - {{ $t('setting.passkeyAdd') }} - - {{ passkeyCountText }} -
-
-
- - - - - - - - - - -
+ @@ -238,22 +207,12 @@ import TimeoutSetting from '@/views/setting/safe/timeout/index.vue'; import EntranceSetting from '@/views/setting/safe/entrance/index.vue'; import DomainSetting from '@/views/setting/safe/domain/index.vue'; import AllowIPsSetting from '@/views/setting/safe/allowips/index.vue'; -import { - updateSetting, - getSettingInfo, - getSystemAvailable, - updateSSL, - loadSSLInfo, - passkeyRegisterBegin, - passkeyRegisterFinish, - passkeyList as fetchPasskeyList, - passkeyDelete, -} from '@/api/modules/setting'; +import PasskeySetting from '@/views/setting/safe/passkey/index.vue'; +import { updateSetting, getSettingInfo, getSystemAvailable, updateSSL, loadSSLInfo } from '@/api/modules/setting'; import i18n from '@/lang'; -import { MsgError, MsgSuccess } from '@/utils/message'; +import { MsgSuccess } from '@/utils/message'; import { Setting } from '@/api/interface/setting'; import { GlobalStore } from '@/store'; -import { base64UrlToBuffer, bufferToBase64Url } from '@/utils/util'; const globalStore = GlobalStore(); const loading = ref(false); @@ -269,6 +228,7 @@ const lastSSL = ref('Disable'); const sslInfo = ref(); const domainRef = ref(); const allowIPsRef = ref(); +const passkeyRef = ref(); const mobile = computed(() => { return globalStore.isMobile(); }); @@ -292,12 +252,7 @@ const form = reactive({ noAuthSettingValue: '200', }); -const passkeyDialogVisible = ref(false); -const passkeyLoading = ref(false); -const passkeyList = ref([]); -const passkeyForm = reactive({ name: '' }); const passkeySupported = ref(false); -const passkeyMaxCount = 5; const passkeyHint = computed(() => { if (form.ssl === 'Disable') { return i18n.global.t('setting.passkeyRequireSSL'); @@ -307,18 +262,6 @@ const passkeyHint = computed(() => { } return i18n.global.t('setting.passkeyHelper'); }); -const passkeyCountText = computed(() => { - return i18n.global.t('setting.passkeyCount', [passkeyList.value.length, passkeyMaxCount]); -}); -const canRegisterPasskey = computed(() => { - return ( - form.ssl !== 'Disable' && - passkeySupported.value && - passkeyList.value.length < passkeyMaxCount && - passkeyForm.name.trim().length > 0 - ); -}); - const unset = ref(i18n.global.t('setting.unSetting')); const search = async () => { @@ -397,105 +340,7 @@ const handleMFA = async () => { }; const openPasskeyDialog = async () => { - passkeyDialogVisible.value = true; - await loadPasskeys(); -}; - -const loadPasskeys = async () => { - passkeyLoading.value = true; - try { - const res = await fetchPasskeyList(); - passkeyList.value = res.data || []; - } catch (error) { - passkeyList.value = []; - } finally { - passkeyLoading.value = false; - } -}; - -const registerPasskey = async () => { - if (form.ssl === 'Disable') { - MsgError(i18n.global.t('setting.passkeyRequireSSL')); - return; - } - if (!passkeySupported.value) { - MsgError(i18n.global.t('setting.passkeyNotSupported')); - return; - } - if (passkeyList.value.length >= passkeyMaxCount) { - MsgError(i18n.global.t('setting.passkeyLimit')); - return; - } - if (!passkeyForm.name.trim()) { - MsgError(i18n.global.t('commons.rule.requiredInput')); - return; - } - passkeyLoading.value = true; - try { - const res = await passkeyRegisterBegin({ name: passkeyForm.name.trim() }); - const publicKey = normalizePasskeyCreation(res.data.publicKey); - const credential = (await navigator.credentials.create({ publicKey })) as PublicKeyCredential | null; - if (!credential) { - MsgError(i18n.global.t('setting.passkeyFailed')); - return; - } - const payload = buildPasskeyAttestation(credential); - await passkeyRegisterFinish(payload, res.data.sessionId); - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - passkeyForm.name = ''; - await loadPasskeys(); - } catch (res: any) { - if (res?.message) { - console.log(res.message); - MsgError(i18n.global.t('setting.passkeyFailed')); - } - } finally { - passkeyLoading.value = false; - } -}; - -const removePasskey = async (id: string) => { - ElMessageBox.confirm(i18n.global.t('setting.passkeyDeleteConfirm'), i18n.global.t('setting.passkey'), { - confirmButtonText: i18n.global.t('commons.button.confirm'), - cancelButtonText: i18n.global.t('commons.button.cancel'), - }) - .then(async () => { - passkeyLoading.value = true; - await passkeyDelete(id); - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - await loadPasskeys(); - }) - .catch(() => {}) - .finally(() => { - passkeyLoading.value = false; - }); -}; - -const normalizePasskeyCreation = (publicKey: Record): PublicKeyCredentialCreationOptions => { - const request = { ...publicKey }; - request.challenge = base64UrlToBuffer(request.challenge); - request.user = { ...request.user, id: base64UrlToBuffer(request.user.id) }; - if (request.excludeCredentials && Array.isArray(request.excludeCredentials)) { - request.excludeCredentials = request.excludeCredentials.map((item) => { - return { ...item, id: base64UrlToBuffer(item.id) }; - }); - } - return request as PublicKeyCredentialCreationOptions; -}; - -const buildPasskeyAttestation = (credential: PublicKeyCredential) => { - const response = credential.response as AuthenticatorAttestationResponse; - return { - id: credential.id, - rawId: bufferToBase64Url(credential.rawId), - type: credential.type, - response: { - clientDataJSON: bufferToBase64Url(response.clientDataJSON), - attestationObject: bufferToBase64Url(response.attestationObject), - }, - clientExtensionResults: credential.getClientExtensionResults(), - authenticatorAttachment: credential.authenticatorAttachment, - }; + passkeyRef.value.acceptParams({ sslStatus: form.ssl, supported: passkeySupported.value }); }; const onChangeEntrance = () => { diff --git a/frontend/src/views/setting/safe/passkey/index.vue b/frontend/src/views/setting/safe/passkey/index.vue new file mode 100644 index 000000000..5e4779f6a --- /dev/null +++ b/frontend/src/views/setting/safe/passkey/index.vue @@ -0,0 +1,187 @@ + + +