266 lines
8.0 KiB
Go
266 lines
8.0 KiB
Go
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)})
|
||
}
|