From d509befa3531a02fed9e7a6147797bf68700a677 Mon Sep 17 00:00:00 2001 From: lqlive Date: Wed, 25 Feb 2026 14:58:22 +0800 Subject: [PATCH] Refactor auth policy forms, add useDebounce hook Extracted JWT and OIDC config sections into separate components for better modularity in AuthenticationPolicyEdit. Added a reusable useDebounce hook and applied it to Certificates, Clusters, and Routes search fields. Introduced a shared form data type for authentication policies. Improved code organization and search performance across list pages. --- web/src/hooks/useDebounce.ts | 12 + .../AuthenticationPolicyEdit.tsx | 401 +----------------- .../JwtBearerConfigForm.tsx | 227 ++++++++++ .../OpenIdConnectConfigForm.tsx | 188 ++++++++ web/src/pages/AuthenticationPolicies/types.ts | 30 ++ web/src/pages/Certificates/index.tsx | 21 +- web/src/pages/Clusters/index.tsx | 21 +- web/src/pages/Routes/index.tsx | 21 +- 8 files changed, 498 insertions(+), 423 deletions(-) create mode 100644 web/src/hooks/useDebounce.ts create mode 100644 web/src/pages/AuthenticationPolicies/JwtBearerConfigForm.tsx create mode 100644 web/src/pages/AuthenticationPolicies/OpenIdConnectConfigForm.tsx create mode 100644 web/src/pages/AuthenticationPolicies/types.ts diff --git a/web/src/hooks/useDebounce.ts b/web/src/hooks/useDebounce.ts new file mode 100644 index 0000000..f1fc1c6 --- /dev/null +++ b/web/src/hooks/useDebounce.ts @@ -0,0 +1,12 @@ +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number = 300): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +} diff --git a/web/src/pages/AuthenticationPolicies/AuthenticationPolicyEdit.tsx b/web/src/pages/AuthenticationPolicies/AuthenticationPolicyEdit.tsx index 2776f16..db37c9b 100644 --- a/web/src/pages/AuthenticationPolicies/AuthenticationPolicyEdit.tsx +++ b/web/src/pages/AuthenticationPolicies/AuthenticationPolicyEdit.tsx @@ -1,33 +1,34 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; -import { ChevronLeftIcon, CheckIcon, PlusIcon, XMarkIcon, KeyIcon, LockClosedIcon } from '@heroicons/react/24/outline'; +import { ChevronLeftIcon, CheckIcon, KeyIcon, LockClosedIcon } from '@heroicons/react/24/outline'; import { Checkbox } from '../../components/Checkbox'; import type { AuthenticationSchemeType } from '../../types'; import { AuthenticationPolicyService } from '../../services/authenticationPolicyService'; +import type { AuthenticationPolicyFormData } from './types'; +import JwtBearerConfigForm from './JwtBearerConfigForm'; +import OpenIdConnectConfigForm from './OpenIdConnectConfigForm'; const AuthenticationPolicyEdit: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const isEdit = id && id !== 'new'; - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ name: '', description: '', - type: '' as AuthenticationSchemeType | '', + type: '', enabled: true, - // JWT Bearer fields jwtAuthority: '', jwtAudience: '', jwtRequireHttpsMetadata: true, jwtSaveToken: false, - jwtValidIssuers: [] as string[], - jwtValidAudiences: [] as string[], + jwtValidIssuers: [], + jwtValidAudiences: [], jwtValidateIssuer: true, jwtValidateAudience: true, jwtValidateLifetime: true, jwtValidateIssuerSigningKey: true, jwtClockSkew: '', - // OpenID Connect fields oidcAuthority: '', oidcClientId: '', oidcClientSecret: '', @@ -35,13 +36,10 @@ const AuthenticationPolicyEdit: React.FC = () => { oidcRequireHttpsMetadata: true, oidcSaveTokens: true, oidcGetClaimsFromUserInfoEndpoint: true, - oidcScope: [] as string[], + oidcScope: [], oidcClockSkew: '', }); - const [validIssuerInput, setValidIssuerInput] = useState(''); - const [validAudienceInput, setValidAudienceInput] = useState(''); - const [scopeInput, setScopeInput] = useState(''); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); @@ -151,56 +149,6 @@ const AuthenticationPolicyEdit: React.FC = () => { }; - const addValidIssuer = () => { - if (validIssuerInput.trim() && !formData.jwtValidIssuers.includes(validIssuerInput.trim())) { - setFormData(prev => ({ - ...prev, - jwtValidIssuers: [...prev.jwtValidIssuers, validIssuerInput.trim()] - })); - setValidIssuerInput(''); - } - }; - - const removeValidIssuer = (issuer: string) => { - setFormData(prev => ({ - ...prev, - jwtValidIssuers: prev.jwtValidIssuers.filter(i => i !== issuer) - })); - }; - - const addValidAudience = () => { - if (validAudienceInput.trim() && !formData.jwtValidAudiences.includes(validAudienceInput.trim())) { - setFormData(prev => ({ - ...prev, - jwtValidAudiences: [...prev.jwtValidAudiences, validAudienceInput.trim()] - })); - setValidAudienceInput(''); - } - }; - - const removeValidAudience = (audience: string) => { - setFormData(prev => ({ - ...prev, - jwtValidAudiences: prev.jwtValidAudiences.filter(a => a !== audience) - })); - }; - - const addScope = () => { - if (scopeInput.trim() && !formData.oidcScope.includes(scopeInput.trim())) { - setFormData(prev => ({ - ...prev, - oidcScope: [...prev.oidcScope, scopeInput.trim()] - })); - setScopeInput(''); - } - }; - - const removeScope = (scope: string) => { - setFormData(prev => ({ - ...prev, - oidcScope: prev.oidcScope.filter(s => s !== scope) - })); - }; if (loading) { return ( @@ -337,339 +285,12 @@ const AuthenticationPolicyEdit: React.FC = () => { - {/* JWT Bearer Configuration */} {formData.type === 'JwtBearer' && ( -
-
- -

