Files
NexaVPN/backend/internal/device/handler.go
nessi 3289da24af refactor: update module path from github.com/nexavpn/nexavpn/backend to nexavpn/backend
Update go.mod module declaration and all internal imports across the backend codebase to use simplified nexavpn/backend path instead of full GitHub URL.
2026-03-15 16:42:25 +01:00

177 lines
5.1 KiB
Go

package device
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"nexavpn/backend/internal/apiutil"
"nexavpn/backend/internal/audit"
"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})
}