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

223
docs/api.md Normal file
View File

@@ -0,0 +1,223 @@
# REST API Contract
Base path: `/api/v1`
All responses are JSON.
## Authentication
### `POST /auth/login`
Request:
```json
{
"username": "alice",
"password": "secret-password"
}
```
Response:
```json
{
"access_token": "jwt",
"refresh_token": "opaque-token",
"expires_in": 900,
"user": {
"id": "uuid",
"username": "alice",
"display_name": "Alice",
"role": "admin"
}
}
```
### `POST /auth/refresh`
Request:
```json
{
"refresh_token": "opaque-token"
}
```
### `POST /auth/logout`
Request:
```json
{
"refresh_token": "opaque-token"
}
```
### `GET /auth/me`
Returns the authenticated user and role.
## User Self-Service
### `GET /me/devices`
Returns the current user devices.
### `GET /me/profile`
Returns profile metadata for the current active device when the client needs to resync.
## Device Enrollment and Provisioning
### `POST /devices/enroll`
Request:
```json
{
"name": "Alice MacBook Pro",
"platform": "macos",
"os_version": "14.4",
"app_version": "0.1.0",
"device_fingerprint": "sha256:...",
"public_key": "base64-wireguard-public-key"
}
```
Response:
```json
{
"device": {
"id": "uuid",
"name": "Alice MacBook Pro",
"status": "active"
},
"peer": {
"assigned_ip": "100.96.0.10/32",
"dns_servers": ["10.20.0.53"],
"allowed_ips": ["172.16.10.0/24"],
"gateway": {
"id": "uuid",
"name": "primary-gateway",
"endpoint": "vpn.example.com:51820",
"public_key": "gateway-public-key"
},
"profile_revision": 1
},
"profile": {
"format": "wireguard",
"content": "[Interface]\n..."
},
"resources": [
{
"type": "cidr",
"value": "172.16.10.0/24",
"label": "Private subnet"
}
]
}
```
### `GET /devices/{deviceId}/profile`
Admin-only debug endpoint for rendered config retrieval.
### `POST /devices/{deviceId}/rotate`
Rotates the device profile revision and forces reprovisioning.
### `POST /devices/{deviceId}/revoke`
Revokes the device and removes it from future gateway sync output.
### `POST /devices/{deviceId}/heartbeat`
Optional client status sync for last-seen and runtime metadata.
## Connection Metadata
### `GET /connection/status`
Returns assigned IP, latest sync time, and effective allowed resources for the current authenticated device session.
## Admin: Users
### `GET /admin/users`
### `POST /admin/users`
### `GET /admin/users/{id}`
### `PATCH /admin/users/{id}`
### `POST /admin/users/{id}/disable`
### `POST /admin/users/{id}/enable`
### `POST /admin/users/{id}/password`
## Admin: Devices
### `GET /admin/devices`
### `GET /admin/devices/{id}`
### `PATCH /admin/devices/{id}`
### `POST /admin/devices/{id}/revoke`
### `POST /admin/devices/{id}/rotate`
## Admin: Policies
### `GET /admin/policies`
### `POST /admin/policies`
### `GET /admin/policies/{id}`
### `PATCH /admin/policies/{id}`
### `DELETE /admin/policies/{id}`
Policy create request:
```json
{
"name": "Finance subnet access",
"description": "Access for finance team",
"priority": 100,
"effect": "allow",
"full_tunnel": false,
"destinations": [
"172.16.20.0/24",
"172.16.21.10/32"
],
"targets": [
{
"type": "user",
"id": "uuid"
}
]
}
```
## Admin: Gateways
### `GET /admin/gateways`
### `POST /admin/gateways`
### `GET /admin/gateways/{id}`
### `PATCH /admin/gateways/{id}`
### `GET /admin/gateways/{id}/sync`
The sync endpoint returns the peer and firewall bundle consumed by the gateway helper.
## Admin: Audit
### `GET /admin/audit-logs`
Query params:
- `event_type`
- `entity_type`
- `status`
- `page`
- `page_size`
## Error Format
```json
{
"error": {
"code": "validation_error",
"message": "public_key is required"
}
}
```

180
docs/architecture.md Normal file
View File

