Add Mesh section to Settings for configuring/testing the mesh gate

Admins can now toggle mesh.required, run verify/override, and see
current mesh status entirely from the app, without hitting the API
directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hu9pZvJY4BgmcQeAw2ugk
This commit is contained in:
Claude 2026-06-20 21:44:27 +00:00
parent 800072ffbb
commit 4a4a5a01b3
No known key found for this signature in database

View file

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { api, ApiError, type Integration, type AuthSession, type LoginEvent, type ManagedUser } from '../lib/api'
import { api, ApiError, type Integration, type AuthSession, type LoginEvent, type ManagedUser, type MeshStatus } from '../lib/api'
import { useAuth } from '../lib/AuthContext'
import {
User,
@ -23,6 +23,7 @@ import {
LogOut,
Users,
UserPlus,
Network,
} from 'lucide-react'
const navSections = [
@ -30,6 +31,7 @@ const navSections = [
{ id: 'appearance', label: 'Appearance', icon: Palette },
{ id: 'security', label: 'Security', icon: Shield },
{ id: 'users', label: 'Users', icon: Users, adminOnly: true },
{ id: 'mesh', label: 'Mesh', icon: Network, adminOnly: true },
{ id: 'integrations', label: 'Integrations', icon: Plug },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'data', label: 'Data & Backup', icon: Database },
@ -1804,11 +1806,181 @@ function UsersSection() {
)
}
function MeshSection() {
const [status, setStatus] = useState<MeshStatus | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [toggling, setToggling] = useState(false)
const [cidr, setCidr] = useState('')
const [testIp, setTestIp] = useState('')
const [verifying, setVerifying] = useState(false)
const [verifyResult, setVerifyResult] = useState<{ ok: boolean; message: string; hostMeshIp: string | null } | null>(null)
const [overriding, setOverriding] = useState(false)
async function load() {
try {
const s = await api.getMeshStatus()
setStatus(s)
setCidr(s.cidr ?? '')
setError('')
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Failed to load mesh status')
} finally {
setLoading(false)
}
}
useEffect(() => {
load()
}, [])
async function handleToggle() {
if (!status) return
setToggling(true)
try {
const s = await api.setMeshRequired(!status.required)
setStatus(s)
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Failed to update mesh requirement')
} finally {
setToggling(false)
}
}
async function handleVerify() {
setError('')
setVerifyResult(null)
setVerifying(true)
try {
const result = await api.verifyMesh(cidr, testIp || undefined)
setVerifyResult(result)
load()
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Failed to verify mesh')
} finally {
setVerifying(false)
}
}
async function handleOverride() {
setOverriding(true)
try {
const s = await api.overrideMesh()
setStatus(s)
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Failed to skip mesh setup')
} finally {
setOverriding(false)
}
}
if (loading) {
return (
<div className="flex flex-col gap-5">
<div style={cardBase}>
<p style={{ fontSize: '12px', color: '#7A7D85' }}>Loading</p>
</div>
</div>
)
}
return (
<div className="flex flex-col gap-5">
<div style={cardBase}>
<div className="flex items-center justify-between" style={{ marginBottom: '14px' }}>
<h3 style={{ ...sectionTitle, marginBottom: 0 }}>Mesh Network Gate</h3>
<div className="flex items-center gap-2">
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{status?.required ? 'Required' : 'Not required'}</span>
<Toggle on={!!status?.required} onClick={handleToggle} />
</div>
</div>
<p style={{ fontSize: '12px', color: '#7A7D85', marginBottom: '14px' }}>
When enabled, admins must verify this host is on a private mesh network (NetBird, WireGuard,
ZeroTier, Tailscale, etc.) before accessing the rest of the app. Members are never blocked they
just see a notice banner until an admin finishes verification.
</p>
{toggling && <p style={{ fontSize: '11px', color: '#7A7D85' }}>Updating</p>}
{status && (
<div
className="flex flex-col gap-1"
style={{ marginBottom: '4px', padding: '12px', borderRadius: '8px', backgroundColor: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.06)' }}
>
<p style={{ fontSize: '11px', color: '#7A7D85' }}>
Verified: <span style={{ color: status.verified ? '#2ECC71' : '#E8E6E0' }}>{status.verified ? 'Yes' : 'No'}</span>
{status.verifiedVia && ` (${status.verifiedVia})`}
</p>
{status.cidr && <p style={{ fontSize: '11px', color: '#7A7D85' }}>CIDR: {status.cidr}</p>}
{status.hostMeshIp && <p style={{ fontSize: '11px', color: '#7A7D85' }}>Host mesh IP: {status.hostMeshIp}</p>}
<p style={{ fontSize: '11px', color: '#7A7D85' }}>
Override: <span style={{ color: status.overridden ? '#E67E22' : '#E8E6E0' }}>{status.overridden ? 'Active (gate skipped)' : 'None'}</span>
</p>
</div>
)}
</div>
<div style={cardBase}>
<h3 style={sectionTitle}>Configure & Verify</h3>
<label style={labelStyle}>Mesh Network CIDR</label>
<input
style={inputStyle}
value={cidr}
onChange={(e) => setCidr(e.target.value)}
placeholder="e.g. 100.64.0.0/10 (NetBird/Tailscale CGNAT range)"
/>
<label style={{ ...labelStyle, marginTop: '12px' }}>Peer/Gateway IP on the mesh (optional)</label>
<input
style={inputStyle}
value={testIp}
onChange={(e) => setTestIp(e.target.value)}
placeholder="e.g. 100.64.0.1 — only needed if this host's own IP isn't in the mesh range"
/>
<p style={{ fontSize: '10px', color: '#5C5F66', marginTop: '6px' }}>
If this host reaches the mesh through routing instead of holding a local mesh IP (e.g. a VPC
peered into the mesh), give us an address on the mesh we can ping to confirm reachability.
</p>
{error && <p style={{ fontSize: '12px', color: '#E74C3C', marginTop: '12px' }}>{error}</p>}
{verifyResult && (
<p style={{ fontSize: '12px', color: verifyResult.ok ? '#2ECC71' : '#E74C3C', marginTop: '12px' }}>
{verifyResult.message}
{verifyResult.hostMeshIp && ` — host mesh IP: ${verifyResult.hostMeshIp}`}
</p>
)}
<div className="flex items-center justify-between" style={{ marginTop: '16px' }}>
<GoldButton onClick={handleVerify} disabled={verifying || !cidr}>
{verifying ? 'Verifying…' : 'Verify Connection'}
</GoldButton>
<button
onClick={handleOverride}
disabled={overriding}
className="cursor-pointer"
style={{
fontSize: '11px',
color: '#7A7D85',
background: 'transparent',
border: '1px solid rgba(255,255,255,0.12)',
borderRadius: '6px',
padding: '8px 14px',
opacity: overriding ? 0.6 : 1,
}}
>
{overriding ? 'Skipping…' : 'Skip for now'}
</button>
</div>
</div>
</div>
)
}
const sectionComponents: Record<string, () => React.ReactElement> = {
profile: ProfileSection,
appearance: AppearanceSection,
security: SecuritySection,
users: UsersSection,
mesh: MeshSection,
integrations: IntegrationsSection,
notifications: NotificationsSection,
data: DataBackupSection,