码桶
发现社区成员的开源项目
SslManagement.jsx20.9 KB
import React, { useState, useEffect, useCallback } from 'react';
import { RefreshCw, Shield, ShieldCheck, ShieldAlert, ShieldOff, ToggleLeft, ToggleRight, ChevronDown, AlertTriangle, CheckCircle } from 'lucide-react';
import TabSkeleton from './TabSkeleton';
const SslManagement = ({ zone, getHeaders, authFetch, t, showToast }) => {
const af = authFetch || fetch;
const [sslMode, setSslMode] = useState(null);
const [alwaysHttps, setAlwaysHttps] = useState(null);
const [minTls, setMinTls] = useState(null);
const [autoRewrites, setAutoRewrites] = useState(null);
const [loading, setLoading] = useState(true);
const [fetchError, setFetchError] = useState(null);
const [updatingSsl, setUpdatingSsl] = useState(false);
const [updatingHttps, setUpdatingHttps] = useState(false);
const [updatingTls, setUpdatingTls] = useState(false);
const [updatingRewrites, setUpdatingRewrites] = useState(false);
const fetchSettings = useCallback(async () => {
setLoading(true);
setFetchError(null);
try {
const res = await af(`/api/zones/${zone.id}/ssl`, { headers: getHeaders() });
const data = await res.json();
if (data.success) {
setSslMode(data.ssl?.value || 'off');
setAlwaysHttps(data.always_use_https?.value || 'off');
setMinTls(data.min_tls_version?.value || '1.0');
setAutoRewrites(data.automatic_https_rewrites?.value || 'off');
} else {
const msg = data.errors?.[0]?.message || data.error || t('errorOccurred');
setFetchError(msg);
}
} catch (e) {
console.error('Failed to fetch SSL settings:', e);
setFetchError(t('errorOccurred'));
}
setLoading(false);
}, [zone.id]);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
const updateSetting = async (setting, value, setUpdating) => {
setUpdating(true);
try {
const res = await af(`/api/zones/${zone.id}/ssl`, {
method: 'POST',
headers: getHeaders(true),
body: JSON.stringify({ setting, value })
});
const data = await res.json();
if (data.success) {
showToast(t('sslUpdateSuccess'));
return true;
} else {
showToast(data.errors?.[0]?.message || t('errorOccurred'), 'error');
return false;
}
} catch (e) {
showToast(t('errorOccurred'), 'error');
return false;
} finally {
setUpdating(false);
}
};
const handleSslModeChange = async (mode) => {
const prev = sslMode;
setSslMode(mode);
const success = await updateSetting('ssl_mode', mode, setUpdatingSsl);
if (!success) setSslMode(prev);
};
const handleAlwaysHttpsToggle = async () => {
const newValue = alwaysHttps === 'on' ? 'off' : 'on';
const prev = alwaysHttps;
setAlwaysHttps(newValue);
const success = await updateSetting('always_use_https', newValue, setUpdatingHttps);
if (!success) setAlwaysHttps(prev);
};
const handleMinTlsChange = async (version) => {
const prev = minTls;
setMinTls(version);
const success = await updateSetting('min_tls_version', version, setUpdatingTls);
if (!success) setMinTls(prev);
};
const handleAutoRewritesToggle = async () => {
const newValue = autoRewrites === 'on' ? 'off' : 'on';
const prev = autoRewrites;
setAutoRewrites(newValue);
const success = await updateSetting('automatic_https_rewrites', newValue, setUpdatingRewrites);
if (!success) setAutoRewrites(prev);
};
const sslModes = [
{
value: 'off',
label: t('sslModeOff'),
description: t('sslModeOffDesc'),
color: '#ef4444',
bgColor: 'rgba(239, 68, 68, 0.08)',
borderColor: 'rgba(239, 68, 68, 0.3)',
icon: <ShieldOff size={20} color="#ef4444" />,
badge: null,
},
{
value: 'flexible',
label: t('sslModeFlexible'),
description: t('sslModeFlexibleDesc'),
color: '#f59e0b',
bgColor: 'rgba(245, 158, 11, 0.08)',
borderColor: 'rgba(245, 158, 11, 0.3)',
icon: <ShieldAlert size={20} color="#f59e0b" />,
badge: null,
},
{
value: 'full',
label: t('sslModeFull'),
description: t('sslModeFullDesc'),
color: '#3b82f6',
bgColor: 'rgba(59, 130, 246, 0.08)',
borderColor: 'rgba(59, 130, 246, 0.3)',
icon: <Shield size={20} color="#3b82f6" />,
badge: null,
},
{
value: 'strict',
label: t('sslModeStrict'),
description: t('sslModeStrictDesc'),
color: '#22c55e',
bgColor: 'rgba(34, 197, 94, 0.08)',
borderColor: 'rgba(34, 197, 94, 0.3)',
icon: <ShieldCheck size={20} color="#22c55e" />,
badge: t('recommended'),
},
];
const tlsVersions = [
{ value: '1.0', label: 'TLS 1.0' },
{ value: '1.1', label: 'TLS 1.1' },
{ value: '1.2', label: 'TLS 1.2', recommended: true },
{ value: '1.3', label: 'TLS 1.3' },
];
if (loading && !fetchError) {
return <TabSkeleton variant="settings" />;
}
if (fetchError) {
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: '6px',
padding: '0.75rem 1rem', borderRadius: '8px',
background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.3)',
fontSize: '0.8125rem', color: 'var(--error, #ef4444)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<AlertTriangle size={16} />
<span>{t('loadSettingsError').replace('{error}', fetchError)}</span>
</div>
{/auth/i.test(fetchError) && (
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{t('tokenPermissionHint')}
</span>
)}
<button
className="btn btn-outline"
onClick={fetchSettings}
style={{ alignSelf: 'flex-start', padding: '4px 12px', fontSize: '0.75rem', marginTop: '4px' }}
>
<RefreshCw size={12} /> {t('refresh') || 'Retry'}
</button>
</div>
);
}
return (
<div className="tab-enter" style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{/* SSL/TLS Mode Selector */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Shield size={16} color="var(--primary)" />
<h3 style={{ fontSize: '0.875rem', margin: 0 }}>{t('sslModeTitle')}</h3>
{updatingSsl && <RefreshCw size={12} className="spin" style={{ color: 'var(--primary)' }} />}
</div>
<button
className="btn btn-outline"
onClick={fetchSettings}
disabled={loading}
style={{ padding: '4px 8px', height: 'auto', fontSize: '0.75rem' }}
>
<RefreshCw size={12} className={loading ? 'spin' : ''} />
</button>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '1rem', lineHeight: 1.5 }}>
{t('sslModeDescription')}
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.75rem' }}>
{sslModes.map((mode) => {
const isActive = sslMode === mode.value;
return (
<button
key={mode.value}
onClick={() => !updatingSsl && handleSslModeChange(mode.value)}
disabled={updatingSsl}
className="unstyled"
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
padding: '1rem',
borderRadius: '10px',
border: `2px solid ${isActive ? mode.color : 'var(--border)'}`,
background: isActive ? mode.bgColor : 'transparent',
cursor: updatingSsl ? 'wait' : 'pointer',
transition: 'all 0.2s',
textAlign: 'left',
width: '100%',
position: 'relative',
opacity: updatingSsl && !isActive ? 0.6 : 1,
}}
>
{isActive && (
<div style={{
position: 'absolute', top: '8px', right: '8px',
}}>
<CheckCircle size={16} color={mode.color} />
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{mode.icon}
<span style={{
fontWeight: 600, fontSize: '0.8125rem',
color: isActive ? mode.color : 'var(--text)',
}}>
{mode.label}
</span>
{mode.badge && (
<span className="badge badge-green" style={{ fontSize: '0.6rem', padding: '1px 6px' }}>
{mode.badge}
</span>
)}
</div>
<span style={{
fontSize: '0.7rem', color: 'var(--text-muted)', lineHeight: 1.5,
}}>
{mode.description}
</span>
{/* Visual diagram */}
<div style={{
display: 'flex', alignItems: 'center', gap: '4px',
fontSize: '0.6rem', color: 'var(--text-muted)',
padding: '0.35rem 0.5rem',
background: 'var(--subtle-bg)',
borderRadius: '6px',
marginTop: '0.25rem',
}}>
<span style={{ fontWeight: 600 }}>{t('sslDiagramBrowser')}</span>
<span style={{ color: mode.value === 'off' ? '#ef4444' : '#22c55e' }}>
{mode.value === 'off' ? '---' : '==='}
</span>
<span style={{ fontWeight: 600 }}>CF</span>
<span style={{
color: mode.value === 'off' || mode.value === 'flexible' ? '#ef4444' : '#22c55e'
}}>
{mode.value === 'off' || mode.value === 'flexible' ? '---' : '==='}
</span>
<span style={{ fontWeight: 600 }}>{t('sslDiagramOrigin')}</span>
{mode.value === 'strict' && (
<CheckCircle size={10} color="#22c55e" style={{ marginLeft: '2px' }} />
)}
</div>
</button>
);
})}
</div>
{sslMode === 'off' && (
<div style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '0.5rem 0.75rem', borderRadius: '6px',
background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)',
marginTop: '0.75rem', fontSize: '0.75rem', color: '#ef4444'
}}>
<AlertTriangle size={14} />
<span>{t('sslOffWarning')}</span>
</div>
)}
</div>
{/* Always Use HTTPS */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '0.5rem' }}>
<h3 style={{ fontSize: '0.875rem', margin: 0 }}>{t('sslAlwaysHttps')}</h3>
{updatingHttps && <RefreshCw size={12} className="spin" style={{ color: 'var(--primary)' }} />}
<span className={`badge ${alwaysHttps === 'on' ? 'badge-green' : 'badge-orange'}`} style={{ fontSize: '0.65rem' }}>
{alwaysHttps === 'on' ? 'ON' : 'OFF'}
</span>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', margin: 0, lineHeight: 1.5 }}>
{t('sslAlwaysHttpsDesc')}
</p>
</div>
<button
className="btn"
onClick={handleAlwaysHttpsToggle}
disabled={updatingHttps}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: alwaysHttps === 'on' ? 'var(--primary)' : 'var(--subtle-bg)',
color: alwaysHttps === 'on' ? 'white' : 'var(--text)',
border: alwaysHttps === 'on' ? 'none' : '1px solid var(--border)',
padding: '0.5rem 1rem', fontSize: '0.8125rem',
minWidth: '80px', justifyContent: 'center',
}}
>
{updatingHttps ? (
<RefreshCw size={14} className="spin" />
) : alwaysHttps === 'on' ? (
<ToggleRight size={18} />
) : (
<ToggleLeft size={18} />
)}
</button>
</div>
</div>
{/* Minimum TLS Version */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '0.5rem' }}>
<h3 style={{ fontSize: '0.875rem', margin: 0 }}>{t('sslMinTls')}</h3>
{updatingTls && <RefreshCw size={12} className="spin" style={{ color: 'var(--primary)' }} />}
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.75rem', lineHeight: 1.5 }}>
{t('sslMinTlsDesc')}
</p>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{tlsVersions.map((ver) => {
const isActive = minTls === ver.value;
return (
<button
key={ver.value}
onClick={() => !updatingTls && handleMinTlsChange(ver.value)}
disabled={updatingTls}
className="btn"
style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '0.5rem 1rem',
fontSize: '0.8125rem',
fontWeight: isActive ? 600 : 400,
background: isActive ? 'var(--primary)' : 'transparent',
color: isActive ? 'white' : 'var(--text)',
border: isActive ? '2px solid var(--primary)' : '1px solid var(--border)',
borderRadius: '8px',
cursor: updatingTls ? 'wait' : 'pointer',
transition: 'all 0.2s',
opacity: updatingTls && !isActive ? 0.6 : 1,
}}
>
{ver.label}
{ver.recommended && (
<span className="badge badge-green" style={{
fontSize: '0.55rem', padding: '1px 4px',
background: isActive ? 'rgba(255,255,255,0.25)' : undefined,
color: isActive ? 'white' : undefined,
}}>
{t('recommended')}
</span>
)}
{isActive && <CheckCircle size={14} />}
</button>
);
})}
</div>
</div>
{/* Automatic HTTPS Rewrites */}
<div className="glass-card" style={{ padding: '1.25rem', background: 'var(--subtle-bg)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '0.5rem' }}>
<h3 style={{ fontSize: '0.875rem', margin: 0 }}>{t('sslAutoRewrites')}</h3>
{updatingRewrites && <RefreshCw size={12} className="spin" style={{ color: 'var(--primary)' }} />}
<span className={`badge ${autoRewrites === 'on' ? 'badge-green' : 'badge-orange'}`} style={{ fontSize: '0.65rem' }}>
{autoRewrites === 'on' ? 'ON' : 'OFF'}
</span>
</div>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', margin: 0, lineHeight: 1.5 }}>
{t('sslAutoRewritesDesc')}
</p>
</div>
<button
className="btn"
onClick={handleAutoRewritesToggle}
disabled={updatingRewrites}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: autoRewrites === 'on' ? 'var(--primary)' : 'var(--subtle-bg)',
color: autoRewrites === 'on' ? 'white' : 'var(--text)',
border: autoRewrites === 'on' ? 'none' : '1px solid var(--border)',
padding: '0.5rem 1rem', fontSize: '0.8125rem',
minWidth: '80px', justifyContent: 'center',
}}
>
{updatingRewrites ? (
<RefreshCw size={14} className="spin" />
) : autoRewrites === 'on' ? (
<ToggleRight size={18} />
) : (
<ToggleLeft size={18} />
)}
</button>
</div>
</div>
</div>
);
};
export default SslManagement;