@@ -0,0 +1,180 @@
# NexaVPN Architecture
## System Overview
NexaVPN is a self-hosted remote access platform that uses WireGuard for transport and a centralized control plane for identity, device enrollment, provisioning, and policy enforcement.
The platform is split into four major planes:
1. Control plane
- Go REST API
- PostgreSQL
- JWT auth and refresh sessions
- policy engine
- audit logging
- WireGuard profile builder
- gateway state publisher
2. Management plane
- React admin console
- user, device, policy, gateway, and audit workflows
3. Endpoint plane
- Tauri desktop client for Windows and macOS
- local secure token/config storage
- on-device WireGuard keypair generation
- embedded tunnel lifecycle management
4. Data plane
- Linux WireGuard gateway
- nftables policy enforcement
- routed access to protected resources
## Logical Components
### Backend
- `auth`
- username/password login
- access and refresh token issuance
- session tracking
- `user`
- user CRUD
- role assignment
- account enable/disable
- `device`
- device registration
- enrollment lifecycle
- device revocation
- device profile rotation
- `policy`
- user and device policy resolution
- group-aware target model
- allow-list centric MVP with deny precedence reserved in schema
- `gateway`
- gateway inventory
- endpoint metadata
- peer sync bundle generation
- firewall rule translation output
- `profile`
- WireGuard config assembly
- connect metadata response
- `audit`
- immutable security and admin events
- `ipam`
- VPN address pool allocation
- uniqueness and lifecycle tracking
### Admin UI
- Dashboard
- counts, enrollment trend, latest audit events
- Users
- create, edit, disable, password reset
- Devices
- list, revoke, rotate, inspect assigned profile metadata
- Policies
- create CIDR-based allow rules and attach them to users, devices, or groups
- Gateways
- endpoint configuration, sync status, address pool view
- Audit
- searchable event history
- Settings
- DNS defaults, token lifetimes, bootstrap settings
### Desktop Client
- onboarding
- server URL
- username
- password
- enrollment
- machine fingerprint generation
- WireGuard keypair generation on-device
- device registration
- profile provisioning
- runtime
- secure local storage
- connect/disconnect
- status display
- last sync time
- allowed resources display
- diagnostics
- logs
- TLS trust warning surface
- profile refresh retry
### Gateway
- WireGuard interface
- issued peers synced from control plane
- nftables chain generated from effective device policies
- route advertisement limited to assigned resources or full tunnel mode
## Key Design Decisions
### Authentication
- Access tokens are short-lived JWTs.
- Refresh tokens are opaque, hashed in the database, and bound to a session.
- Admin and standard-user authorization is role-based.
### Device Trust
- A device is represented as a durable record linked to a user.
- Clients generate their own WireGuard keypairs.
- Only the public key is stored server-side.
- Device rotation invalidates the old peer and issues a fresh profile revision.
### Policy Model
- Effective access is the union of active allow policies targeted at:
- the user
- the device
- any future groups
- Device-specific policies can narrow or extend user-level access.
- Gateway enforcement is authoritative; the client display is informational.
### WireGuard Provisioning
- The backend returns structured peer metadata and a rendered profile payload.
- The desktop client stores the private key and profile locally.
- The MVP uses an embedded tunnel-management abstraction rather than depending on the standalone WireGuard desktop app.
### Expandability
The schema and package layout reserve room for:
- MFA
- OIDC and SSO
- approval-based enrollment
- group and role expansion
- multiple gateways
- route and posture-aware policies
- richer sync agents at the gateway edge
## Request Flow Summary
### Login and Enrollment
1. User enters server URL, username, and password in the desktop app.
2. Client authenticates against `/api/v1/auth/login`.
3. Client generates a WireGuard keypair and device fingerprint locally.
4. Client registers with `/api/v1/devices/enroll`.
5. Backend resolves policy, allocates IP, selects a gateway, stores the peer, and returns profile metadata.
6. Client stores tokens and WireGuard config securely.
7. Client uses the embedded tunnel manager to create the local profile and expose one-click connect/disconnect.
### Policy Update
1. Admin changes a policy in the web UI.
2. Backend stores the update and writes an audit event.
3. Gateway sync state is recalculated.
4. Gateway rule bundle is regenerated for affected peers.
5. The client sees refreshed allowed resources on next sync.
## Security Posture
- Passwords use Argon2id.
- Refresh tokens are stored hashed.
- Device private keys stay local.
- Audit logs capture auth, enrollment, policy, and admin actions.
- TLS is assumed in production behind a reverse proxy.
- Gateway firewalling enforces allowed destinations independently from the client.

