231 lines
7.2 KiB
Go
231 lines
7.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fasthttp/router"
|
|
"github.com/google/uuid"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
"github.com/maximhq/bifrost/framework/configstore"
|
|
"github.com/maximhq/bifrost/framework/configstore/tables"
|
|
"github.com/maximhq/bifrost/framework/encrypt"
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// SessionHandler manages HTTP requests for session operations
|
|
type SessionHandler struct {
|
|
configStore configstore.ConfigStore
|
|
wsTicketStore *WSTicketStore
|
|
}
|
|
|
|
// NewSessionHandler creates a new session handler instance
|
|
func NewSessionHandler(configStore configstore.ConfigStore, wsTicketStore *WSTicketStore) *SessionHandler {
|
|
return &SessionHandler{
|
|
configStore: configStore,
|
|
wsTicketStore: wsTicketStore,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers the session-related routes
|
|
func (h *SessionHandler) RegisterRoutes(r *router.Router, middlewares ...schemas.BifrostHTTPMiddleware) {
|
|
r.POST("/api/session/login", lib.ChainMiddlewares(h.login, middlewares...))
|
|
r.POST("/api/session/logout", lib.ChainMiddlewares(h.logout, middlewares...))
|
|
r.GET("/api/session/is-auth-enabled", lib.ChainMiddlewares(h.isAuthEnabled, middlewares...))
|
|
r.POST("/api/session/ws-ticket", lib.ChainMiddlewares(h.issueWSTicket, middlewares...))
|
|
}
|
|
|
|
// isAuthEnabled handles GET /api/session/is-auth-enabled - Check if auth is enabled
|
|
func (h *SessionHandler) isAuthEnabled(ctx *fasthttp.RequestCtx) {
|
|
if h.configStore == nil {
|
|
SendJSON(ctx, map[string]any{
|
|
"is_auth_enabled": false,
|
|
})
|
|
return
|
|
}
|
|
authConfig, err := h.configStore.GetAuthConfig(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get auth config: %v", err))
|
|
return
|
|
}
|
|
if authConfig == nil {
|
|
SendJSON(ctx, map[string]any{
|
|
"is_auth_enabled": false,
|
|
})
|
|
return
|
|
}
|
|
// Check if the header has a token and is valid (Authorization header or cookie)
|
|
token := ""
|
|
if authHeader := string(ctx.Request.Header.Peek("Authorization")); strings.HasPrefix(authHeader, "Bearer ") {
|
|
token = strings.TrimPrefix(authHeader, "Bearer ")
|
|
}
|
|
if token == "" {
|
|
token = string(ctx.Request.Header.Cookie("token"))
|
|
}
|
|
hasValidToken := false
|
|
if token != "" {
|
|
session, err := h.configStore.GetSession(ctx, token)
|
|
if err == nil && session != nil && session.ExpiresAt.After(time.Now()) {
|
|
hasValidToken = true
|
|
}
|
|
}
|
|
SendJSON(ctx, map[string]any{
|
|
"is_auth_enabled": authConfig.IsEnabled,
|
|
"has_valid_token": hasValidToken,
|
|
})
|
|
}
|
|
|
|
// login handles POST /api/session/login - Login a user
|
|
func (h *SessionHandler) login(ctx *fasthttp.RequestCtx) {
|
|
if h.configStore == nil {
|
|
SendError(ctx, fasthttp.StatusForbidden, "Authentication is not enabled")
|
|
return
|
|
}
|
|
payload := struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}{}
|
|
if err := json.Unmarshal(ctx.PostBody(), &payload); err != nil {
|
|
SendError(ctx, fasthttp.StatusBadRequest, fmt.Sprintf("Invalid request format: %v", err))
|
|
return
|
|
}
|
|
|
|
// Get auth config
|
|
authConfig, err := h.configStore.GetAuthConfig(ctx)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to get auth config: %v", err))
|
|
return
|
|
}
|
|
|
|
// Check if auth is enabled
|
|
if authConfig == nil || !authConfig.IsEnabled {
|
|
SendError(ctx, fasthttp.StatusForbidden, "Authentication is not enabled")
|
|
return
|
|
}
|
|
|
|
// Verify credentials
|
|
if payload.Username != authConfig.AdminUserName.GetValue() {
|
|
SendError(ctx, fasthttp.StatusUnauthorized, "Invalid username or password")
|
|
return
|
|
}
|
|
compare, err := encrypt.CompareHash(authConfig.AdminPassword.GetValue(), payload.Password)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
if !compare {
|
|
SendError(ctx, fasthttp.StatusUnauthorized, "Invalid username or password")
|
|
return
|
|
}
|
|
|
|
// Creating a new session
|
|
token := uuid.New().String()
|
|
session := &tables.SessionsTable{
|
|
Token: token,
|
|
ExpiresAt: time.Now().Add(time.Hour * 24 * 30), // 30 days
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err = h.configStore.CreateSession(ctx, session)
|
|
if err != nil {
|
|
SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to create session: %v", err))
|
|
return
|
|
}
|
|
|
|
// Setting cookies
|
|
cookie := fasthttp.AcquireCookie()
|
|
defer fasthttp.ReleaseCookie(cookie)
|
|
cookie.SetKey("token")
|
|
cookie.SetValue(token)
|
|
cookie.SetExpire(time.Now().Add(time.Hour * 24 * 30))
|
|
cookie.SetPath("/")
|
|
cookie.SetHTTPOnly(true)
|
|
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
|
// Check if source is https then set secure
|
|
if string(ctx.Request.Header.Peek("X-Forwarded-Proto")) == "https" {
|
|
cookie.SetSecure(true)
|
|
}
|
|
ctx.Response.Header.SetCookie(cookie)
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "Login successful",
|
|
})
|
|
}
|
|
|
|
// logout handles POST /api/session/logout - Logout a user
|
|
func (h *SessionHandler) logout(ctx *fasthttp.RequestCtx) {
|
|
if h.configStore == nil {
|
|
SendError(ctx, fasthttp.StatusForbidden, "Authentication is not enabled")
|
|
return
|
|
}
|
|
// Get token from Authorization header
|
|
token := string(ctx.Request.Header.Peek("Authorization"))
|
|
token = strings.TrimPrefix(token, "Bearer ")
|
|
|
|
// If no token in header, try to get from cookie
|
|
if token == "" {
|
|
token = string(ctx.Request.Header.Cookie("token"))
|
|
}
|
|
|
|
// clear token from cookies
|
|
cookie := fasthttp.AcquireCookie()
|
|
defer fasthttp.ReleaseCookie(cookie)
|
|
cookie.SetKey("token")
|
|
cookie.SetValue("")
|
|
cookie.SetExpire(time.Now().Add(-time.Hour * 24 * 30))
|
|
cookie.SetPath("/")
|
|
cookie.SetHTTPOnly(true)
|
|
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
|
// Check if source is https then set secure
|
|
if string(ctx.Request.Header.Peek("X-Forwarded-Proto")) == "https" {
|
|
cookie.SetSecure(true)
|
|
}
|
|
ctx.Response.Header.SetCookie(cookie)
|
|
|
|
// delete session from database if token exists
|
|
if token != "" {
|
|
err := h.configStore.DeleteSession(ctx, token)
|
|
if err != nil && !errors.Is(err, configstore.ErrNotFound) {
|
|
logger.Error("failed to delete session during logout: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, "Failed to invalidate session. Please try again.")
|
|
return
|
|
}
|
|
}
|
|
|
|
SendJSON(ctx, map[string]any{
|
|
"message": "Logout successful",
|
|
})
|
|
}
|
|
|
|
// issueWSTicket handles POST /api/session/ws-ticket - Issue a short-lived ticket for WebSocket auth.
|
|
// The caller must already be authenticated (via cookie or Authorization header).
|
|
// Returns a one-time-use ticket that the frontend passes as ?ticket= when opening the WebSocket.
|
|
func (h *SessionHandler) issueWSTicket(ctx *fasthttp.RequestCtx) {
|
|
if h.wsTicketStore == nil {
|
|
SendError(ctx, fasthttp.StatusServiceUnavailable, "WebSocket tickets are not available")
|
|
return
|
|
}
|
|
sessionToken,ok := ctx.UserValue(schemas.BifrostContextKeySessionToken).(string)
|
|
if !ok {
|
|
SendError(ctx, fasthttp.StatusUnauthorized, "Unauthorized")
|
|
return
|
|
}
|
|
if sessionToken == "" {
|
|
// This is the case where auth is not configured or not enabled
|
|
sessionToken = "dummy-session"
|
|
}
|
|
ticket, err := h.wsTicketStore.Issue(sessionToken)
|
|
if err != nil {
|
|
logger.Error("failed to issue WS ticket: %v", err)
|
|
SendError(ctx, fasthttp.StatusInternalServerError, "Failed to issue WebSocket ticket")
|
|
return
|
|
}
|
|
SendJSON(ctx, map[string]any{
|
|
"ticket": ticket,
|
|
})
|
|
}
|