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.
177 lines
5.2 KiB
Go
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})
|
|
}
|