60
docs/deployment.md Normal file
View File

@@ -0,0 +1,60 @@
# Deployment Layout
## Services
- `postgres`
- primary relational database
- `backend`
- Go API and migration runner
- `admin-web`
- static React admin UI served by nginx
- `gateway`
- WireGuard plus nftables helper container or host-managed service
- `reverse-proxy`
- TLS termination and routing
## Docker Compose Networks
- `control`
- backend, postgres, admin-web, reverse-proxy
- `gateway`
- backend and gateway helper communication
## Volume Layout
- postgres data volume
- backend local state volume for dev logs if needed
- gateway config volume for rendered peer sync
## Bootstrap
1. Start PostgreSQL.
2. Run migrations.
3. Start the backend.
4. Seed roles, settings, and the initial admin user.
5. Start the admin UI and reverse proxy.
6. Register the first gateway.
## Example Commands
```bash
cd deploy
cp .env.example .env
docker compose up -d postgres
docker compose up -d backend admin-web reverse-proxy
```
For SQL bootstrap during early MVP testing:
```bash
psql "$DATABASE_URL" -f backend/migrations/000001_init.sql
psql "$DATABASE_URL" -f backend/seed/001_seed.sql
```
## Production Notes
- Terminate TLS at nginx or another reverse proxy.
- Restrict backend and database exposure to private networks.
- Run the gateway with the privileges required for WireGuard and nftables.
- Replace example secrets before deployment.
- Use an external secret manager when available.

76
docs/enrollment.md Normal file
View File

@@ -0,0 +1,76 @@
# Device Enrollment And Auto-Provisioning
## Enrollment Flow
1. The user launches the desktop client.
2. The client shows a minimal form with:
- server URL
- username
- password
3. The client validates the server URL and performs a TLS reachability check.
4. The user submits credentials.
5. The client calls `POST /api/v1/auth/login`.
6. On success, the client securely stores:
- access token
- refresh token
- remembered server URL
7. The client generates a WireGuard keypair locally.
8. The client computes a stable device fingerprint.
9. The client calls `POST /api/v1/devices/enroll`.
10. The backend:
- creates or updates the device record
- selects an active gateway
- allocates a VPN IP
- resolves effective policy destinations
- stores the peer
- renders the WireGuard profile
- emits audit events
11. The client stores the profile locally in secure application storage.
12. The client registers the profile with the local tunnel manager.
13. The post-login view shows:
- connection status
- assigned VPN IP
- allowed resources
- connect/disconnect button
- last sync time
## Private Key Handling
- The WireGuard private key is generated on-device.
- It is never sent to the backend.
- The backend stores only the public key and provisioning metadata.
- The desktop app keeps the private key inside the OS-secured storage boundary where possible.
## Auto-Profile Provisioning Design
The provisioning response returns both structured metadata and a rendered WireGuard config. The client uses the rendered config for immediate compatibility and the structured metadata for UI status.
### Preferred MVP Strategy
Use an embedded tunnel manager inside the Tauri app:
- Windows
- manage the tunnel using a Rust bridge and local service abstraction
- support future integration with WireGuardNT or the official service
- macOS
- manage the tunnel through a privileged helper or Network Extension bridge in a later hardening phase
- MVP keeps the app abstraction stable while platform backends can evolve
This approach keeps the user flow consistent even if the platform-specific implementation differs.
### Fallback Strategy
If native embedded control is not ready on a platform:
- the app still auto-creates the profile locally
- the app exposes a one-click import handoff to the system WireGuard client
- this fallback is clearly labeled as temporary in documentation, not as the intended end state
## Reprovisioning
- Profile rotation increments `profile_revision`.
- On next sync or forced refresh, the client fetches a new profile.
- Revoked devices lose access because:
- the gateway peer is removed
- tokens can be invalidated
- the client marks the local profile unusable

