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}) }