Files
EveryPublish/server/internal/api/handlers/challenge.go
T

200 lines
7.7 KiB
Go

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)
}