84
docs/folder-structure.md Normal file
View File

@@ -0,0 +1,84 @@
# Folder Structure
```text
NexaVPN/
├── README.md
├── docs/
│ ├── api.md
│ ├── architecture.md
│ ├── deployment.md
│ ├── enrollment.md
│ ├── folder-structure.md
│ ├── gateway.md
│ └── schema.md
├── backend/
│ ├── cmd/api/
│ │ └── main.go
│ ├── internal/
│ │ ├── apiutil/
│ │ ├── app/
│ │ ├── audit/
│ │ ├── auth/
│ │ ├── config/
│ │ ├── db/
│ │ ├── device/
│ │ ├── gateway/
│ │ ├── httpserver/
│ │ ├── ipam/
│ │ ├── policy/
│ │ ├── profile/
│ │ ├── user/
│ │ └── wireguard/
│ ├── migrations/
│ │ └── 000001_init.sql
│ ├── seed/
│ │ └── 001_seed.sql
│ ├── Dockerfile
│ └── go.mod
├── admin-web/
│ ├── src/
│ │ ├── api/
│ │ ├── app/
│ │ ├── components/
│ │ ├── features/
│ │ │ ├── audit/
│ │ │ ├── dashboard/
│ │ │ ├── devices/
│ │ │ ├── gateways/
│ │ │ ├── policies/
│ │ │ ├── settings/
│ │ │ └── users/
│ │ └── styles/
│ ├── Dockerfile
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── desktop-client/
│ ├── src/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ └── styles.css
│ ├── src-tauri/
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ └── main.rs
│ │ ├── build.rs
│ │ ├── Cargo.toml
│ │ └── tauri.conf.json
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── vite.config.ts
└── deploy/
├── .env.example
├── docker-compose.yml
├── nginx/
│ ├── admin.conf
│ └── reverse-proxy.conf
└── scripts/
├── bootstrap-admin.sh
└── gateway-entrypoint.sh
```

69
docs/gateway.md Normal file
View File

@@ -0,0 +1,69 @@
# Gateway Enforcement Strategy
## WireGuard And Firewall Roles
- WireGuard authenticates peers and provides encrypted transport.
- nftables enforces which protected destinations a peer may reach.
- NexaVPN control plane translates policy into gateway-side rules.
## Gateway Sync Bundle
Each gateway receives a generated sync bundle that contains:
- interface settings
- peer list
- peer allowed source address
- destination policy matrix
- DNS metadata
- revision metadata
Example bundle shape:
```json
{
"gateway_id": "uuid",
"revision": 12,
"interface": {
"address": "100.96.0.1/24",
"listen_port": 51820
},
"peers": [
{
"device_id": "uuid",
"public_key": "peer-key",
"assigned_ip": "100.96.0.10/32",
"allowed_destinations": [
"172.16.10.0/24"
]
}
]
}
```
## nftables Model
Recommended model:
1. Accept WireGuard interface input.
2. Map peer source VPN IP to allowed destination CIDRs.
3. Drop traffic from VPN clients to destinations outside their effective allow list.
4. Permit full tunnel peers through explicit default-route policy.
High-level chain logic:
- traffic enters from `wg0`
- source address identifies the device
- destination is matched against generated sets
- allowed traffic is accepted
- unmatched traffic is dropped and optionally logged
## Enforcement Details
- Each device receives a unique VPN IP, which makes firewall mapping deterministic.
- The generated firewall rules are derived from the effective policy union.
- Device revocation removes both the WireGuard peer and its nftables set members.
- Full-tunnel policy expands to `0.0.0.0/0` and `::/0` when enabled in later IPv6 support.
## Multi-Gateway Readiness
The backend stores policies independently from the gateway implementation. Each gateway receives only the peers assigned to it, which keeps multi-gateway expansion straightforward later.

204
docs/schema.md Normal file
View File

