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,37 @@
package gateway
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/nexavpn/nexavpn/backend/internal/apiutil"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
items, err := h.service.List(r.Context())
if err != nil {
apiutil.Error(w, http.StatusInternalServerError, "gateways_list_failed", "unable to list gateways")
return
}
apiutil.JSON(w, http.StatusOK, items)
}
func (h *Handler) SyncBundle(w http.ResponseWriter, r *http.Request) {
bundle, err := h.service.BuildSyncBundle(r.Context(), chi.URLParam(r, "id"))
if err != nil {
apiutil.Error(w, http.StatusBadRequest, "gateway_sync_failed", "unable to build sync bundle")
return
}
apiutil.JSON(w, http.StatusOK, bundle)
}

View File

@@ -0,0 +1,102 @@
package gateway
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/nexavpn/nexavpn/backend/internal/wireguard"
)
type Repository interface {
List(ctx context.Context) ([]Gateway, error)
FirstActive(ctx context.Context) (Gateway, error)
BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) (wireguard.GatewayBundle, error)
}
type PGRepository struct {
db *pgxpool.Pool
}
func NewPGRepository(db *pgxpool.Pool) *PGRepository {
return &PGRepository{db: db}
}
func (r *PGRepository) List(ctx context.Context) ([]Gateway, error) {
rows, err := r.db.Query(ctx, `
select id, name, endpoint, public_key, listen_port, vpn_cidr, dns_servers, is_active
from gateways
where deleted_at is null
order by created_at desc
`)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Gateway
for rows.Next() {
var item Gateway
if err := rows.Scan(&item.ID, &item.Name, &item.Endpoint, &item.PublicKey, &item.ListenPort, &item.VPNCIDR, &item.DNSServers, &item.IsActive); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (r *PGRepository) FirstActive(ctx context.Context) (Gateway, error) {
row := r.db.QueryRow(ctx, `
select id, name, endpoint, public_key, listen_port, vpn_cidr, dns_servers, is_active
from gateways
where deleted_at is null and is_active = true
order by created_at asc
limit 1
`)
var item Gateway
err := row.Scan(&item.ID, &item.Name, &item.Endpoint, &item.PublicKey, &item.ListenPort, &item.VPNCIDR, &item.DNSServers, &item.IsActive)
return item, err
}
func (r *PGRepository) BuildSyncBundle(ctx context.Context, gatewayID uuid.UUID) (wireguard.GatewayBundle, error) {
var bundle wireguard.GatewayBundle
bundle.GatewayID = gatewayID.String()
bundle.Revision = 1
row := r.db.QueryRow(ctx, `
select host(vpn_cidr), listen_port
from gateways
where id = $1 and deleted_at is null
`, gatewayID)
if err := row.Scan(&bundle.Interface.Address, &bundle.Interface.ListenPort); err != nil {
return wireguard.GatewayBundle{}, err
}
rows, err := r.db.Query(ctx, `
select d.id, wp.public_key, host(wp.assigned_ip), coalesce(array_agg(pd.destination::text) filter (where pd.destination is not null), '{}')
from devices d
join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
left join policy_targets pt on pt.target_id = d.id and pt.target_type = 'device'
left join policy_destinations pd on pd.policy_id = pt.policy_id
where d.gateway_id = $1 and d.deleted_at is null and d.status = 'active'
group by d.id, wp.public_key, wp.assigned_ip
`, gatewayID)
if err != nil {
return wireguard.GatewayBundle{}, err
}
defer rows.Close()
for rows.Next() {
var peer wireguard.Peer
var deviceID uuid.UUID
if err := rows.Scan(&deviceID, &peer.PublicKey, &peer.AssignedIP, &peer.AllowedDestinations); err != nil {
return wireguard.GatewayBundle{}, err
}
peer.DeviceID = deviceID.String()
bundle.Peers = append(bundle.Peers, peer)
}
return bundle, rows.Err()
}

View File

@@ -0,0 +1,33 @@
package gateway
import (
"context"
"github.com/google/uuid"
"github.com/nexavpn/nexavpn/backend/internal/wireguard"
)
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List(ctx context.Context) ([]Gateway, error) {
return s.repo.List(ctx)
}
func (s *Service) SelectActive(ctx context.Context) (Gateway, error) {
return s.repo.FirstActive(ctx)
}
func (s *Service) BuildSyncBundle(ctx context.Context, gatewayID string) (wireguard.GatewayBundle, error) {
id, err := uuid.Parse(gatewayID)
if err != nil {
return wireguard.GatewayBundle{}, err
}
return s.repo.BuildSyncBundle(ctx, id)
}

View File

@@ -0,0 +1,14 @@
package gateway
import "github.com/google/uuid"
type Gateway struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
PublicKey string `json:"public_key"`
ListenPort int `json:"listen_port"`
VPNCIDR string `json:"vpn_cidr"`
DNSServers []string `json:"dns_servers"`
IsActive bool `json:"is_active"`
}