Expand README with desktop platform requirements (Windows x86, macOS ARM), helper build commands, gateway utility scripts, and updated local test flow. Add realistic MVP usage section clarifying current platform build status, gateway configuration needs, and admin debug profile behavior with client private key handling.
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
type EnrollmentState = {
|
|
assignedIp: string;
|
|
resources: string[];
|
|
profileRevision: number;
|
|
gatewayEndpoint: string;
|
|
profilePath: string;
|
|
lastSyncTime: string;
|
|
tunnelStrategy: string;
|
|
};
|
|
|
|
export function App() {
|
|
const [serverUrl, setServerUrl] = useState("http://localhost");
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [connected, setConnected] = useState(false);
|
|
const [state, setState] = useState<EnrollmentState | null>(null);
|
|
|
|
useEffect(() => {
|
|
void invoke<EnrollmentState | null>("load_state")
|
|
.then((value) => {
|
|
if (value) {
|
|
setState(value);
|
|
}
|
|
})
|
|
.catch(() => undefined);
|
|
}, []);
|
|
|
|
async function onSubmit(event: FormEvent) {
|
|
event.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await invoke<EnrollmentState>("enroll_device", {
|
|
payload: { serverUrl, username, password }
|
|
});
|
|
setState(result);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Enrollment failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function toggleConnection() {
|
|
const command = connected ? "disconnect_tunnel" : "connect_tunnel";
|
|
try {
|
|
await invoke(command);
|
|
setConnected((value) => !value);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Tunnel action failed");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="client-shell">
|
|
<div className="hero">
|
|
<p className="eyebrow">NexaVPN</p>
|
|
<h1>Private access without manual WireGuard setup.</h1>
|
|
<p className="lede">
|
|
Enter your VPN address, username, and password. NexaVPN provisions and stores the profile for you.
|
|
</p>
|
|
</div>
|
|
|
|
{!state ? (
|
|
<form className="panel" onSubmit={onSubmit}>
|
|
<label>
|
|
VPN server address
|
|
<input value={serverUrl} onChange={(event) => setServerUrl(event.target.value)} />
|
|
</label>
|
|
<label>
|
|
Username
|
|
<input value={username} onChange={(event) => setUsername(event.target.value)} />
|
|
</label>
|
|
<label>
|
|
Password
|
|
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
|
|
</label>
|
|
{error ? <div className="error">{error}</div> : null}
|
|
<button disabled={loading} type="submit">
|
|
{loading ? "Provisioning..." : "Sign in"}
|
|
</button>
|
|
</form>
|
|
) : (
|
|
<div className="panel status">
|
|
<div className="status-row">
|
|
<div>
|
|
<p className="eyebrow">Connection</p>
|
|
<h2>{connected ? "Connected" : "Disconnected"}</h2>
|
|
</div>
|
|
<button onClick={toggleConnection}>{connected ? "Disconnect" : "Connect"}</button>
|
|
</div>
|
|
<div className="details">
|
|
<div>
|
|
<span>Assigned VPN IP</span>
|
|
<strong>{state.assignedIp}</strong>
|
|
</div>
|
|
<div>
|
|
<span>Gateway</span>
|
|
<strong>{state.gatewayEndpoint}</strong>
|
|
</div>
|
|
<div>
|
|
<span>Profile revision</span>
|
|
<strong>{state.profileRevision}</strong>
|
|
</div>
|
|
<div>
|
|
<span>Last sync</span>
|
|
<strong>{state.lastSyncTime}</strong>
|
|
</div>
|
|
</div>
|
|
<div className="details">
|
|
<div>
|
|
<span>Profile path</span>
|
|
<strong>{state.profilePath}</strong>
|
|
</div>
|
|
<div>
|
|
<span>Tunnel strategy</span>
|
|
<strong>{state.tunnelStrategy}</strong>
|
|
</div>
|
|
</div>
|
|
{error ? <div className="error">{error}</div> : null}
|
|
<div>
|
|
<p className="eyebrow">Allowed resources</p>
|
|
<ul className="resource-list">
|
|
{state.resources.map((resource) => (
|
|
<li key={resource}>{resource}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|