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

View File

@@ -0,0 +1,204 @@
use std::sync::Mutex;
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use rand_core::OsRng;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tauri::State;
use x25519_dalek::{PublicKey, StaticSecret};
struct AppState {
session: Mutex<Option<SessionState>>,
}
#[derive(Debug, Clone)]
struct SessionState {
access_token: String,
refresh_token: String,
server_url: String,
enrollment: EnrollmentResult,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollmentPayload {
server_url: String,
username: String,
password: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct EnrollmentResult {
assigned_ip: String,
resources: Vec<String>,
profile_revision: u32,
gateway_endpoint: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct LoginRequest<'a> {
username: &'a str,
password: &'a str,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LoginResponse {
access_token: String,
refresh_token: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct EnrollRequest<'a> {
name: &'a str,
platform: &'a str,
os_version: &'a str,
app_version: &'a str,
device_fingerprint: String,
public_key: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EnrollResponse {
peer: PeerView,
resources: Vec<ResourceView>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PeerView {
assigned_ip: String,
gateway: GatewayView,
profile_revision: u32,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GatewayView {
endpoint: String,
}
#[derive(Debug, Deserialize)]
struct ResourceView {
value: String,
}
#[tauri::command]
async fn enroll_device(payload: EnrollmentPayload, state: State<'_, AppState>) -> Result<EnrollmentResult, String> {
if payload.server_url.trim().is_empty() || payload.username.trim().is_empty() || payload.password.trim().is_empty() {
return Err("Server URL, username, and password are required".into());
}
let client = Client::builder()
.use_rustls_tls()
.build()
.map_err(|err| err.to_string())?;
let login_response = client
.post(format!("{}/api/v1/auth/login", payload.server_url.trim_end_matches('/')))
.json(&LoginRequest {
username: &payload.username,
password: &payload.password,
})
.send()
.await
.map_err(|err| format!("Login failed: {}", err))?;
if !login_response.status().is_success() {
return Err(format!("Login failed with status {}", login_response.status()));
}
let login = login_response
.json::<LoginResponse>()
.await
.map_err(|err| format!("Unable to decode login response: {}", err))?;
let (private_key, public_key) = generate_keypair();
let enroll_response = client
.post(format!("{}/api/v1/devices/enroll", payload.server_url.trim_end_matches('/')))
.bearer_auth(&login.access_token)
.json(&EnrollRequest {
name: "NexaVPN Desktop",
platform: if cfg!(target_os = "macos") { "macos" } else { "windows" },
os_version: std::env::consts::OS,
app_version: env!("CARGO_PKG_VERSION"),
device_fingerprint: build_fingerprint(&payload.server_url, &payload.username, &public_key),
public_key,
})
.send()
.await
.map_err(|err| format!("Enrollment failed: {}", err))?;
if !enroll_response.status().is_success() {
return Err(format!("Enrollment failed with status {}", enroll_response.status()));
}
let enroll = enroll_response
.json::<EnrollResponse>()
.await
.map_err(|err| format!("Unable to decode enrollment response: {}", err))?;
let result = EnrollmentResult {
assigned_ip: enroll.peer.assigned_ip,
resources: enroll.resources.into_iter().map(|resource| resource.value).collect(),
profile_revision: enroll.peer.profile_revision,
gateway_endpoint: enroll.peer.gateway.endpoint,
};
let mut session = state.session.lock().map_err(|_| "Unable to store client state".to_string())?;
*session = Some(SessionState {
access_token: login.access_token,
refresh_token: login.refresh_token,
server_url: payload.server_url,
enrollment: result.clone(),
});
let _ = private_key;
Ok(result)
}
#[tauri::command]
fn connect_tunnel(state: State<'_, AppState>) -> Result<(), String> {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
if session.is_none() {
return Err("No enrolled profile is available yet".into());
}
Ok(())
}
#[tauri::command]
fn disconnect_tunnel(state: State<'_, AppState>) -> Result<(), String> {
let session = state.session.lock().map_err(|_| "Unable to read client state".to_string())?;
if session.is_none() {
return Err("No active session is available".into());
}
Ok(())
}
fn generate_keypair() -> (String, String) {
let private = StaticSecret::random_from_rng(OsRng);
let public = PublicKey::from(&private);
(
STANDARD_NO_PAD.encode(private.to_bytes()),
STANDARD_NO_PAD.encode(public.to_bytes()),
)
}
fn build_fingerprint(server_url: &str, username: &str, public_key: &str) -> String {
format!("nexavpn:{}:{}:{}", server_url, username, public_key)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(AppState {
session: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![enroll_device, connect_tunnel, disconnect_tunnel])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,3 @@
fn main() {
nexavpn_desktop::run();
}