Files
NexaVPN/backend/internal/device/handler.go
nessi 298d301ce8 refactor: extract request context utilities into dedicated package
Move ClaimsFromContext and MustUserID helpers from httpserver to new requestctx package for better separation of concerns. Update all imports across auth, device, policy, and user handlers. Fix Dockerfile to copy go.sum and run go mod tidy before download.
2026-03-15 16:37:01 +01:00

177 lines
5.2 KiB
Go

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/requestctx"
)
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 := requestctx.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 := requestctx.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 := requestctx.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 := requestctx.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 := requestctx.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 := requestctx.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})
}