feat(profile): align mfa payloads

This commit is contained in:
2026-06-06 22:37:26 +08:00
parent 6e66af78c7
commit 21421c6a42
8 changed files with 392 additions and 19 deletions
+122
View File
@@ -52,6 +52,17 @@ type DisableMFARequest struct {
TOTPCode string `json:"totp_code" binding:"required"` // current TOTP code for verification
}
type profileMFAVerifyRequest struct {
OTPCode string `json:"otp_code"`
TOTPCode string `json:"totp_code"`
}
type profileMFADisableRequest struct {
Password string `json:"password"`
OTPCode string `json:"otp_code"`
BackupCode string `json:"backup_code"`
}
// --- Handlers ---
// EnableMFA initiates MFA setup: generates a TOTP secret and QR URI.
@@ -187,6 +198,117 @@ func RegisterMFARoutes(rg *gin.RouterGroup, handler *MFAHandler) {
}
}
// ProfileMFAStatus matches Chatwoot Profile::MfaController#show.
func (h *MFAHandler) ProfileMFAStatus(c *gin.Context) {
userID := getUserID(c)
if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
enabled, err := h.mfaService.IsMFAEnabled(userID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
backupCodesGenerated, err := h.mfaService.BackupCodesGenerated(userID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"feature_available": true,
"enabled": enabled,
"backup_codes_generated": backupCodesGenerated,
})
}
// ProfileEnableMFA matches Chatwoot Profile::MfaController#create.
func (h *MFAHandler) ProfileEnableMFA(c *gin.Context) {
userID := getUserID(c)
if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
enabled, err := h.mfaService.IsMFAEnabled(userID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if enabled {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "MFA is already enabled"})
return
}
secret, uri, err := h.mfaService.BeginTOTPSetup(userID)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"provisioning_url": uri, "secret": secret})
}
// ProfileVerifyMFA matches Chatwoot Profile::MfaController#verify.
func (h *MFAHandler) ProfileVerifyMFA(c *gin.Context) {
userID := getUserID(c)
if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req profileMFAVerifyRequest
_ = c.ShouldBindJSON(&req)
code := req.OTPCode
if code == "" {
code = req.TOTPCode
}
backupCodes, err := h.mfaService.VerifyAndActivateTOTP(userID, code)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"enabled": true, "backup_codes": backupCodes})
}
// ProfileDisableMFA matches Chatwoot Profile::MfaController#destroy.
func (h *MFAHandler) ProfileDisableMFA(c *gin.Context) {
userID := getUserID(c)
if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req profileMFADisableRequest
_ = c.ShouldBindJSON(&req)
if err := h.mfaService.DisableTOTPWithPassword(userID, req.Password, req.OTPCode, req.BackupCode); err != nil {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"enabled": false})
}
// ProfileBackupCodes matches Chatwoot Profile::MfaController#backup_codes.
func (h *MFAHandler) ProfileBackupCodes(c *gin.Context) {
userID := getUserID(c)
if userID == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
var req profileMFAVerifyRequest
_ = c.ShouldBindJSON(&req)
code := req.OTPCode
if code == "" {
code = req.TOTPCode
}
valid, err := h.mfaService.VerifyTOTPCode(userID, code)
if err != nil || !valid {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid totp code"})
return
}
codes, err := h.mfaService.GenerateBackupCodes(userID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"backup_codes": codes})
}
// BackupCodes generates one-time MFA backup codes.
// POST /api/v1/profile/mfa/backup_codes or /api/v1/auth/mfa/backup_codes
// Reference: Chatwoot MfaController#backup_codes