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
This commit is contained in:
137
backend/internal/auth/handler.go
Normal file
137
backend/internal/auth/handler.go
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user