Files
NexaVPN/backend/internal/auth/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

138 lines
4.0 KiB
Go

package auth
import (
"encoding/json"
"net/http"
"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) Login(w http.ResponseWriter, r *http.Request) {
var input LoginRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
return
}
response, err := h.service.Login(r.Context(), input.Username, input.Password, r.RemoteAddr, r.UserAgent())
if err != nil {
_ = h.audit.Record(r.Context(), audit.Entry{
EventType: "auth.login.failed",
EntityType: "user",
Status: "failed",
Message: "user login failed",
Metadata: map[string]any{
"username": input.Username,
},
})
apiutil.Error(w, http.StatusUnauthorized, "invalid_credentials", "invalid username or password")
return
}
_ = h.audit.Record(r.Context(), audit.Entry{
ActorUserID: &response.User.ID,
EventType: "auth.login",
EntityType: "user",
EntityID: &response.User.ID,
Status: "success",
Message: "user login succeeded",
})
apiutil.JSON(w, http.StatusOK, response)
}
func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) {
var input BootstrapRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
return
}
if input.Username == "" || input.Password == "" {
apiutil.Error(w, http.StatusBadRequest, "validation_error", "username and password are required")
return
}
if input.DisplayName == "" {
input.DisplayName = input.Username
}
user, err := h.service.BootstrapAdmin(r.Context(), input.Username, input.DisplayName, input.Password)
if err != nil {
apiutil.Error(w, http.StatusConflict, "bootstrap_failed", "initial admin already exists or could not be created")
return
}
_ = h.audit.Record(r.Context(), audit.Entry{
ActorUserID: &user.ID,
EntityType: "user",
EntityID: &user.ID,
EventType: "system.bootstrap_admin",
Status: "success",
Message: "initial admin account created",
})
apiutil.JSON(w, http.StatusCreated, user)
}
func (h *Handler) Refresh(w http.ResponseWriter, r *http.Request) {
var input RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
return
}
response, err := h.service.Refresh(r.Context(), input.RefreshToken)
if err != nil {
apiutil.Error(w, http.StatusUnauthorized, "invalid_refresh_token", "unable to refresh session")
return
}
apiutil.JSON(w, http.StatusOK, response)
}
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
var input RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body")
return
}
if err := h.service.Logout(r.Context(), input.RefreshToken); err != nil {
apiutil.Error(w, http.StatusBadRequest, "logout_failed", "unable to revoke session")
return
}
if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok {
_ = h.audit.Record(r.Context(), audit.Entry{
ActorUserID: &claims.UserID,
EventType: "auth.logout",
EntityType: "session",
Status: "success",
Message: "session logout succeeded",
})
}
apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
claims, ok := requestctx.ClaimsFromContext(r.Context())
if !ok {
apiutil.Error(w, http.StatusUnauthorized, "unauthorized", "missing auth claims")
return
}
apiutil.JSON(w, http.StatusOK, map[string]any{
"id": claims.UserID,
"username": claims.Username,
"role": claims.Role,
})
}