feat(auth): add chatwoot session routes
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
math_rand "math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -27,15 +29,21 @@ import (
|
||||
|
||||
// AuthHandler handles authentication HTTP endpoints.
|
||||
type AuthHandler struct {
|
||||
authService *service.AuthService
|
||||
oauthService *auth.OAuthService
|
||||
authService *service.AuthService
|
||||
oauthService *auth.OAuthService
|
||||
profileService *service.ProfileService
|
||||
}
|
||||
|
||||
// NewAuthHandler creates an auth handler with service dependencies.
|
||||
func NewAuthHandler(authService *service.AuthService, oauthService *auth.OAuthService) *AuthHandler {
|
||||
func NewAuthHandler(authService *service.AuthService, oauthService *auth.OAuthService, profileService ...*service.ProfileService) *AuthHandler {
|
||||
var profileSvc *service.ProfileService
|
||||
if len(profileService) > 0 {
|
||||
profileSvc = profileService[0]
|
||||
}
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
oauthService: oauthService,
|
||||
authService: authService,
|
||||
oauthService: oauthService,
|
||||
profileService: profileSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +51,7 @@ func NewAuthHandler(authService *service.AuthService, oauthService *auth.OAuthSe
|
||||
|
||||
// LoginRequest is the JSON body for login endpoint.
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
@@ -122,6 +130,92 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ChatwootSignIn authenticates through the DeviseTokenAuth-compatible route.
|
||||
// POST /auth/sign_in
|
||||
func (h *AuthHandler) ChatwootSignIn(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
output, err := h.authService.Login(c.Request.Context(), &service.LoginInput{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if output.MFARequired {
|
||||
c.JSON(http.StatusPartialContent, gin.H{
|
||||
"mfa_required": true,
|
||||
"mfa_token": strconv.FormatUint(uint64(output.User.ID), 10),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.setChatwootAuthHeaders(c, output)
|
||||
profile, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
||||
if err != nil {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": profile})
|
||||
}
|
||||
|
||||
// ChatwootValidateToken returns the current user payload for Auth.validityCheck.
|
||||
// GET /auth/validate_token
|
||||
func (h *AuthHandler) ChatwootValidateToken(c *gin.Context) {
|
||||
accessToken := extractChatwootAccessToken(c)
|
||||
if accessToken == "" {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "access token required")
|
||||
return
|
||||
}
|
||||
|
||||
output, err := h.authService.ValidateAccessToken(c.Request.Context(), accessToken)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
h.setChatwootAuthHeaders(c, &service.LoginOutput{User: output.User, AccountID: output.AccountID})
|
||||
c.Header("access-token", accessToken)
|
||||
profile, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
||||
if err != nil {
|
||||
handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"payload": gin.H{
|
||||
"success": true,
|
||||
"data": profile,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ChatwootSignOut revokes the current session for the DeviseTokenAuth route.
|
||||
// DELETE /auth/sign_out
|
||||
func (h *AuthHandler) ChatwootSignOut(c *gin.Context) {
|
||||
accessToken := extractChatwootAccessToken(c)
|
||||
if accessToken == "" {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "access token required")
|
||||
return
|
||||
}
|
||||
|
||||
output, err := h.authService.ValidateAccessToken(c.Request.Context(), accessToken)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid token")
|
||||
return
|
||||
}
|
||||
if err := h.authService.Logout(c.Request.Context(), output.User.ID); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Logout failed")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// LoginMFA completes login after MFA TOTP code verification.
|
||||
// POST /api/v1/auth/login/mfa
|
||||
func (h *AuthHandler) LoginMFA(c *gin.Context) {
|
||||
@@ -384,6 +478,46 @@ func RegisterAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChatwootAuthRoutes maps the root auth routes consumed by the Chatwoot frontend.
|
||||
func RegisterChatwootAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
|
||||
rg.POST("/sign_in", handler.ChatwootSignIn)
|
||||
rg.DELETE("/sign_out", handler.ChatwootSignOut)
|
||||
rg.GET("/validate_token", handler.ChatwootValidateToken)
|
||||
rg.POST("/password", handler.ResetPassword)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) chatwootUserPayload(c *gin.Context, userID uint, accountID uint) (any, error) {
|
||||
if h.profileService == nil {
|
||||
return gin.H{"id": userID, "account_id": accountID}, nil
|
||||
}
|
||||
return h.profileService.Get(c.Request.Context(), userID, accountID)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) setChatwootAuthHeaders(c *gin.Context, output *service.LoginOutput) {
|
||||
c.Header("token-type", "Bearer")
|
||||
if output == nil || output.User == nil {
|
||||
return
|
||||
}
|
||||
c.Header("uid", output.User.Email)
|
||||
if output.TokenPair == nil {
|
||||
return
|
||||
}
|
||||
c.Header("access-token", output.TokenPair.AccessToken)
|
||||
c.Header("client", output.TokenPair.RefreshToken)
|
||||
c.Header("expiry", strconv.FormatInt(output.TokenPair.ExpiresAt.Unix(), 10))
|
||||
}
|
||||
|
||||
func extractChatwootAccessToken(c *gin.Context) string {
|
||||
if token := strings.TrimSpace(c.GetHeader("access-token")); token != "" {
|
||||
return token
|
||||
}
|
||||
authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
|
||||
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||
return strings.TrimSpace(authHeader[7:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// generateOAuthState creates a cryptographically random state token for OAuth CSRF protection.
|
||||
// Production note: state should also be stored server-side (Redis) and validated on callback.
|
||||
func generateOAuthState() string {
|
||||
@@ -399,4 +533,4 @@ func randomHex(n int) string {
|
||||
math_rand.Read(b)
|
||||
}
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user