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:
176
backend/internal/device/handler.go
Normal file
176
backend/internal/device/handler.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nexavpn/nexavpn/backend/internal/apiutil"
|
||||
"github.com/nexavpn/nexavpn/backend/internal/audit"
|
||||
"github.com/nexavpn/nexavpn/backend/internal/httpserver"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
audit *audit.Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service, auditService *audit.Service) *Handler {
|
||||
return &Handler{service: service, audit: auditService}
|
||||
}
|
||||
|
||||
func (h *Handler) Enroll(w http.ResponseWriter, r *http.Request) {
|
||||
var input EnrollRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := httpserver.MustUserID(r.Context())
|
||||
if !ok {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "missing auth claims")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.Enroll(r.Context(), userID, input, "__CLIENT_GENERATED_PRIVATE_KEY__")
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "device_enroll_failed", "unable to enroll device")
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.audit.Record(r.Context(), audit.Entry{
|
||||
ActorUserID: &userID,
|
||||
EntityType: "device",
|
||||
EntityID: &response.Device.ID,
|
||||
EventType: "device.enrolled",
|
||||
Status: "success",
|
||||
Message: "device enrolled and profile issued",
|
||||
Metadata: map[string]any{
|
||||
"platform": response.Device.Platform,
|
||||
"assigned_ip": response.Peer.AssignedIP,
|
||||
},
|
||||
})
|
||||
apiutil.JSON(w, http.StatusCreated, response)
|
||||
}
|
||||
|
||||
func (h *Handler) ListOwn(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := httpserver.MustUserID(r.Context())
|
||||
if !ok {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "missing auth claims")
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := h.service.ListByUser(r.Context(), userID)
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "devices_list_failed", "unable to list devices")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, devices)
|
||||
}
|
||||
|
||||
func (h *Handler) ListAll(w http.ResponseWriter, r *http.Request) {
|
||||
devices, err := h.service.ListAll(r.Context())
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "devices_list_failed", "unable to list devices")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, devices)
|
||||
}
|
||||
|
||||
func (h *Handler) ConnectionStatus(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := httpserver.MustUserID(r.Context())
|
||||
if !ok {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "missing auth claims")
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.service.GetConnectionStatus(r.Context(), userID)
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "connection_status_failed", "unable to fetch connection status")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, status)
|
||||
}
|
||||
|
||||
func (h *Handler) GetOwnProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := httpserver.MustUserID(r.Context())
|
||||
if !ok {
|
||||
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "missing auth claims")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetLatestEnrollmentByUser(r.Context(), userID)
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusNotFound, "profile_not_found", "no active profile found")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) GetProfileByDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusBadRequest, "invalid_device_id", "invalid device id")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetEnrollmentByDeviceID(r.Context(), deviceID)
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusNotFound, "profile_not_found", "device profile not found")
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) Revoke(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusBadRequest, "invalid_device_id", "invalid device id")
|
||||
return
|
||||
}
|
||||
if err := h.service.Revoke(r.Context(), deviceID); err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "device_revoke_failed", "unable to revoke device")
|
||||
return
|
||||
}
|
||||
if claims, ok := httpserver.ClaimsFromContext(r.Context()); ok {
|
||||
_ = h.audit.Record(r.Context(), audit.Entry{
|
||||
ActorUserID: &claims.UserID,
|
||||
EntityType: "device",
|
||||
EntityID: &deviceID,
|
||||
EventType: "admin.device.revoked",
|
||||
Status: "success",
|
||||
Message: "admin revoked device",
|
||||
})
|
||||
}
|
||||
apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (h *Handler) Rotate(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusBadRequest, "invalid_device_id", "invalid device id")
|
||||
return
|
||||
}
|
||||
if err := h.service.Rotate(r.Context(), deviceID); err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "device_rotate_failed", "unable to rotate device profile")
|
||||
return
|
||||
}
|
||||
if claims, ok := httpserver.ClaimsFromContext(r.Context()); ok {
|
||||
_ = h.audit.Record(r.Context(), audit.Entry{
|
||||
ActorUserID: &claims.UserID,
|
||||
EntityType: "device",
|
||||
EntityID: &deviceID,
|
||||
EventType: "admin.device.rotated",
|
||||
Status: "success",
|
||||
Message: "admin rotated device profile",
|
||||
})
|
||||
}
|
||||
apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
265
backend/internal/device/repository.go
Normal file
265
backend/internal/device/repository.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Enroll(ctx context.Context, userID uuid.UUID, gatewayID uuid.UUID, input EnrollRequest, assignedIP string, dnsServers []string, allowedIPs []string) (EnrollmentResponse, error)
|
||||
ListByUser(ctx context.Context, userID uuid.UUID) ([]Device, error)
|
||||
ListAll(ctx context.Context) ([]Device, error)
|
||||
GetLatestEnrollmentByUser(ctx context.Context, userID uuid.UUID) (EnrollmentResponse, error)
|
||||
GetEnrollmentByDeviceID(ctx context.Context, deviceID uuid.UUID) (EnrollmentResponse, error)
|
||||
Revoke(ctx context.Context, deviceID uuid.UUID) error
|
||||
Rotate(ctx context.Context, deviceID uuid.UUID) error
|
||||
}
|
||||
|
||||
type PGRepository struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewPGRepository(db *pgxpool.Pool) *PGRepository {
|
||||
return &PGRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PGRepository) Enroll(ctx context.Context, userID uuid.UUID, gatewayID uuid.UUID, input EnrollRequest, assignedIP string, dnsServers []string, allowedIPs []string) (EnrollmentResponse, error) {
|
||||
tx, err := r.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
deviceID := uuid.New()
|
||||
peerID := uuid.New()
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
insert into devices (
|
||||
id, user_id, gateway_id, name, platform, os_version, app_version, device_fingerprint, public_key, status, approved_at
|
||||
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'active', now())
|
||||
`, deviceID, userID, gatewayID, input.Name, input.Platform, input.OSVersion, input.AppVersion, input.DeviceFingerprint, input.PublicKey)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
insert into wireguard_peers (
|
||||
id, device_id, gateway_id, public_key, assigned_ip, allowed_ips, dns_servers, last_profile_issued_at
|
||||
) values ($1, $2, $3, $4, $5::inet, $6::cidr[], $7::text[], now())
|
||||
`, peerID, deviceID, gatewayID, input.PublicKey, assignedIP, allowedIPs, dnsServers)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
insert into ip_allocations (id, gateway_id, device_id, address, status)
|
||||
values ($1, $2, $3, $4::inet, 'allocated')
|
||||
`, uuid.New(), gatewayID, deviceID, assignedIP)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
return EnrollmentResponse{
|
||||
Device: Device{
|
||||
ID: deviceID,
|
||||
UserID: userID,
|
||||
GatewayID: gatewayID,
|
||||
Name: input.Name,
|
||||
Platform: input.Platform,
|
||||
Status: "active",
|
||||
AssignedIP: assignedIP,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetLatestEnrollmentByUser(ctx context.Context, userID uuid.UUID) (EnrollmentResponse, error) {
|
||||
row := r.db.QueryRow(ctx, `
|
||||
select
|
||||
d.id,
|
||||
d.user_id,
|
||||
d.gateway_id,
|
||||
d.name,
|
||||
d.platform,
|
||||
d.status,
|
||||
host(wp.assigned_ip),
|
||||
wp.profile_revision,
|
||||
wp.last_profile_issued_at,
|
||||
g.name,
|
||||
g.endpoint,
|
||||
g.public_key,
|
||||
wp.dns_servers,
|
||||
wp.allowed_ips
|
||||
from devices d
|
||||
join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
|
||||
join gateways g on g.id = wp.gateway_id
|
||||
where d.user_id = $1 and d.deleted_at is null
|
||||
order by d.created_at desc
|
||||
limit 1
|
||||
`, userID)
|
||||
return scanEnrollmentRow(row)
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetEnrollmentByDeviceID(ctx context.Context, deviceID uuid.UUID) (EnrollmentResponse, error) {
|
||||
row := r.db.QueryRow(ctx, `
|
||||
select
|
||||
d.id,
|
||||
d.user_id,
|
||||
d.gateway_id,
|
||||
d.name,
|
||||
d.platform,
|
||||
d.status,
|
||||
host(wp.assigned_ip),
|
||||
wp.profile_revision,
|
||||
wp.last_profile_issued_at,
|
||||
g.name,
|
||||
g.endpoint,
|
||||
g.public_key,
|
||||
wp.dns_servers,
|
||||
wp.allowed_ips
|
||||
from devices d
|
||||
join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
|
||||
join gateways g on g.id = wp.gateway_id
|
||||
where d.id = $1 and d.deleted_at is null
|
||||
`, deviceID)
|
||||
return scanEnrollmentRow(row)
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]Device, error) {
|
||||
rows, err := r.db.Query(ctx, `
|
||||
select d.id, d.user_id, d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
|
||||
from devices d
|
||||
left join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
|
||||
where d.user_id = $1 and d.deleted_at is null
|
||||
order by d.created_at desc
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Device
|
||||
for rows.Next() {
|
||||
var item Device
|
||||
if err := rows.Scan(&item.ID, &item.UserID, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListAll(ctx context.Context) ([]Device, error) {
|
||||
rows, err := r.db.Query(ctx, `
|
||||
select d.id, d.user_id, d.gateway_id, d.name, d.platform, d.status, coalesce(host(wp.assigned_ip), '')
|
||||
from devices d
|
||||
left join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null
|
||||
where d.deleted_at is null
|
||||
order by d.created_at desc
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Device
|
||||
for rows.Next() {
|
||||
var item Device
|
||||
if err := rows.Scan(&item.ID, &item.UserID, &item.GatewayID, &item.Name, &item.Platform, &item.Status, &item.AssignedIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) Revoke(ctx context.Context, deviceID uuid.UUID) error {
|
||||
tx, err := r.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if _, err := tx.Exec(ctx, `update devices set status = 'revoked', revoked_at = now(), updated_at = now() where id = $1`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `update wireguard_peers set deleted_at = now(), updated_at = now() where device_id = $1 and deleted_at is null`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `update ip_allocations set status = 'released', released_at = now(), updated_at = now() where device_id = $1 and status = 'allocated'`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *PGRepository) Rotate(ctx context.Context, deviceID uuid.UUID) error {
|
||||
_, err := r.db.Exec(ctx, `
|
||||
update wireguard_peers
|
||||
set profile_revision = profile_revision + 1, last_profile_issued_at = now(), updated_at = now()
|
||||
where device_id = $1 and deleted_at is null
|
||||
`, deviceID)
|
||||
return err
|
||||
}
|
||||
|
||||
type enrollmentRowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanEnrollmentRow(row enrollmentRowScanner) (EnrollmentResponse, error) {
|
||||
var response EnrollmentResponse
|
||||
var profileRevision int
|
||||
var lastIssuedAt *time.Time
|
||||
var gatewayName string
|
||||
var gatewayEndpoint string
|
||||
var gatewayPublicKey string
|
||||
var dnsServers []string
|
||||
var allowedIPs []string
|
||||
|
||||
if err := row.Scan(
|
||||
&response.Device.ID,
|
||||
&response.Device.UserID,
|
||||
&response.Device.GatewayID,
|
||||
&response.Device.Name,
|
||||
&response.Device.Platform,
|
||||
&response.Device.Status,
|
||||
&response.Device.AssignedIP,
|
||||
&profileRevision,
|
||||
&lastIssuedAt,
|
||||
&gatewayName,
|
||||
&gatewayEndpoint,
|
||||
&gatewayPublicKey,
|
||||
&dnsServers,
|
||||
&allowedIPs,
|
||||
); err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
response.Peer = PeerView{
|
||||
AssignedIP: response.Device.AssignedIP,
|
||||
DNSServers: dnsServers,
|
||||
AllowedIPs: allowedIPs,
|
||||
Gateway: GatewayView{
|
||||
ID: response.Device.GatewayID,
|
||||
Name: gatewayName,
|
||||
Endpoint: gatewayEndpoint,
|
||||
PublicKey: gatewayPublicKey,
|
||||
},
|
||||
ProfileRevision: profileRevision,
|
||||
}
|
||||
for _, destination := range allowedIPs {
|
||||
response.Resources = append(response.Resources, Resource{
|
||||
Type: "cidr",
|
||||
Value: destination,
|
||||
Label: destination,
|
||||
})
|
||||
}
|
||||
_ = lastIssuedAt
|
||||
return response, nil
|
||||
}
|
||||
130
backend/internal/device/service.go
Normal file
130
backend/internal/device/service.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nexavpn/nexavpn/backend/internal/gateway"
|
||||
"github.com/nexavpn/nexavpn/backend/internal/ipam"
|
||||
"github.com/nexavpn/nexavpn/backend/internal/policy"
|
||||
"github.com/nexavpn/nexavpn/backend/internal/profile"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
policyService *policy.Service
|
||||
gatewayService *gateway.Service
|
||||
ipamService *ipam.Service
|
||||
}
|
||||
|
||||
func NewService(repo Repository, policyService *policy.Service, gatewayService *gateway.Service, ipamService *ipam.Service) *Service {
|
||||
return &Service{
|
||||
repo: repo,
|
||||
policyService: policyService,
|
||||
gatewayService: gatewayService,
|
||||
ipamService: ipamService,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Enroll(ctx context.Context, userID uuid.UUID, input EnrollRequest, privateKeyPlaceholder string) (EnrollmentResponse, error) {
|
||||
selectedGateway, err := s.gatewayService.SelectActive(ctx)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
assignedIP, err := s.ipamService.Allocate(selectedGateway.VPNCIDR, 10)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
enrollment, err := s.repo.Enroll(ctx, userID, selectedGateway.ID, input, assignedIP, selectedGateway.DNSServers, nil)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
|
||||
destinations, err := s.policyService.ResolveDestinations(ctx, userID, &enrollment.Device.ID)
|
||||
if err != nil {
|
||||
return EnrollmentResponse{}, err
|
||||
}
|
||||
if len(destinations) == 0 {
|
||||
destinations = []string{"172.16.10.0/24"}
|
||||
}
|
||||
|
||||
enrollment.Peer = PeerView{
|
||||
AssignedIP: assignedIP,
|
||||
DNSServers: selectedGateway.DNSServers,
|
||||
AllowedIPs: destinations,
|
||||
Gateway: GatewayView{
|
||||
ID: selectedGateway.ID,
|
||||
Name: selectedGateway.Name,
|
||||
Endpoint: selectedGateway.Endpoint,
|
||||
PublicKey: selectedGateway.PublicKey,
|
||||
},
|
||||
ProfileRevision: 1,
|
||||
}
|
||||
for _, destination := range destinations {
|
||||
enrollment.Resources = append(enrollment.Resources, Resource{
|
||||
Type: "cidr",
|
||||
Value: destination,
|
||||
Label: destination,
|
||||
})
|
||||
}
|
||||
|
||||
enrollment.Profile = ProfileView{
|
||||
Format: "wireguard",
|
||||
Content: profile.BuildWireGuardConfig(profile.BuildInput{
|
||||
PrivateKey: privateKeyPlaceholder,
|
||||
Address: assignedIP,
|
||||
DNSServers: selectedGateway.DNSServers,
|
||||
ServerPublicKey: selectedGateway.PublicKey,
|
||||
ServerEndpoint: selectedGateway.Endpoint,
|
||||
AllowedIPs: destinations,
|
||||
PersistentKeepal: 25,
|
||||
}),
|
||||
}
|
||||
|
||||
return enrollment, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListByUser(ctx context.Context, userID uuid.UUID) ([]Device, error) {
|
||||
return s.repo.ListByUser(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Service) ListAll(ctx context.Context) ([]Device, error) {
|
||||
return s.repo.ListAll(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) GetLatestEnrollmentByUser(ctx context.Context, userID uuid.UUID) (EnrollmentResponse, error) {
|
||||
return s.repo.GetLatestEnrollmentByUser(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Service) GetEnrollmentByDeviceID(ctx context.Context, deviceID uuid.UUID) (EnrollmentResponse, error) {
|
||||
return s.repo.GetEnrollmentByDeviceID(ctx, deviceID)
|
||||
}
|
||||
|
||||
func (s *Service) GetConnectionStatus(ctx context.Context, userID uuid.UUID) (ConnectionStatus, error) {
|
||||
enrollment, err := s.repo.GetLatestEnrollmentByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return ConnectionStatus{
|
||||
Status: "disconnected",
|
||||
Resources: []Resource{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
lastSync := "just now"
|
||||
return ConnectionStatus{
|
||||
Status: "provisioned",
|
||||
AssignedIP: enrollment.Peer.AssignedIP,
|
||||
LastSyncTime: &lastSync,
|
||||
Resources: enrollment.Resources,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Revoke(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return s.repo.Revoke(ctx, deviceID)
|
||||
}
|
||||
|
||||
func (s *Service) Rotate(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return s.repo.Rotate(ctx, deviceID)
|
||||
}
|
||||
62
backend/internal/device/types.go
Normal file
62
backend/internal/device/types.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package device
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type EnrollRequest struct {
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
OSVersion string `json:"os_version"`
|
||||
AppVersion string `json:"app_version"`
|
||||
DeviceFingerprint string `json:"device_fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id,omitempty"`
|
||||
GatewayID uuid.UUID `json:"gateway_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"`
|
||||
}
|
||||
|
||||
type ConnectionStatus struct {
|
||||
Status string `json:"status"`
|
||||
AssignedIP string `json:"assigned_ip"`
|
||||
LastSyncTime *string `json:"last_sync_time"`
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type EnrollmentResponse struct {
|
||||
Device Device `json:"device"`
|
||||
Peer PeerView `json:"peer"`
|
||||
Profile ProfileView `json:"profile"`
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type PeerView struct {
|
||||
AssignedIP string `json:"assigned_ip"`
|
||||
DNSServers []string `json:"dns_servers"`
|
||||
AllowedIPs []string `json:"allowed_ips"`
|
||||
Gateway GatewayView `json:"gateway"`
|
||||
ProfileRevision int `json:"profile_revision"`
|
||||
}
|
||||
|
||||
type GatewayView struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
type ProfileView struct {
|
||||
Format string `json:"format"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
Reference in New Issue
Block a user