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:
37
backend/internal/gateway/handler.go
Normal file
37
backend/internal/gateway/handler.go
Normal 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)
|
||||
}
|
||||
102
backend/internal/gateway/repository.go
Normal file
102
backend/internal/gateway/repository.go
Normal 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()
|
||||
}
|
||||
33
backend/internal/gateway/service.go
Normal file
33
backend/internal/gateway/service.go
Normal 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)
|
||||
}
|
||||
14
backend/internal/gateway/types.go
Normal file
14
backend/internal/gateway/types.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user