173 lines
6 KiB
TypeScript
173 lines
6 KiB
TypeScript
|
|
import { useState } from 'react'
|
||
|
|
import { Network, ShieldAlert } from 'lucide-react'
|
||
|
|
import { useAuth } from '../lib/AuthContext'
|
||
|
|
import { api, ApiError } from '../lib/api'
|
||
|
|
|
||
|
|
const cardBase: React.CSSProperties = {
|
||
|
|
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||
|
|
border: '1px solid rgba(200, 164, 52, 0.12)',
|
||
|
|
borderRadius: '14px',
|
||
|
|
padding: '32px',
|
||
|
|
}
|
||
|
|
|
||
|
|
const fieldLabel: React.CSSProperties = {
|
||
|
|
fontSize: '11px',
|
||
|
|
color: '#7A7D85',
|
||
|
|
marginBottom: '6px',
|
||
|
|
display: 'block',
|
||
|
|
}
|
||
|
|
|
||
|
|
const fieldInput: React.CSSProperties = {
|
||
|
|
width: '100%',
|
||
|
|
height: '36px',
|
||
|
|
borderRadius: '8px',
|
||
|
|
border: '1px solid rgba(200,164,52,0.12)',
|
||
|
|
backgroundColor: 'rgba(255,255,255,0.03)',
|
||
|
|
color: '#E8E6E0',
|
||
|
|
fontSize: '13px',
|
||
|
|
padding: '0 12px',
|
||
|
|
outline: 'none',
|
||
|
|
}
|
||
|
|
|
||
|
|
const goldButton: React.CSSProperties = {
|
||
|
|
height: '38px',
|
||
|
|
borderRadius: '8px',
|
||
|
|
border: 'none',
|
||
|
|
fontSize: '13px',
|
||
|
|
fontWeight: 600,
|
||
|
|
color: '#0A0B0D',
|
||
|
|
backgroundColor: '#C8A434',
|
||
|
|
boxShadow: '0 0 14px rgba(200,164,52,0.2)',
|
||
|
|
padding: '0 20px',
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function MeshGate() {
|
||
|
|
const { refreshMeshStatus } = useAuth()
|
||
|
|
const [cidr, setCidr] = useState('')
|
||
|
|
const [testIp, setTestIp] = useState('')
|
||
|
|
const [error, setError] = useState<string | null>(null)
|
||
|
|
const [testResult, setTestResult] = useState<{ ok: boolean; message: string; hostMeshIp: string | null } | null>(null)
|
||
|
|
const [submitting, setSubmitting] = useState(false)
|
||
|
|
const [overriding, setOverriding] = useState(false)
|
||
|
|
|
||
|
|
async function handleVerify(e: React.FormEvent) {
|
||
|
|
e.preventDefault()
|
||
|
|
setError(null)
|
||
|
|
setTestResult(null)
|
||
|
|
setSubmitting(true)
|
||
|
|
try {
|
||
|
|
const result = await api.verifyMesh(cidr, testIp || undefined)
|
||
|
|
setTestResult(result)
|
||
|
|
if (result.ok) await refreshMeshStatus()
|
||
|
|
} catch (err) {
|
||
|
|
setError(err instanceof ApiError ? err.message : 'Failed to verify mesh')
|
||
|
|
} finally {
|
||
|
|
setSubmitting(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleOverride() {
|
||
|
|
setOverriding(true)
|
||
|
|
try {
|
||
|
|
await api.overrideMesh()
|
||
|
|
await refreshMeshStatus()
|
||
|
|
} catch (err) {
|
||
|
|
setError(err instanceof ApiError ? err.message : 'Failed to skip mesh setup')
|
||
|
|
} finally {
|
||
|
|
setOverriding(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-screen w-screen items-center justify-center bg-page">
|
||
|
|
<div style={{ width: '420px' }}>
|
||
|
|
<form onSubmit={handleVerify} style={cardBase}>
|
||
|
|
<div className="flex items-center gap-2" style={{ marginBottom: '4px' }}>
|
||
|
|
<Network size={18} color="#C8A434" />
|
||
|
|
<h1
|
||
|
|
style={{
|
||
|
|
fontSize: '18px',
|
||
|
|
fontWeight: 700,
|
||
|
|
letterSpacing: '1px',
|
||
|
|
textTransform: 'uppercase',
|
||
|
|
color: '#C8A434',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Mesh Setup Required
|
||
|
|
</h1>
|
||
|
|
</div>
|
||
|
|
<p style={{ fontSize: '12px', color: '#7A7D85', marginBottom: '24px' }}>
|
||
|
|
ArchNest expects this host to be on a private mesh network before the rest of the app
|
||
|
|
can be configured. Works with any mesh — NetBird, WireGuard, ZeroTier, Tailscale, etc.
|
||
|
|
Enter the mesh's IP range below; we verify by checking this host has an address in it.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<label style={fieldLabel}>Mesh Network CIDR</label>
|
||
|
|
<input
|
||
|
|
style={fieldInput}
|
||
|
|
value={cidr}
|
||
|
|
onChange={(e) => setCidr(e.target.value)}
|
||
|
|
placeholder="e.g. 100.64.0.0/10 (NetBird/Tailscale CGNAT range)"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
|
||
|
|
<label style={{ ...fieldLabel, marginTop: '14px' }}>Peer/Gateway IP on the mesh (optional)</label>
|
||
|
|
<input
|
||
|
|
style={fieldInput}
|
||
|
|
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>}
|
||
|
|
{testResult && (
|
||
|
|
<p style={{ fontSize: '12px', color: testResult.ok ? '#2ECC71' : '#E74C3C', marginTop: '12px' }}>
|
||
|
|
{testResult.message}
|
||
|
|
{testResult.hostMeshIp && ` — this host's mesh IP: ${testResult.hostMeshIp}`}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<button type="submit" disabled={submitting} style={{ ...goldButton, width: '100%', marginTop: '22px', opacity: submitting ? 0.6 : 1 }}>
|
||
|
|
{submitting ? 'Verifying…' : 'Connect & Verify'}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div
|
||
|
|
className="flex items-start gap-2"
|
||
|
|
style={{ marginTop: '20px', paddingTop: '16px', borderTop: '1px solid rgba(255,255,255,0.06)' }}
|
||
|
|
>
|
||
|
|
<ShieldAlert size={14} color="#7A7D85" style={{ flexShrink: 0, marginTop: '1px' }} />
|
||
|
|
<div>
|
||
|
|
<p style={{ fontSize: '11px', color: '#7A7D85', marginBottom: '8px' }}>
|
||
|
|
Mesh provider down, or setting this up later? You can skip this check for now — it stays
|
||
|
|
skipped until you re-enable it from Settings.
|
||
|
|
</p>
|
||
|
|
<button
|
||
|
|
type="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: '6px 12px',
|
||
|
|
opacity: overriding ? 0.6 : 1,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{overriding ? 'Skipping…' : 'Skip for now'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|