feat(auth): align password reset flows

This commit is contained in:
2026-06-05 22:59:15 +08:00
parent 9db133697e
commit ad5d864944
8 changed files with 397 additions and 54 deletions
+67 -4
View File
@@ -83,6 +83,16 @@ type ResetPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
type ConfirmResetPasswordRequest struct {
ResetPasswordToken string `json:"reset_password_token" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
PasswordConfirmation string `json:"password_confirmation" binding:"required,min=6"`
}
type ConfirmEmailRequest struct {
ConfirmationToken string `json:"confirmation_token" binding:"required"`
}
// OAuthCallbackRequest is the JSON body for OAuth callback.
type OAuthCallbackRequest struct {
Provider string `json:"provider" binding:"required"`
@@ -358,9 +368,35 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
Email: req.Email,
})
response.OK(c, gin.H{
"message": "If the email exists, a reset link has been sent.",
c.JSON(http.StatusOK, gin.H{"message": service.ChatwootPasswordResetMessage})
}
// ConfirmResetPassword completes Chatwoot's Devise-compatible password reset.
// PUT /auth/password
func (h *AuthHandler) ConfirmResetPassword(c *gin.Context) {
var req ConfirmResetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
output, err := h.authService.ConfirmResetPassword(c.Request.Context(), &service.ConfirmResetPasswordInput{
Token: req.ResetPasswordToken,
Password: req.Password,
PasswordConfirmation: req.PasswordConfirmation,
})
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"message": err.Error(), "redirect_url": "/"})
return
}
h.setChatwootAuthHeaders(c, output)
data, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
if err != nil {
handleServiceError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"data": data})
}
// ConfirmEmail verifies email confirmation token.
@@ -372,7 +408,7 @@ func (h *AuthHandler) ConfirmEmail(c *gin.Context) {
return
}
user, err := h.authService.ConfirmEmail(c.Request.Context(), &service.ConfirmEmailInput{
output, err := h.authService.ConfirmEmail(c.Request.Context(), &service.ConfirmEmailInput{
Token: token,
})
if err != nil {
@@ -381,11 +417,35 @@ func (h *AuthHandler) ConfirmEmail(c *gin.Context) {
}
response.OK(c, gin.H{
"user": user,
"user": output.User,
"message": "Email confirmed successfully.",
})
}
// ChatwootConfirmEmail verifies the confirmation token from the reused frontend.
// POST /auth/confirmation
func (h *AuthHandler) ChatwootConfirmEmail(c *gin.Context) {
var req ConfirmEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
output, err := h.authService.ConfirmEmail(c.Request.Context(), &service.ConfirmEmailInput{Token: req.ConfirmationToken})
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"message": err.Error(), "redirect_url": "/"})
return
}
h.setChatwootAuthHeaders(c, output)
data, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
if err != nil {
handleServiceError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"data": data})
}
// OAuthCallback handles OAuth2 provider callback.
// POST /api/v1/auth/oauth/callback
// Receives provider + code from frontend (frontend handles redirect flow).
@@ -470,6 +530,7 @@ func RegisterAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
// Password & email
authGroup.POST("/reset_password", handler.ResetPassword)
authGroup.PUT("/reset_password", handler.ConfirmResetPassword)
authGroup.GET("/confirm_email", handler.ConfirmEmail)
// OAuth
@@ -484,6 +545,8 @@ func RegisterChatwootAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
rg.DELETE("/sign_out", handler.ChatwootSignOut)
rg.GET("/validate_token", handler.ChatwootValidateToken)
rg.POST("/password", handler.ResetPassword)
rg.PUT("/password", handler.ConfirmResetPassword)
rg.POST("/confirmation", handler.ChatwootConfirmEmail)
}
func (h *AuthHandler) chatwootUserPayload(c *gin.Context, userID uint, accountID uint) (any, error) {