Update go.mod module declaration and all internal imports across the backend codebase to use simplified nexavpn/backend path instead of full GitHub URL.
138 lines
3.9 KiB
Go
138 lines
3.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"nexavpn/backend/internal/apiutil"
|
|
"nexavpn/backend/internal/audit"
|
|
"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,
|
|
})
|
|
}
|