server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -10,18 +11,35 @@ import (
|
||||
|
||||
"everypublish/server/internal/api/response"
|
||||
"everypublish/server/internal/models"
|
||||
"everypublish/server/internal/platform"
|
||||
"everypublish/server/internal/ws"
|
||||
)
|
||||
|
||||
// ChallengeHandler 二次验证挑战处理器
|
||||
type ChallengeHandler struct {
|
||||
DB *gorm.DB
|
||||
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) {
|
||||
h.DB.Model(&models.Challenge{}).
|
||||
Where("workspace_id = ? and status = ? and expires_at < ?", c.GetUint64("wsid"), "active", time.Now()).
|
||||
Update("status", "expired")
|
||||
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)
|
||||
@@ -34,7 +52,8 @@ func (h *ChallengeHandler) List(c *gin.Context) {
|
||||
response.OK(c, gin.H{"list": list})
|
||||
}
|
||||
|
||||
// Solve 人工完成挑战(验证码/APP确认;扫码由 Agent 完成)
|
||||
// Solve 完成网页挑战。mock QR 只接受显式联调值;真实平台挑战必须由 adapter
|
||||
// 在用户完成扫码后回写,不能通过空请求把账号标成 active。
|
||||
func (h *ChallengeHandler) Solve(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -44,7 +63,10 @@ func (h *ChallengeHandler) Solve(c *gin.Context) {
|
||||
var req struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
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, "挑战不存在")
|
||||
@@ -54,16 +76,35 @@ func (h *ChallengeHandler) Solve(c *gin.Context) {
|
||||
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 一键重发(重置过期时间,实际重发出 Agent 执行)
|
||||
// Resend 一键重发(重置过期时间,交由当前 Web adapter 继续处理)
|
||||
func (h *ChallengeHandler) Resend(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -79,14 +120,57 @@ func (h *ChallengeHandler) Resend(c *gin.Context) {
|
||||
response.Fail(c, http.StatusConflict, 3003, "当前状态不可重发")
|
||||
return
|
||||
}
|
||||
if err = h.DB.Model(&challenge).Updates(map[string]interface{}{
|
||||
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),
|
||||
}).Error; err != nil {
|
||||
"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, nil)
|
||||
response.OK(c, gin.H{"challengeId": challenge.ID, "qrUrl": challenge.QRURL, "qrToken": challenge.QRToken, "prompt": challenge.Prompt, "expiresAt": challenge.ExpiresAt})
|
||||
}
|
||||
|
||||
// Suspend 挂起(超时/人工)
|
||||
@@ -109,6 +193,7 @@ func (h *ChallengeHandler) Suspend(c *gin.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user