@@ -0,0 +1,204 @@
# PostgreSQL Schema
## Core Tables
### `roles`
- `id uuid primary key`
- `name text unique not null`
- `description text not null default ''`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
### `users`
- `id uuid primary key`
- `role_id uuid not null references roles(id)`
- `username citext unique not null`
- `display_name text not null`
- `email citext unique`
- `password_hash text not null`
- `is_active boolean not null default true`
- `last_login_at timestamptz`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
### `sessions`
- `id uuid primary key`
- `user_id uuid not null references users(id)`
- `ip_address inet`
- `user_agent text`
- `last_seen_at timestamptz not null default now()`
- `expires_at timestamptz not null`
- `created_at timestamptz not null default now()`
- `revoked_at timestamptz`
### `refresh_tokens`
- `id uuid primary key`
- `session_id uuid not null references sessions(id)`
- `user_id uuid not null references users(id)`
- `token_hash text not null`
- `expires_at timestamptz not null`
- `created_at timestamptz not null default now()`
- `revoked_at timestamptz`
### `gateways`
- `id uuid primary key`
- `name text unique not null`
- `endpoint text not null`
- `public_key text not null`
- `listen_port integer not null`
- `vpn_cidr cidr not null`
- `dns_servers text[] not null default '{}'`
- `is_active boolean not null default true`
- `last_sync_at timestamptz`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
### `devices`
- `id uuid primary key`
- `user_id uuid not null references users(id)`
- `gateway_id uuid references gateways(id)`
- `name text not null`
- `platform text not null`
- `os_version text not null default ''`
- `app_version text not null default ''`
- `device_fingerprint text not null`
- `public_key text not null`
- `status text not null default 'active'`
- `last_seen_at timestamptz`
- `last_connected_at timestamptz`
- `approved_at timestamptz`
- `revoked_at timestamptz`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
Unique index:
- `(user_id, device_fingerprint)` where `deleted_at is null`
### `wireguard_peers`
- `id uuid primary key`
- `device_id uuid not null references devices(id)`
- `gateway_id uuid not null references gateways(id)`
- `public_key text unique not null`
- `assigned_ip inet not null`
- `preshared_key_ciphertext text`
- `allowed_ips cidr[] not null default '{}'`
- `dns_servers text[] not null default '{}'`
- `profile_revision integer not null default 1`
- `last_profile_issued_at timestamptz`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
### `ip_allocations`
- `id uuid primary key`
- `gateway_id uuid not null references gateways(id)`
- `device_id uuid references devices(id)`
- `address inet not null`
- `status text not null default 'allocated'`
- `allocated_at timestamptz not null default now()`
- `released_at timestamptz`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
Unique indexes:
- `(gateway_id, address)`
- `(device_id)` where `status = 'allocated'`
### `policies`
- `id uuid primary key`
- `name text unique not null`
- `description text not null default ''`
- `priority integer not null default 100`
- `effect text not null default 'allow'`
- `is_active boolean not null default true`
- `full_tunnel boolean not null default false`
- `created_by uuid references users(id)`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
### `policy_targets`
- `id uuid primary key`
- `policy_id uuid not null references policies(id)`
- `target_type text not null`
- `target_id uuid not null`
- `created_at timestamptz not null default now()`
Target types:
- `user`
- `device`
- `group`
### `policy_destinations`
- `id uuid primary key`
- `policy_id uuid not null references policies(id)`
- `destination cidr not null`
- `description text not null default ''`
- `created_at timestamptz not null default now()`
### `groups`
- `id uuid primary key`
- `name text unique not null`
- `description text not null default ''`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz`
### `group_memberships`
- `id uuid primary key`
- `group_id uuid not null references groups(id)`
- `user_id uuid not null references users(id)`
- `created_at timestamptz not null default now()`
### `audit_logs`
- `id uuid primary key`
- `actor_user_id uuid references users(id)`
- `actor_device_id uuid references devices(id)`
- `event_type text not null`
- `entity_type text not null`
- `entity_id uuid`
- `status text not null`
- `ip_address inet`
- `message text not null`
- `metadata jsonb not null default '{}'::jsonb`
- `created_at timestamptz not null default now()`
### `settings`
- `id uuid primary key`
- `category text not null`
- `key text not null`
- `value jsonb not null`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
Unique index:
- `(category, key)`
## Notes
- UUIDs are generated with `gen_random_uuid()`.
- `citext` is used for case-insensitive usernames and emails.
- Soft deletes are enabled where historical traceability matters.
- Group tables are included now so policy resolution can grow without a destructive migration later.