chore: initial project scaffold with admin web, backend, desktop client, and deployment setup

Add monorepo structure for NexaVPN WireGuard control plane including:
- .gitignore for node_modules, build artifacts, and environment files
- README with project overview, monorepo layout, and quick start guide
- Admin web UI with React, Vite, TypeScript, and nginx reverse proxy
- API client with type definitions for users, devices, policies, gateways, and audit logs
- Admin pages for dashboard, users, devices, policies, g
This commit is contained in:
2026-03-15 16:32:34 +01:00
commit 830491cb0d
91 changed files with 5279 additions and 0 deletions

107
desktop-client/src/App.tsx Normal file
View File

@@ -0,0 +1,107 @@
import { FormEvent, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
type EnrollmentState = {
assignedIp: string;
resources: string[];
profileRevision: number;
gatewayEndpoint: 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);
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";
await invoke(command);
setConnected((value) => !value);
}
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>
<div>
<p className="eyebrow">Allowed resources</p>
<ul className="resource-list">
{state.resources.map((resource) => (
<li key={resource}>{resource}</li>
))}
</ul>
</div>
</div>
)}
</div>
);
}