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:
parent
800072ffbb
commit
4a4a5a01b3
1 changed files with 173 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue