Files
InfoGenie/infogenie-backend-go/internal/handler/siteconfig.go
2026-03-28 20:59:52 +08:00

266 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)})
}