diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 4ac0d57ef7..a97299aea9 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -2634,7 +2634,7 @@ func (c *Config) Validate() error { u.Host = strings.ToLower(u.Host) host := strings.ToLower(u.Hostname()) localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1" - if u.Scheme != "https" && !(u.Scheme == "http" && localDevelopment) { + if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) { return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin) } if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) { diff --git a/backend/internal/repository/passkey_repo.go b/backend/internal/repository/passkey_repo.go index f834b255dd..d46124e778 100644 --- a/backend/internal/repository/passkey_repo.go +++ b/backend/internal/repository/passkey_repo.go @@ -92,7 +92,7 @@ func (r *passkeyRepository) ListByUserID( if err != nil { return nil, fmt.Errorf("list passkey credentials: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() records := make([]service.PasskeyCredentialRecord, 0) for rows.Next() { diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 06c738cd3c..bbbc25dfd6 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -711,6 +711,10 @@ func TestAPIContracts(t *testing.T) { "frontend_url": "", "totp_enabled": false, "totp_encryption_key_configured": false, + "passkey_enabled": false, + "passkey_configured": false, + "passkey_rp_id": "", + "passkey_rp_origins": [], "session_binding_enabled": false, "step_up_enabled": false, "audit_log_retention_days": 180, @@ -1028,6 +1032,10 @@ func TestAPIContracts(t *testing.T) { "invitation_code_enabled": false, "totp_enabled": false, "totp_encryption_key_configured": false, + "passkey_enabled": false, + "passkey_configured": false, + "passkey_rp_id": "", + "passkey_rp_origins": [], "session_binding_enabled": false, "step_up_enabled": false, "audit_log_retention_days": 180, diff --git a/backend/internal/service/setting_features.go b/backend/internal/service/setting_features.go index c2beefe76a..50a3fbcfd8 100644 --- a/backend/internal/service/setting_features.go +++ b/backend/internal/service/setting_features.go @@ -182,7 +182,7 @@ func (s *SettingService) PasskeyEnabled(ctx context.Context) (bool, error) { } value, err := s.settingRepo.GetValue(ctx, SettingKeyPasskeyEnabled) if errors.Is(err, ErrSettingNotFound) { - return true, nil // preserve enabled deployments when upgrading from config-only releases + return true, nil // configured deployments default to enabled until the admin persists the switch } if err != nil { return false, fmt.Errorf("read passkey setting: %w", err) @@ -206,10 +206,16 @@ func (s *SettingService) passkeyConfigured() bool { return s != nil && s.cfg != nil && s.cfg.WebAuthn.Enabled } +// passkeySettingEnabled must stay ANDed with passkeyConfigured: a stale +// "true" row after the WebAuthn config is removed would otherwise make the +// admin update gate reject every settings save while the UI toggle is locked. func (s *SettingService) passkeySettingEnabled(settings map[string]string) bool { + if !s.passkeyConfigured() { + return false + } value, ok := settings[SettingKeyPasskeyEnabled] if !ok { - return s.passkeyConfigured() + return true } return value == "true" } diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go index e3cbddc09f..b689f56781 100644 --- a/backend/internal/service/setting_service_update_test.go +++ b/backend/internal/service/setting_service_update_test.go @@ -879,3 +879,17 @@ func TestSettingService_PasskeySwitchPersistsAndDefaultsToConfigured(t *testing. require.NoError(t, err) require.False(t, publicSettings.PasskeyEnabled) } + +// 移除 WebAuthn 配置后,残留的 passkey_enabled="true" 不得再让 GetAllSettings +// 报告开关开启:admin 更新门控以此为准,一旦误报为 true 会拒绝所有设置保存, +// 而此时前端开关处于禁用态,管理员无法在 UI 里自救。 +func TestSettingService_StalePasskeyTrueWithoutConfigReportsDisabled(t *testing.T) { + repo := &settingGetAllRepoStub{values: map[string]string{ + SettingKeyPasskeyEnabled: "true", + }} + service := NewSettingService(repo, &config.Config{}) + + settings, err := service.GetAllSettings(context.Background()) + require.NoError(t, err) + require.False(t, settings.PasskeyEnabled) +}