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