diff --git a/controller/passkey.go b/controller/passkey.go index 6c73b006a777..f63731b84e57 100644 --- a/controller/passkey.go +++ b/controller/passkey.go @@ -186,14 +186,17 @@ func PasskeyStatus(c *gin.Context) { return } + data := gin.H{ + "enabled": false, + "system_enabled": system_setting.GetPasskeySettings().Enabled, + } + credential, err := model.GetPasskeyByUserID(user.Id) if errors.Is(err, model.ErrPasskeyNotFound) { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", - "data": gin.H{ - "enabled": false, - }, + "data": data, }) return } @@ -202,10 +205,8 @@ func PasskeyStatus(c *gin.Context) { return } - data := gin.H{ - "enabled": true, - "last_used_at": credential.LastUsedAt, - } + data["enabled"] = true + data["last_used_at"] = credential.LastUsedAt c.JSON(http.StatusOK, gin.H{ "success": true, diff --git a/controller/passkey_test.go b/controller/passkey_test.go new file mode 100644 index 000000000000..5beaee7ec14e --- /dev/null +++ b/controller/passkey_test.go @@ -0,0 +1,102 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +package controller + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting/system_setting" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type passkeyStatusResponse struct { + Success bool `json:"success"` + Data struct { + Enabled bool `json:"enabled"` + SystemEnabled bool `json:"system_enabled"` + } `json:"data"` +} + +func TestPasskeyStatusSeparatesCredentialAndSystemState(t *testing.T) { + db := setupModelListControllerTestDB(t) + require.NoError(t, db.AutoMigrate(&model.PasskeyCredential{})) + + user := &model.User{ + Username: "passkey-status-user", + Password: "password", + Role: common.RoleCommonUser, + Status: common.UserStatusEnabled, + Group: "default", + } + require.NoError(t, db.Create(user).Error) + + passkeySettings := system_setting.GetPasskeySettings() + originalEnabled := passkeySettings.Enabled + t.Cleanup(func() { + passkeySettings.Enabled = originalEnabled + }) + + requestStatus := func() passkeyStatusResponse { + recorder := httptest.NewRecorder() + router := gin.New() + router.Use(sessions.Sessions("session", cookie.NewStore([]byte("passkey-status-secret")))) + router.GET("/", func(c *gin.Context) { + session := sessions.Default(c) + session.Set("id", user.Id) + PasskeyStatus(c) + }) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + router.ServeHTTP(recorder, request) + + require.Equal(t, http.StatusOK, recorder.Code) + var response passkeyStatusResponse + require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response)) + require.True(t, response.Success) + return response + } + + passkeySettings.Enabled = false + response := requestStatus() + assert.False(t, response.Data.Enabled) + assert.False(t, response.Data.SystemEnabled) + + passkeySettings.Enabled = true + response = requestStatus() + assert.False(t, response.Data.Enabled) + assert.True(t, response.Data.SystemEnabled) + + require.NoError(t, db.Create(&model.PasskeyCredential{ + UserID: user.Id, + CredentialID: "credential-id", + PublicKey: "public-key", + }).Error) + passkeySettings.Enabled = false + response = requestStatus() + assert.True(t, response.Data.Enabled) + assert.False(t, response.Data.SystemEnabled) +} diff --git a/web/default/src/features/auth/passkey/hooks/use-passkey-management.ts b/web/default/src/features/auth/passkey/hooks/use-passkey-management.ts index 670c9d664fcb..765062d9ae7c 100644 --- a/web/default/src/features/auth/passkey/hooks/use-passkey-management.ts +++ b/web/default/src/features/auth/passkey/hooks/use-passkey-management.ts @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import i18next from 'i18next' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { @@ -171,8 +171,9 @@ export function usePasskeyManagement( } }, [fetchStatus]) - const enabled = useMemo(() => Boolean(status?.enabled), [status]) - const lastUsed = useMemo(() => status?.last_used_at ?? null, [status]) + const enabled = Boolean(status?.enabled) + const systemEnabled = status?.system_enabled ?? null + const lastUsed = status?.last_used_at ?? null return { status, @@ -181,6 +182,7 @@ export function usePasskeyManagement( removing, supported, enabled, + systemEnabled, lastUsed, fetchStatus, register, diff --git a/web/default/src/features/auth/passkey/types.ts b/web/default/src/features/auth/passkey/types.ts index a331a549c146..a3c4563db15d 100644 --- a/web/default/src/features/auth/passkey/types.ts +++ b/web/default/src/features/auth/passkey/types.ts @@ -24,6 +24,7 @@ export interface ApiResponse { export interface PasskeyStatus { enabled: boolean + system_enabled: boolean last_used_at?: string | null backup_eligible?: boolean backup_state?: boolean diff --git a/web/default/src/features/auth/secure-verification/api.ts b/web/default/src/features/auth/secure-verification/api.ts index d5cada898061..a7da0eb545ef 100644 --- a/web/default/src/features/auth/secure-verification/api.ts +++ b/web/default/src/features/auth/secure-verification/api.ts @@ -46,7 +46,8 @@ export async function checkVerificationMethods(): Promise { Boolean(twoFAResponse?.success) && Boolean(twoFAResponse?.data?.enabled) const hasPasskey = Boolean(passkeyResponse?.success) && - Boolean(passkeyResponse?.data?.enabled) + Boolean(passkeyResponse?.data?.enabled) && + Boolean(passkeyResponse?.data?.system_enabled) return { has2FA, diff --git a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx index dcced4cf4bf9..ce2397e17010 100644 --- a/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx +++ b/web/default/src/features/auth/secure-verification/components/secure-verification-dialog.tsx @@ -61,7 +61,9 @@ export function SecureVerificationDialog({ }, [methods]) const activeMethod = - state.method ?? (availableTabs.length > 0 ? availableTabs[0] : null) + state.method && availableTabs.includes(state.method) + ? state.method + : (availableTabs[0] ?? null) const title = state.title ?? diff --git a/web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts b/web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts index a9d95cccc3fe..a6b2cb8d35a0 100644 --- a/web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts +++ b/web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import i18next from 'i18next' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { @@ -26,6 +26,7 @@ import { } from '@/lib/secure-verification' import { checkVerificationMethods, verify } from '../api' +import { selectVerificationMethod } from '../lib/select-verification-method' import type { SecureVerificationState, StartVerificationOptions, @@ -87,7 +88,12 @@ export function useSecureVerification( const { preferredMethod, title, description } = config const availableMethods = await fetchVerificationMethods() - if (!availableMethods.has2FA && !availableMethods.hasPasskey) { + const defaultMethod = selectVerificationMethod( + availableMethods, + preferredMethod + ) + + if (!defaultMethod) { toast.error( i18next.t( 'Please enable Two-factor Authentication or Passkey before proceeding' @@ -101,15 +107,6 @@ export function useSecureVerification( return false } - let defaultMethod: VerificationMethod | null = preferredMethod ?? null - if (!defaultMethod) { - if (availableMethods.hasPasskey && availableMethods.passkeySupported) { - defaultMethod = 'passkey' - } else if (availableMethods.has2FA) { - defaultMethod = '2fa' - } - } - setState((prev) => ({ ...prev, apiCall, @@ -211,11 +208,7 @@ export function useSecureVerification( [methods] ) - const recommendedMethod = useMemo(() => { - if (methods.hasPasskey && methods.passkeySupported) return 'passkey' - if (methods.has2FA) return '2fa' - return null - }, [methods]) + const recommendedMethod = selectVerificationMethod(methods) return { open, diff --git a/web/default/src/features/auth/secure-verification/lib/select-verification-method.test.ts b/web/default/src/features/auth/secure-verification/lib/select-verification-method.test.ts new file mode 100644 index 000000000000..ec55fe90e650 --- /dev/null +++ b/web/default/src/features/auth/secure-verification/lib/select-verification-method.test.ts @@ -0,0 +1,87 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' + +import type { VerificationMethod, VerificationMethods } from '../types' +import { selectVerificationMethod } from './select-verification-method' + +describe('selectVerificationMethod', () => { + const cases: Array<{ + name: string + methods: VerificationMethods + preferred?: VerificationMethod + expected: VerificationMethod | null + }> = [ + { + name: 'falls back to 2FA when the preferred Passkey is unavailable', + methods: { has2FA: true, hasPasskey: false, passkeySupported: true }, + preferred: 'passkey', + expected: '2fa', + }, + { + name: 'falls back to 2FA when a bound Passkey is unsupported by the device', + methods: { has2FA: true, hasPasskey: true, passkeySupported: false }, + preferred: 'passkey', + expected: '2fa', + }, + { + name: 'uses an available preferred 2FA method', + methods: { has2FA: true, hasPasskey: true, passkeySupported: true }, + preferred: '2fa', + expected: '2fa', + }, + { + name: 'uses an available preferred Passkey method', + methods: { has2FA: true, hasPasskey: true, passkeySupported: true }, + preferred: 'passkey', + expected: 'passkey', + }, + { + name: 'defaults to Passkey when no preference is provided', + methods: { has2FA: true, hasPasskey: true, passkeySupported: true }, + expected: 'passkey', + }, + { + name: 'defaults to 2FA when it is the only available method', + methods: { has2FA: true, hasPasskey: false, passkeySupported: true }, + expected: '2fa', + }, + { + name: 'falls back to Passkey when preferred 2FA is unavailable', + methods: { has2FA: false, hasPasskey: true, passkeySupported: true }, + preferred: '2fa', + expected: 'passkey', + }, + { + name: 'returns null when no usable method is available', + methods: { has2FA: false, hasPasskey: true, passkeySupported: false }, + expected: null, + }, + ] + + for (const testCase of cases) { + test(testCase.name, () => { + assert.equal( + selectVerificationMethod(testCase.methods, testCase.preferred), + testCase.expected + ) + }) + } +}) diff --git a/web/default/src/features/auth/secure-verification/lib/select-verification-method.ts b/web/default/src/features/auth/secure-verification/lib/select-verification-method.ts new file mode 100644 index 000000000000..7eca0cbf0b0b --- /dev/null +++ b/web/default/src/features/auth/secure-verification/lib/select-verification-method.ts @@ -0,0 +1,32 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import type { VerificationMethod, VerificationMethods } from '../types' + +export function selectVerificationMethod( + methods: VerificationMethods, + preferredMethod?: VerificationMethod +): VerificationMethod | null { + const passkeyAvailable = methods.hasPasskey && methods.passkeySupported + + if (preferredMethod === '2fa' && methods.has2FA) return '2fa' + if (preferredMethod === 'passkey' && passkeyAvailable) return 'passkey' + if (passkeyAvailable) return 'passkey' + if (methods.has2FA) return '2fa' + return null +} diff --git a/web/default/src/features/profile/components/passkey-card.tsx b/web/default/src/features/profile/components/passkey-card.tsx index 6ef09e6778fa..90bd3b51fc83 100644 --- a/web/default/src/features/profile/components/passkey-card.tsx +++ b/web/default/src/features/profile/components/passkey-card.tsx @@ -16,12 +16,14 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ +import { Link } from '@tanstack/react-router' import { AlertTriangle, KeyRound, Loader2, ShieldAlert } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { StatusBadge } from '@/components/status-badge' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { AlertDialog, AlertDialogAction, @@ -51,6 +53,8 @@ import { type VerificationMethods, } from '@/features/auth/secure-verification' import dayjs from '@/lib/dayjs' +import { ROLE } from '@/lib/roles' +import { useAuthStore } from '@/stores/auth-store' interface PasskeyCardProps { loading: boolean @@ -61,6 +65,8 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { const [confirmOpen, setConfirmOpen] = useState(false) const [restrictedMethod, setRestrictedMethod] = useState(null) + const isSuperAdmin = + useAuthStore((state) => state.auth.user?.role) === ROLE.SUPER_ADMIN const { status, @@ -69,6 +75,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { removing, supported, enabled, + systemEnabled, lastUsed, register, remove, @@ -102,6 +109,11 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { }, [restrictedMethod, verificationMethods]) const handleRegister = useCallback(async () => { + if (systemEnabled !== true) { + toast.info(t('Passkey login is disabled by the administrator.')) + return + } + if (!supported) { toast.info(t('This device does not support Passkey')) return @@ -123,7 +135,14 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { 'Confirm your identity with Two-factor Authentication before registering a Passkey.' ), }) - }, [fetchVerificationMethods, register, startVerification, supported, t]) + }, [ + fetchVerificationMethods, + register, + startVerification, + supported, + systemEnabled, + t, + ]) const handleRemove = useCallback(async () => { const methods = await fetchVerificationMethods() @@ -250,7 +269,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) { {t('Passkey Authentication')} - {!enabled && ( + {!enabled && systemEnabled === true && ( + {systemEnabled === false && ( + + + {t('Passkey is unavailable')} + + {t('Passkey login is disabled by the administrator.')} + {isSuperAdmin && ( + + {t('Open Passkey settings')} + + )} + + + )} + {enabled && ( diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 9eba821e391d..f577f1deb5e3 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -3097,6 +3097,7 @@ "Open in new tab": "Open in new tab", "Open in New Tab": "Open in New Tab", "Open menu": "Open menu", + "Open Passkey settings": "Open Passkey settings", "Open release": "Open release", "Open source": "Open source", "Open Source": "Open Source", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "Passkey is not supported in this environment", "Passkey is not supported on this device": "Passkey is not supported on this device", "Passkey is not supported on this device.": "Passkey is not supported on this device.", + "Passkey is unavailable": "Passkey is unavailable", "Passkey Login": "Passkey Login", "Passkey login failed": "Passkey login failed", + "Passkey login is disabled by the administrator.": "Passkey login is disabled by the administrator.", "Passkey login was cancelled": "Passkey login was cancelled", "Passkey login was cancelled or timed out": "Passkey login was cancelled or timed out", "Passkey not supported on this device": "Passkey not supported on this device", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index f4014a48f7f2..65f00030414e 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -3097,6 +3097,7 @@ "Open in new tab": "Ouvrir dans un nouvel onglet", "Open in New Tab": "Ouvrir dans un nouvel onglet", "Open menu": "Ouvrir le menu", + "Open Passkey settings": "Ouvrir les paramètres Passkey", "Open release": "Ouvrir la version", "Open source": "Open source", "Open Source": "Open source", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "Passkey n'est pas pris en charge dans cet environnement", "Passkey is not supported on this device": "Passkey n'est pas pris en charge sur cet appareil", "Passkey is not supported on this device.": "La Passkey n'est pas prise en charge sur cet appareil.", + "Passkey is unavailable": "Passkey indisponible", "Passkey Login": "Connexion avec Passkey", "Passkey login failed": "Échec de la connexion Passkey", + "Passkey login is disabled by the administrator.": "La connexion par Passkey est désactivée par l’administrateur.", "Passkey login was cancelled": "Connexion Passkey annulée", "Passkey login was cancelled or timed out": "Connexion Passkey annulée ou expirée", "Passkey not supported on this device": "Passkey non prise en charge sur cet appareil", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 527f49b9f25e..a731f8f28182 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -3097,6 +3097,7 @@ "Open in new tab": "新しいタブで開く", "Open in New Tab": "新しいタブで開く", "Open menu": "メニューを開く", + "Open Passkey settings": "Passkey 設定を開く", "Open release": "リリースを開く", "Open source": "オープンソース", "Open Source": "オープンソース", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "この環境ではパスキーはサポートされていません", "Passkey is not supported on this device": "このデバイスではパスキーがサポートされていません", "Passkey is not supported on this device.": "このデバイスではパスキーはサポートされていません。", + "Passkey is unavailable": "Passkey は利用できません", "Passkey Login": "Passkeyログイン", "Passkey login failed": "パスキーログインに失敗しました", + "Passkey login is disabled by the administrator.": "管理者によって Passkey ログインが無効化されています。", "Passkey login was cancelled": "パスキーログインがキャンセルされました", "Passkey login was cancelled or timed out": "パスキーログインがキャンセルまたはタイムアウトされました", "Passkey not supported on this device": "このデバイスではパスキーはサポートされていません", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 0c424ad55de2..fd923681c11c 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -3097,6 +3097,7 @@ "Open in new tab": "Открыть в новой вкладке", "Open in New Tab": "Открыть в новой вкладке", "Open menu": "Открыть меню", + "Open Passkey settings": "Открыть настройки Passkey", "Open release": "Открыть выпуск", "Open source": "Открытый исходный код", "Open Source": "Открытый исходный код", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "Passkey не поддерживается в этой среде", "Passkey is not supported on this device": "Passkey не поддерживается на этом устройстве", "Passkey is not supported on this device.": "Passkey не поддерживается на этом устройстве.", + "Passkey is unavailable": "Passkey недоступен", "Passkey Login": "Вход через Passkey", "Passkey login failed": "Ошибка входа с Passkey", + "Passkey login is disabled by the administrator.": "Вход с помощью Passkey отключён администратором.", "Passkey login was cancelled": "Вход с Passkey был отменён", "Passkey login was cancelled or timed out": "Вход с Passkey был отменён или истёк тайм-аут", "Passkey not supported on this device": "Passkey не поддерживается на этом устройстве", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 17fc695a8471..fc881acfcd10 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -3097,6 +3097,7 @@ "Open in new tab": "Mở trong tab mới", "Open in New Tab": "Mở trong tab mới", "Open menu": "Mở menu", + "Open Passkey settings": "Mở cài đặt Passkey", "Open release": "Phát hành mở", "Open source": "Mã nguồn mở", "Open Source": "Mã nguồn mở", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "Passkey không được hỗ trợ trong môi trường này", "Passkey is not supported on this device": "Passkey không được hỗ trợ trên thiết bị này", "Passkey is not supported on this device.": "Khóa truy cập không được hỗ trợ trên thiết bị này.", + "Passkey is unavailable": "Passkey không khả dụng", "Passkey Login": "Đăng nhập bằng khóa truy cập", "Passkey login failed": "Đăng nhập Passkey thất bại", + "Passkey login is disabled by the administrator.": "Quản trị viên đã tắt đăng nhập bằng Passkey.", "Passkey login was cancelled": "Đăng nhập Passkey đã bị hủy", "Passkey login was cancelled or timed out": "Đăng nhập Passkey đã bị hủy hoặc hết thời gian", "Passkey not supported on this device": "Khóa truy cập không được hỗ trợ trên thiết bị này", diff --git a/web/default/src/i18n/locales/zh-TW.json b/web/default/src/i18n/locales/zh-TW.json index 279e4e6e7bec..c67809df7c17 100644 --- a/web/default/src/i18n/locales/zh-TW.json +++ b/web/default/src/i18n/locales/zh-TW.json @@ -3097,6 +3097,7 @@ "Open in new tab": "在新標籤頁中打開", "Open in New Tab": "在新標籤頁中打開", "Open menu": "打開選單", + "Open Passkey settings": "開啟 Passkey 設定", "Open release": "打開版本", "Open source": "開源", "Open Source": "開源項目", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "此環境中不支援 Passkey", "Passkey is not supported on this device": "此設備不支援 Passkey", "Passkey is not supported on this device.": "此設備不支援通行金鑰。", + "Passkey is unavailable": "Passkey 暫時無法使用", "Passkey Login": "Passkey 登入", "Passkey login failed": "Passkey 登入失敗", + "Passkey login is disabled by the administrator.": "管理員已停用 Passkey 登入。", "Passkey login was cancelled": "Passkey 登入已取消", "Passkey login was cancelled or timed out": "Passkey 登入已取消或逾時", "Passkey not supported on this device": "此設備不支援通行金鑰", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 45368da4da94..d4269d8c7368 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -3097,6 +3097,7 @@ "Open in new tab": "在新标签页中打开", "Open in New Tab": "在新标签页中打开", "Open menu": "打开菜单", + "Open Passkey settings": "打开 Passkey 设置", "Open release": "打开版本", "Open source": "开源", "Open Source": "开源项目", @@ -3239,8 +3240,10 @@ "Passkey is not supported in this environment": "此环境中不支持 Passkey", "Passkey is not supported on this device": "此设备不支持 Passkey", "Passkey is not supported on this device.": "此设备不支持通行密钥。", + "Passkey is unavailable": "Passkey 暂不可用", "Passkey Login": "Passkey 登录", "Passkey login failed": "Passkey 登录失败", + "Passkey login is disabled by the administrator.": "管理员已禁用 Passkey 登录。", "Passkey login was cancelled": "Passkey 登录已取消", "Passkey login was cancelled or timed out": "Passkey 登录已取消或超时", "Passkey not supported on this device": "此设备不支持通行密钥",
{t('Passkey Authentication')}
{t('Passkey login is disabled by the administrator.')}