JWT Bearer Configuration

-
- -
-
- - setFormData(prev => ({ ...prev, jwtAuthority: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="https://your-auth-server.com" - /> -
- -
- - setFormData(prev => ({ ...prev, jwtAudience: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="your-api-audience" - /> -
- -
- -
- setValidIssuerInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addValidIssuer())} - className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="https://issuer.example.com" - /> - -
- {formData.jwtValidIssuers.length > 0 && ( -
- {formData.jwtValidIssuers.map((issuer, index) => ( - - {issuer} - - - ))} -
- )} -
- -
- -
- setValidAudienceInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addValidAudience())} - className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="audience-value" - /> - -
- {formData.jwtValidAudiences.length > 0 && ( -
- {formData.jwtValidAudiences.map((audience, index) => ( - - {audience} - - - ))} -
- )} -
- -
- - setFormData(prev => ({ ...prev, jwtClockSkew: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="300" - /> -
- -
- -
- setFormData(prev => ({ ...prev, jwtRequireHttpsMetadata: checked }))} - label="Require HTTPS metadata" - /> - setFormData(prev => ({ ...prev, jwtSaveToken: checked }))} - label="Save token" - /> - setFormData(prev => ({ ...prev, jwtValidateIssuer: checked }))} - label="Validate issuer" - /> - setFormData(prev => ({ ...prev, jwtValidateAudience: checked }))} - label="Validate audience" - /> - setFormData(prev => ({ ...prev, jwtValidateLifetime: checked }))} - label="Validate lifetime" - /> - setFormData(prev => ({ ...prev, jwtValidateIssuerSigningKey: checked }))} - label="Validate signing key" - /> -
-
-
-
+ )} - {/* OpenID Connect Configuration */} {formData.type === 'OpenIdConnect' && ( -
-
- -

OpenID Connect Configuration

-
- -
-
- - setFormData(prev => ({ ...prev, oidcAuthority: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="https://your-identity-provider.com" - /> -
- -
- - setFormData(prev => ({ ...prev, oidcClientId: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="your-client-id" - /> -
- -
- - setFormData(prev => ({ ...prev, oidcClientSecret: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="your-client-secret" - /> -
- -
- - -
- -
- -
- setScopeInput(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addScope())} - className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="openid, profile, email" - /> - -
- {formData.oidcScope.length > 0 && ( -
- {formData.oidcScope.map((scope, index) => ( - - {scope} - - - ))} -
- )} -
- -
- - setFormData(prev => ({ ...prev, oidcClockSkew: e.target.value }))} - className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" - placeholder="300" - /> -
- -
- -
- setFormData(prev => ({ ...prev, oidcRequireHttpsMetadata: checked }))} - label="Require HTTPS metadata" - /> - setFormData(prev => ({ ...prev, oidcSaveTokens: checked }))} - label="Save tokens" - /> -
- setFormData(prev => ({ ...prev, oidcGetClaimsFromUserInfoEndpoint: checked }))} - label="Get claims from user info endpoint" - /> -
-
-
-
-
+ )} {/* Actions */} diff --git a/web/src/pages/AuthenticationPolicies/JwtBearerConfigForm.tsx b/web/src/pages/AuthenticationPolicies/JwtBearerConfigForm.tsx new file mode 100644 index 0000000..48f2f22 --- /dev/null +++ b/web/src/pages/AuthenticationPolicies/JwtBearerConfigForm.tsx @@ -0,0 +1,227 @@ +import React, { useState } from 'react'; +import { PlusIcon, XMarkIcon, KeyIcon } from '@heroicons/react/24/outline'; +import { Checkbox } from '../../components/Checkbox'; +import type { AuthenticationPolicyFormData } from './types'; + +interface JwtBearerConfigFormProps { + formData: AuthenticationPolicyFormData; + setFormData: React.Dispatch>; +} + +const JwtBearerConfigForm: React.FC = ({ formData, setFormData }) => { + const [validIssuerInput, setValidIssuerInput] = useState(''); + const [validAudienceInput, setValidAudienceInput] = useState(''); + + const addValidIssuer = () => { + if (validIssuerInput.trim() && !formData.jwtValidIssuers.includes(validIssuerInput.trim())) { + setFormData(prev => ({ + ...prev, + jwtValidIssuers: [...prev.jwtValidIssuers, validIssuerInput.trim()] + })); + setValidIssuerInput(''); + } + }; + + const removeValidIssuer = (issuer: string) => { + setFormData(prev => ({ + ...prev, + jwtValidIssuers: prev.jwtValidIssuers.filter(i => i !== issuer) + })); + }; + + const addValidAudience = () => { + if (validAudienceInput.trim() && !formData.jwtValidAudiences.includes(validAudienceInput.trim())) { + setFormData(prev => ({ + ...prev, + jwtValidAudiences: [...prev.jwtValidAudiences, validAudienceInput.trim()] + })); + setValidAudienceInput(''); + } + }; + + const removeValidAudience = (audience: string) => { + setFormData(prev => ({ + ...prev, + jwtValidAudiences: prev.jwtValidAudiences.filter(a => a !== audience) + })); + }; + + return ( +
+
+ +

JWT Bearer Configuration

+
+ +
+
+ + setFormData(prev => ({ ...prev, jwtAuthority: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="https://your-auth-server.com" + /> +
+ +
+ + setFormData(prev => ({ ...prev, jwtAudience: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="your-api-audience" + /> +
+ +
+ +
+ setValidIssuerInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addValidIssuer())} + className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="https://issuer.example.com" + /> + +
+ {formData.jwtValidIssuers.length > 0 && ( +
+ {formData.jwtValidIssuers.map((issuer, index) => ( + + {issuer} + + + ))} +
+ )} +
+ +
+ +
+ setValidAudienceInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addValidAudience())} + className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="audience-value" + /> + +
+ {formData.jwtValidAudiences.length > 0 && ( +
+ {formData.jwtValidAudiences.map((audience, index) => ( + + {audience} + + + ))} +
+ )} +
+ +
+ + setFormData(prev => ({ ...prev, jwtClockSkew: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="300" + /> +
+ +
+ +
+ setFormData(prev => ({ ...prev, jwtRequireHttpsMetadata: checked }))} + label="Require HTTPS metadata" + /> + setFormData(prev => ({ ...prev, jwtSaveToken: checked }))} + label="Save token" + /> + setFormData(prev => ({ ...prev, jwtValidateIssuer: checked }))} + label="Validate issuer" + /> + setFormData(prev => ({ ...prev, jwtValidateAudience: checked }))} + label="Validate audience" + /> + setFormData(prev => ({ ...prev, jwtValidateLifetime: checked }))} + label="Validate lifetime" + /> + setFormData(prev => ({ ...prev, jwtValidateIssuerSigningKey: checked }))} + label="Validate signing key" + /> +
+
+
+
+ ); +}; + +export default JwtBearerConfigForm; diff --git a/web/src/pages/AuthenticationPolicies/OpenIdConnectConfigForm.tsx b/web/src/pages/AuthenticationPolicies/OpenIdConnectConfigForm.tsx new file mode 100644 index 0000000..4467f13 --- /dev/null +++ b/web/src/pages/AuthenticationPolicies/OpenIdConnectConfigForm.tsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import { PlusIcon, XMarkIcon, LockClosedIcon } from '@heroicons/react/24/outline'; +import { Checkbox } from '../../components/Checkbox'; +import type { AuthenticationPolicyFormData } from './types'; + +interface OpenIdConnectConfigFormProps { + formData: AuthenticationPolicyFormData; + setFormData: React.Dispatch>; +} + +const OpenIdConnectConfigForm: React.FC = ({ formData, setFormData }) => { + const [scopeInput, setScopeInput] = useState(''); + + const addScope = () => { + if (scopeInput.trim() && !formData.oidcScope.includes(scopeInput.trim())) { + setFormData(prev => ({ + ...prev, + oidcScope: [...prev.oidcScope, scopeInput.trim()] + })); + setScopeInput(''); + } + }; + + const removeScope = (scope: string) => { + setFormData(prev => ({ + ...prev, + oidcScope: prev.oidcScope.filter(s => s !== scope) + })); + }; + + return ( +
+
+ +

OpenID Connect Configuration

+
+ +
+
+ + setFormData(prev => ({ ...prev, oidcAuthority: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="https://your-identity-provider.com" + /> +
+ +
+ + setFormData(prev => ({ ...prev, oidcClientId: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="your-client-id" + /> +
+ +
+ + setFormData(prev => ({ ...prev, oidcClientSecret: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="your-client-secret" + /> +
+ +
+ + +
+ +
+ +
+ setScopeInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addScope())} + className="block flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="openid, profile, email" + /> + +
+ {formData.oidcScope.length > 0 && ( +
+ {formData.oidcScope.map((scope, index) => ( + + {scope} + + + ))} +
+ )} +
+ +
+ + setFormData(prev => ({ ...prev, oidcClockSkew: e.target.value }))} + className="block w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-gray-400 focus:ring-1 focus:ring-gray-400 transition-colors" + placeholder="300" + /> +
+ +
+ +
+ setFormData(prev => ({ ...prev, oidcRequireHttpsMetadata: checked }))} + label="Require HTTPS metadata" + /> + setFormData(prev => ({ ...prev, oidcSaveTokens: checked }))} + label="Save tokens" + /> +
+ setFormData(prev => ({ ...prev, oidcGetClaimsFromUserInfoEndpoint: checked }))} + label="Get claims from user info endpoint" + /> +
+
+
+
+
+ ); +}; + +export default OpenIdConnectConfigForm; diff --git a/web/src/pages/AuthenticationPolicies/types.ts b/web/src/pages/AuthenticationPolicies/types.ts new file mode 100644 index 0000000..78109f3 --- /dev/null +++ b/web/src/pages/AuthenticationPolicies/types.ts @@ -0,0 +1,30 @@ +import type { AuthenticationSchemeType } from '../../types'; + +export interface AuthenticationPolicyFormData { + name: string; + description: string; + type: AuthenticationSchemeType | ''; + enabled: boolean; + // JWT Bearer fields + jwtAuthority: string; + jwtAudience: string; + jwtRequireHttpsMetadata: boolean; + jwtSaveToken: boolean; + jwtValidIssuers: string[]; + jwtValidAudiences: string[]; + jwtValidateIssuer: boolean; + jwtValidateAudience: boolean; + jwtValidateLifetime: boolean; + jwtValidateIssuerSigningKey: boolean; + jwtClockSkew: string; + // OpenID Connect fields + oidcAuthority: string; + oidcClientId: string; + oidcClientSecret: string; + oidcResponseType: string; + oidcRequireHttpsMetadata: boolean; + oidcSaveTokens: boolean; + oidcGetClaimsFromUserInfoEndpoint: boolean; + oidcScope: string[]; + oidcClockSkew: string; +} diff --git a/web/src/pages/Certificates/index.tsx b/web/src/pages/Certificates/index.tsx index f0f2fc1..0f99470 100644 --- a/web/src/pages/Certificates/index.tsx +++ b/web/src/pages/Certificates/index.tsx @@ -9,20 +9,22 @@ import { } from '@heroicons/react/24/outline'; import type { Certificate } from '../../types'; import { CertificateService } from '../../services/certificateService'; +import { useDebounce } from '../../hooks/useDebounce'; const Certificates: React.FC = () => { const [loading, setLoading] = useState(true); const [certificates, setCertificates] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm); const [error, setError] = useState(null); const [deletingId, setDeletingId] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ id: string; name: string } | null>(null); - const loadCertificates = async () => { + const loadCertificates = async (search: string = '') => { try { setLoading(true); setError(null); - const data = await CertificateService.getCertificates(searchTerm); + const data = await CertificateService.getCertificates(search); setCertificates(data); } catch (err) { console.error('Failed to load certificates:', err); @@ -37,19 +39,16 @@ const Certificates: React.FC = () => { }, []); useEffect(() => { - const timer = setTimeout(() => { - if (searchTerm !== '') { - loadCertificates(); - } - }, 300); - return () => clearTimeout(timer); - }, [searchTerm]); + if (debouncedSearchTerm !== '') { + loadCertificates(debouncedSearchTerm); + } + }, [debouncedSearchTerm]); const handleDelete = async (id: string) => { try { setDeletingId(id); await CertificateService.deleteCertificate(id); - await loadCertificates(); + await loadCertificates(searchTerm); setDeleteConfirm(null); } catch (err) { console.error('Failed to delete certificate:', err); @@ -115,7 +114,7 @@ const Certificates: React.FC = () => {

Failed to load certificates

Please check your connection and try again