feat(profile): align mfa payloads
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user