200 lines
7.7 KiB
Go
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)
|
|
}
|