package handlers import ( "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "everypublish/server/internal/api/response" "everypublish/server/internal/models" "everypublish/server/internal/platform" "everypublish/server/internal/ws" ) // ChallengeHandler 二次验证挑战处理器 type ChallengeHandler struct { DB *gorm.DB Hub *ws.Hub Registry *platform.Registry ExecutorMode string StartLoginPoll func(models.Challenge, models.Account, uint64, platform.Adapter) } // List 挑战列表(过期软置 expired) func (h *ChallengeHandler) List(c *gin.Context) { var expired []models.Challenge h.DB.Where("workspace_id = ? and status = ? and expires_at < ?", c.GetUint64("wsid"), "active", time.Now()).Find(&expired) if len(expired) > 0 { ids := make([]uint64, 0, len(expired)) for _, challenge := range expired { ids = append(ids, challenge.ID) } h.DB.Model(&models.Challenge{}).Where("id in ?", ids).Update("status", "expired") accountIDs := make([]uint64, 0, len(expired)) for _, challenge := range expired { accountIDs = append(accountIDs, challenge.AccountID) } _ = h.DB.Model(&models.Account{}).Where("workspace_id = ? and id in ? and status = ?", c.GetUint64("wsid"), accountIDs, "binding").Updates(map[string]interface{}{"status": "expired", "last_error": "登录二维码已过期"}).Error } q := h.DB.Model(&models.Challenge{}).Where("workspace_id = ?", c.GetUint64("wsid")) if s := c.Query("status"); s != "" { q = q.Where("status = ?", s) } if a := c.Query("accountId"); a != "" { q = q.Where("account_id = ?", a) } var list []models.Challenge q.Order("id desc").Limit(100).Find(&list) response.OK(c, gin.H{"list": list}) } // Solve 完成网页挑战。mock QR 只接受显式联调值;真实平台挑战必须由 adapter // 在用户完成扫码后回写,不能通过空请求把账号标成 active。 func (h *ChallengeHandler) Solve(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var req struct { Value string `json:"value"` } if err = c.ShouldBindJSON(&req); err != nil { response.Fail(c, http.StatusBadRequest, 1001, "请输入挑战确认值") return } var challenge models.Challenge if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&challenge).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "挑战不存在") return } if challenge.Status != "active" { response.Fail(c, http.StatusConflict, 3003, "挑战已结束") return } value := strings.TrimSpace(req.Value) if value == "" { response.Fail(c, http.StatusBadRequest, 1001, "挑战确认值不能为空") return } if challenge.Kind == "qr" && !strings.HasPrefix(challenge.QRURL, "mock://") { response.Fail(c, http.StatusConflict, 3003, "真实二维码必须由平台登录适配器确认") return } now := time.Now() if err = h.DB.Model(&challenge).Updates(map[string]interface{}{"status": "solved", "solved_at": &now}).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return } _ = h.DB.Model(&models.Account{}). Where("id = ? and workspace_id = ?", challenge.AccountID, c.GetUint64("wsid")). Updates(map[string]interface{}{"status": "active", "last_active_at": &now}).Error _ = Notify(h.DB, c.GetUint64("wsid"), []uint64{c.GetUint64("uid")}, "challenge", "登录挑战已完成", "账号已恢复可用") if h.Hub != nil { h.Hub.BroadcastWS(c.GetUint64("wsid"), "challenge.status", gin.H{ "challengeId": challenge.ID, "accountId": challenge.AccountID, "status": "solved", }) h.Hub.BroadcastWS(c.GetUint64("wsid"), "notification.new", gin.H{"kind": "challenge", "title": "登录挑战已完成", "content": "账号已恢复可用"}) } response.Audit(c, "challenge.solve", "challenge:"+itoa(id), gin.H{"kind": challenge.Kind}) response.OK(c, nil) } // Resend 一键重发(重置过期时间,交由当前 Web adapter 继续处理) func (h *ChallengeHandler) Resend(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var challenge models.Challenge if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&challenge).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "挑战不存在") return } if challenge.Status != "expired" && challenge.Status != "suspended" { response.Fail(c, http.StatusConflict, 3003, "当前状态不可重发") return } var account models.Account if err = h.DB.Where("id = ? and workspace_id = ?", challenge.AccountID, c.GetUint64("wsid")).First(&account).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "账号不存在") return } var adapter platform.Adapter var session platform.LoginSession if h.ExecutorMode != "mock" && h.Registry != nil { adapter = h.Registry.Get(account.Platform) if adapter != nil { session, err = adapter.BeginLogin(c.Request.Context()) if err != nil { response.Fail(c, http.StatusBadGateway, 2007, adapterErrorMessage(err)) return } } } updates := map[string]interface{}{ "status": "active", "expires_at": time.Now().Add(30 * time.Minute), "prompt": "请在网页中完成 " + account.Platform + " 登录", } if adapter != nil { updates["qr_token"] = session.Token updates["qr_url"] = session.URL } if err = h.DB.Model(&challenge).Updates(updates).Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return } _ = h.DB.Model(&account).Updates(map[string]interface{}{"status": "binding", "last_error": ""}).Error if adapter != nil && h.StartLoginPoll != nil { challenge.QRToken = session.Token challenge.QRURL = session.URL challenge.Status = "active" challenge.ExpiresAt = updates["expires_at"].(time.Time) challenge.Prompt = updates["prompt"].(string) h.StartLoginPoll(challenge, account, c.GetUint64("uid"), adapter) } else { challenge.Status = "active" challenge.ExpiresAt = updates["expires_at"].(time.Time) challenge.Prompt = updates["prompt"].(string) } if h.Hub != nil { h.Hub.BroadcastWS(challenge.WorkspaceID, "challenge.status", gin.H{ "challengeId": challenge.ID, "accountId": challenge.AccountID, "status": "active", "kind": challenge.Kind, "platform": challenge.Platform, "qrUrl": challenge.QRURL, "prompt": challenge.Prompt, "expiresAt": challenge.ExpiresAt, }) } response.Audit(c, "challenge.resend", "challenge:"+itoa(id), nil) response.OK(c, gin.H{"challengeId": challenge.ID, "qrUrl": challenge.QRURL, "qrToken": challenge.QRToken, "prompt": challenge.Prompt, "expiresAt": challenge.ExpiresAt}) } // Suspend 挂起(超时/人工) func (h *ChallengeHandler) Suspend(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { response.Fail(c, http.StatusBadRequest, 1001, "参数不合法") return } var challenge models.Challenge if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&challenge).Error; err != nil { response.Fail(c, http.StatusNotFound, 1004, "挑战不存在") return } if challenge.Status != "active" { response.Fail(c, http.StatusConflict, 3003, "挑战已结束") return } if err = h.DB.Model(&challenge).Update("status", "suspended").Error; err != nil { response.Fail(c, http.StatusInternalServerError, 5000, "更新失败") return } _ = h.DB.Model(&models.Account{}).Where("id = ? and workspace_id = ? and status = ?", challenge.AccountID, c.GetUint64("wsid"), "binding").Updates(map[string]interface{}{"status": "expired", "last_error": "登录挑战已挂起"}).Error response.Audit(c, "challenge.suspend", "challenge:"+itoa(id), nil) response.OK(c, nil) }