update: 2026-03-28 20:59
This commit is contained in:
265
infogenie-backend-go/internal/handler/siteconfig.go
Normal file
265
infogenie-backend-go/internal/handler/siteconfig.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"infogenie-backend/config"
|
||||
"infogenie-backend/internal/database"
|
||||
"infogenie-backend/internal/model"
|
||||
)
|
||||
|
||||
type SiteConfigHandler struct{}
|
||||
|
||||
func NewSiteConfigHandler() *SiteConfigHandler { return &SiteConfigHandler{} }
|
||||
|
||||
func siteAdminTokenOK(headerToken string) bool {
|
||||
if config.Cfg == nil {
|
||||
return false
|
||||
}
|
||||
expected := strings.TrimSpace(config.Cfg.SiteAdminToken)
|
||||
if expected == "" {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(headerToken), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
// Get60sDisabled 公开:返回当前隐藏的 60s 功能 id 列表(与前端 item.id 对应)
|
||||
func (h *SiteConfigHandler) Get60sDisabled(c *gin.Context) {
|
||||
var rows []model.Site60sDisabled
|
||||
if err := database.DB.Order("feature_id").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.FeatureID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"disabled": ids})
|
||||
}
|
||||
|
||||
type put60sDisabledBody struct {
|
||||
Disabled []string `json:"disabled"`
|
||||
}
|
||||
|
||||
// Put60sDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致(建议与前端管理员口令相同)
|
||||
func (h *SiteConfigHandler) Put60sDisabled(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "admin_not_configured",
|
||||
"message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"})
|
||||
return
|
||||
}
|
||||
|
||||
var body put60sDisabledBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
if len(body.Disabled) > 512 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
clean := make([]string, 0, len(body.Disabled))
|
||||
for _, raw := range body.Disabled {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" || len(id) > 96 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.Site60sDisabled{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
for _, id := range clean {
|
||||
if err := tx.Create(&model.Site60sDisabled{FeatureID: id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)})
|
||||
}
|
||||
|
||||
// —— 60s API 上游节点(仅管理员可切换)——
|
||||
|
||||
type sixtySrcInfo struct {
|
||||
Base string
|
||||
Label string
|
||||
}
|
||||
|
||||
var sixtyUpstreamRegistry = map[string]sixtySrcInfo{
|
||||
"self": {Base: "https://60s.api.shumengya.top", Label: "萌芽节点"},
|
||||
"official": {Base: "https://60s.viki.moe", Label: "官方节点"},
|
||||
}
|
||||
|
||||
func resolve60sUpstream(sourceID string) (id string, info sixtySrcInfo) {
|
||||
id = strings.TrimSpace(sourceID)
|
||||
if id == "" {
|
||||
id = "self"
|
||||
}
|
||||
var ok bool
|
||||
info, ok = sixtyUpstreamRegistry[id]
|
||||
if !ok {
|
||||
id = "self"
|
||||
info = sixtyUpstreamRegistry["self"]
|
||||
}
|
||||
return id, info
|
||||
}
|
||||
|
||||
// Get60sSource 公开:当前站点使用的 60s 上游 base_url(供静态页 iframe 传参)
|
||||
func (h *SiteConfigHandler) Get60sSource(c *gin.Context) {
|
||||
var row model.Site60sUpstream
|
||||
_ = database.DB.First(&row, 1).Error
|
||||
sid, info := resolve60sUpstream(row.SourceID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"source_id": sid,
|
||||
"base_url": info.Base,
|
||||
"label": info.Label,
|
||||
})
|
||||
}
|
||||
|
||||
type put60sSourceBody struct {
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
|
||||
// Put60sSource 管理员切换节点:source_id 为 self | official
|
||||
func (h *SiteConfigHandler) Put60sSource(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "admin_not_configured"})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
var body put60sSourceBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
sid := strings.TrimSpace(body.SourceID)
|
||||
if _, ok := sixtyUpstreamRegistry[sid]; !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_source_id"})
|
||||
return
|
||||
}
|
||||
var row model.Site60sUpstream
|
||||
err := database.DB.First(&row, 1).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
row = model.Site60sUpstream{ID: 1}
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
row.SourceID = sid
|
||||
if err := database.DB.Save(&row).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
_, info := resolve60sUpstream(sid)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "source_id": sid, "base_url": info.Base, "label": info.Label})
|
||||
}
|
||||
|
||||
// —— AI 应用可见性控制(仅管理员可配置)——
|
||||
|
||||
// GetAIModelDisabled 公开:返回当前隐藏的 AI 应用 id 列表(与前端 StaticPageConfig 中 AI_MODEL_APPS 的索引对应)
|
||||
func (h *SiteConfigHandler) GetAIModelDisabled(c *gin.Context) {
|
||||
var rows []model.SiteAIModelDisabled
|
||||
if err := database.DB.Order("app_id").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.AppID)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"disabled": ids})
|
||||
}
|
||||
|
||||
type putAIModelDisabledBody struct {
|
||||
Disabled []string `json:"disabled"`
|
||||
}
|
||||
|
||||
// PutAIModelDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致
|
||||
func (h *SiteConfigHandler) PutAIModelDisabled(c *gin.Context) {
|
||||
if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "admin_not_configured",
|
||||
"message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"})
|
||||
return
|
||||
}
|
||||
|
||||
var body putAIModelDisabledBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"})
|
||||
return
|
||||
}
|
||||
if len(body.Disabled) > 64 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"})
|
||||
return
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
clean := make([]string, 0, len(body.Disabled))
|
||||
for _, raw := range body.Disabled {
|
||||
id := strings.TrimSpace(raw)
|
||||
if id == "" || len(id) > 96 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
}
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.SiteAIModelDisabled{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
for _, id := range clean {
|
||||
if err := tx.Create(&model.SiteAIModelDisabled{AppID: id}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)})
|
||||
}
|
||||
Reference in New Issue
Block a user