feat: HTTPS cert status, IDENTITY_CHANGED wiring, remove stale ip_utils Caddyfile writes
Unit Tests / test (push) Successful in 11m18s
Unit Tests / test (push) Successful in 11m18s
- CaddyManager: add refresh_cert_status() and get_cert_status_fresh() that open a live TLS connection to cell-caddy:443 to read cert expiry; avoids needing a volume mount into the API container - CaddyManager: periodic cert refresh in health_monitor_loop (every 60 cycles) - config.py PUT /api/ddns: publish IDENTITY_CHANGED so CaddyManager regenerates the Caddyfile immediately after any domain/cell_name change — previously the event was never fired from this route - config.py: remove all ip_utils.write_caddyfile() calls; CaddyManager is now the sole authority for Caddyfile generation - app.py: add GET /api/caddy/cert-status route - app.py: add GET /api/egress/status and PUT /api/egress/services/<id>/exit routes - Settings.jsx: display cert status badge (valid/expired/internal/unknown) with expiry date and days-remaining in the domain section - Tests: TestRefreshCertStatus (8 tests), TestDdnsConfigUpdatesFiresIdentityChanged, TestCaddyCertStatusRoute added; fix expired-cert helper to set not_valid_before relative to expiry so it's always earlier Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
ChevronDown, ChevronRight, CheckCircle, XCircle,
|
||||
RefreshCw, Lock, FolderDown, X, Globe, Loader
|
||||
} from 'lucide-react';
|
||||
import { cellAPI, ddnsAPI } from '../services/api';
|
||||
import { cellAPI, ddnsAPI, caddyAPI } from '../services/api';
|
||||
import { PORT_CONFLICT_FIELDS, detectPortConflicts } from '../utils/serviceConfig';
|
||||
|
||||
// ── constants ────────────────────────────────────────────────────────────────
|
||||
@@ -354,6 +354,7 @@ function Settings() {
|
||||
const [ddnsDirty, setDdnsDirty] = useState(false);
|
||||
const [ddnsSaving, setDdnsSaving] = useState(false);
|
||||
const [ddnsRegistering, setDdnsRegistering] = useState(false);
|
||||
const [certStatus, setCertStatus] = useState(null); // {status, expiry, days_remaining}
|
||||
|
||||
// service configs
|
||||
const [serviceConfigs, setServiceConfigs] = useState({});
|
||||
@@ -374,11 +375,13 @@ function Settings() {
|
||||
const loadAll = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [cfgRes, bkRes] = await Promise.all([
|
||||
const [cfgRes, bkRes, certRes] = await Promise.all([
|
||||
cellAPI.getConfig(),
|
||||
cellAPI.listBackups(),
|
||||
caddyAPI.getCertStatus().catch(() => null),
|
||||
]);
|
||||
const cfg = cfgRes.data;
|
||||
if (certRes?.data) setCertStatus(certRes.data);
|
||||
setIdentity({
|
||||
cell_name: cfg.cell_name || '',
|
||||
domain: cfg.domain || '',
|
||||
@@ -937,6 +940,46 @@ function Settings() {
|
||||
: 'Local-only install — no external domain configured.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TLS Certificate Status */}
|
||||
{certStatus && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||||
<div className="text-sm font-medium text-gray-700 mb-2">TLS Certificate</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{certStatus.status === 'valid' && (
|
||||
<CheckCircle className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
)}
|
||||
{certStatus.status === 'expired' && (
|
||||
<XCircle className="h-4 w-4 text-red-500 flex-shrink-0" />
|
||||
)}
|
||||
{(certStatus.status === 'unknown' || certStatus.status === 'internal') && (
|
||||
<Lock className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||
)}
|
||||
<div className="text-sm text-gray-600">
|
||||
{certStatus.status === 'valid' && (
|
||||
<>
|
||||
Valid — expires{' '}
|
||||
<span className="font-mono text-gray-800">
|
||||
{new Date(certStatus.expiry).toLocaleDateString()}
|
||||
</span>
|
||||
{certStatus.days_remaining != null && (
|
||||
<span className={`ml-2 font-medium ${certStatus.days_remaining < 14 ? 'text-amber-600' : 'text-green-700'}`}>
|
||||
({certStatus.days_remaining}d remaining)
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{certStatus.status === 'expired' && (
|
||||
<span className="text-red-600 font-medium">
|
||||
Expired on {new Date(certStatus.expiry).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
{certStatus.status === 'internal' && 'Internal CA certificate (LAN mode)'}
|
||||
{certStatus.status === 'unknown' && 'Certificate not yet issued or Caddy unreachable'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
|
||||
@@ -382,4 +382,9 @@ export const containerAPI = {
|
||||
removeVolume: (name, force = false) => api.delete(`/api/volumes/${name}`, { params: { force } }),
|
||||
};
|
||||
|
||||
// Caddy / TLS API
|
||||
export const caddyAPI = {
|
||||
getCertStatus: () => api.get('/api/caddy/cert-status'),
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user