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