Files
NexaVPN/backend/internal/auth/handler.go
nessi 830491cb0d chore: initial project scaffold with admin web, backend, desktop client, and deployment setup
Add monorepo structure for NexaVPN WireGuard control plane including:
- .gitignore for node_modules, build artifacts, and environment files
- README with project overview, monorepo layout, and quick start guide
- Admin web UI with React, Vite, TypeScript, and nginx reverse proxy
- API client with type definitions for users, devices, policies, gateways, and audit logs
- Admin pages for dashboard, users, devices, policies, g
2026-03-15 16:32:34 +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/httpserver"
)
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 := httpserver.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 := httpserver.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,
})
}