package v1 // Reference: M13 ยง3 โ€” SSO session management API // List, inspect, and terminate active SSO sessions. // Accessible to account administrators for auditing and session management. import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/pkg/response" applogger "github.com/gochat/gochat/pkg/logger" ) // SSOSessionHandler handles SSO session management endpoints. type SSOSessionHandler struct { sessionStore *auth.SSOSessionStore } // NewSSOSessionHandler creates a new SSOSession handler. func NewSSOSessionHandler(sessionStore *auth.SSOSessionStore) *SSOSessionHandler { return &SSOSessionHandler{sessionStore: sessionStore} } // ListByUser returns all active SSO sessions for a user. // GET /api/v1/sso/sessions?user_id=123 // Admin-only: lists all SSO sessions for a given user across all providers. func (h *SSOSessionHandler) ListByUser(c *gin.Context) { userIDStr := c.Query("user_id") if userIDStr == "" { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Missing user_id parameter", }, }) return } userID, err := strconv.ParseUint(userIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Invalid user_id", }, }) return } sessions, err := h.sessionStore.GetByUser(c.Request.Context(), uint(userID)) if err != nil { applogger.L().Errorf("Get SSO sessions for user %d: %v", userID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SSO sessions") return } c.JSON(http.StatusOK, response.APIResponse{ Success: true, Data: sessions, }) } // Get retrieves a single SSO session by session ID. // GET /api/v1/sso/sessions/:session_id func (h *SSOSessionHandler) Get(c *gin.Context) { sessionID := c.Param("session_id") if sessionID == "" { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Missing session_id", }, }) return } session, err := h.sessionStore.Get(c.Request.Context(), sessionID) if err != nil { applogger.L().Errorf("Get SSO session %s: %v", sessionID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get SSO session") return } if session == nil { c.JSON(http.StatusNotFound, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrNotFound, Message: "SSO session not found or expired", }, }) return } c.JSON(http.StatusOK, response.APIResponse{ Success: true, Data: session, }) } // Terminate removes a single SSO session. // DELETE /api/v1/sso/sessions/:session_id // Used for manual session termination (admin action). func (h *SSOSessionHandler) Terminate(c *gin.Context) { sessionID := c.Param("session_id") if sessionID == "" { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Missing session_id", }, }) return } terminated, err := h.sessionStore.Terminate(c.Request.Context(), sessionID) if err != nil { applogger.L().Errorf("Terminate SSO session %s: %v", sessionID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to terminate SSO session") return } if !terminated { c.JSON(http.StatusNotFound, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrNotFound, Message: "SSO session not found or already expired", }, }) return } c.JSON(http.StatusOK, response.APIResponse{ Success: true, Data: map[string]string{ "session_id": sessionID, "status": "terminated", }, }) } // TerminateAllByUser terminates all SSO sessions for a user. // POST /api/v1/sso/sessions/terminate_all?user_id=123 // Used for full user logout โ€” terminates all SSO sessions across all providers. func (h *SSOSessionHandler) TerminateAllByUser(c *gin.Context) { userIDStr := c.Query("user_id") if userIDStr == "" { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Missing user_id parameter", }, }) return } userID, err := strconv.ParseUint(userIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Invalid user_id", }, }) return } count, err := h.sessionStore.TerminateUserSessions(c.Request.Context(), uint(userID)) if err != nil { applogger.L().Errorf("Terminate all SSO sessions for user %d: %v", userID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to terminate all SSO sessions") return } c.JSON(http.StatusOK, response.APIResponse{ Success: true, Data: map[string]interface{}{ "user_id": userID, "terminated": count, "status": "all_sessions_terminated", }, }) } // CountByUser returns the count of active SSO sessions for a user. // GET /api/v1/sso/sessions/count?user_id=123 func (h *SSOSessionHandler) CountByUser(c *gin.Context) { userIDStr := c.Query("user_id") if userIDStr == "" { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Missing user_id parameter", }, }) return } userID, err := strconv.ParseUint(userIDStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, response.APIResponse{ Success: false, Error: &response.ErrorBody{ Code: response.ErrBadRequest, Message: "Invalid user_id", }, }) return } count, err := h.sessionStore.CountByUser(c.Request.Context(), uint(userID)) if err != nil { applogger.L().Errorf("Count SSO sessions for user %d: %v", userID, err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to count SSO sessions") return } c.JSON(http.StatusOK, response.APIResponse{ Success: true, Data: map[string]interface{}{ "user_id": userID, "count": count, }, }) } // RegisterSSOSessionRoutes maps SSO session management routes. // All routes require authentication (applied by the router, not here). // Reference: Chatwoot SsoSession management โ€” user-scoped session auditing func RegisterSSOSessionRoutes(g *gin.RouterGroup, h *SSOSessionHandler) { g.GET("/user/:user_id", h.ListByUser) g.GET("/user/:user_id/count", h.CountByUser) g.GET("/:session_id", h.Get) g.POST("/:session_id/terminate", h.Terminate) g.POST("/user/:user_id/terminate_all", h.TerminateAllByUser) }