Files
new-api/model/frontend_option_migration.go
Calcium-Ion 31d70fca39 refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)
* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
2026-07-20 16:48:43 +08:00

240 lines
7.3 KiB
Go

package model
import (
"errors"
"fmt"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/console_setting"
"gorm.io/gorm"
)
const retiredThemeOptionKey = "theme.frontend"
type legacyOptionTransform func(string) (string, error)
// MigrateRetiredFrontendOptions normalizes options that belonged to the
// removed dashboard frontend. Each legacy console setting is migrated in its
// own transaction so one malformed value cannot block the other settings.
func MigrateRetiredFrontendOptions() error {
if DB == nil {
return errors.New("database is not initialized")
}
var migrationErrors []error
if err := normalizeRetiredThemeOption(); err != nil {
migrationErrors = append(migrationErrors, fmt.Errorf("normalize %s: %w", retiredThemeOptionKey, err))
}
migrations := []struct {
source string
target string
transform legacyOptionTransform
}{
{source: "ApiInfo", target: "console_setting.api_info", transform: transformLegacyAPIInfo},
{source: "Announcements", target: "console_setting.announcements", transform: transformLegacyAnnouncements},
{source: "FAQ", target: "console_setting.faq", transform: transformLegacyFAQ},
}
for _, migration := range migrations {
if err := migrateLegacyOption(migration.source, migration.target, migration.transform); err != nil {
migrationErrors = append(migrationErrors, err)
}
}
if err := migrateLegacyUptimeOptions(); err != nil {
migrationErrors = append(migrationErrors, err)
}
return errors.Join(migrationErrors...)
}
func normalizeRetiredThemeOption() error {
return DB.Transaction(func(tx *gorm.DB) error {
var option Option
err := tx.Where(&Option{Key: retiredThemeOptionKey}).First(&option).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Create(&Option{Key: retiredThemeOptionKey, Value: "default"}).Error
}
if err != nil {
return err
}
if option.Value == "default" {
return nil
}
return tx.Model(&option).Update("value", "default").Error
})
}
func migrateLegacyOption(sourceKey, targetKey string, transform legacyOptionTransform) error {
return DB.Transaction(func(tx *gorm.DB) error {
var source Option
if err := tx.Where(&Option{Key: sourceKey}).First(&source).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("read legacy option %s: %w", sourceKey, err)
}
var target Option
err := tx.Where(&Option{Key: targetKey}).First(&target).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("read target option %s: %w", targetKey, err)
}
if err == nil {
return tx.Delete(&source).Error
}
value, transformErr := transform(source.Value)
if transformErr != nil {
common.SysError(fmt.Sprintf("legacy option %s was not migrated: %v", sourceKey, transformErr))
return nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
target = Option{Key: targetKey}
}
target.Value = value
if err := tx.Save(&target).Error; err != nil {
return fmt.Errorf("write target option %s: %w", targetKey, err)
}
if err := tx.Delete(&source).Error; err != nil {
return fmt.Errorf("delete legacy option %s: %w", sourceKey, err)
}
return nil
})
}
func transformLegacyAPIInfo(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", errors.New("value is empty")
}
var items []map[string]any
if err := common.UnmarshalJsonStr(value, &items); err != nil {
return "", err
}
if len(items) > 50 {
items = items[:50]
}
encoded, err := common.Marshal(items)
if err != nil {
return "", err
}
result := string(encoded)
if err := console_setting.ValidateConsoleSettings(result, "ApiInfo"); err != nil {
return "", err
}
return result, nil
}
func transformLegacyAnnouncements(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", errors.New("value is empty")
}
if err := console_setting.ValidateConsoleSettings(value, "Announcements"); err != nil {
return "", err
}
return value, nil
}
func transformLegacyFAQ(value string) (string, error) {
if strings.TrimSpace(value) == "" {
return "", errors.New("value is empty")
}
var legacyItems []map[string]any
if err := common.UnmarshalJsonStr(value, &legacyItems); err != nil {
return "", err
}
items := make([]map[string]any, 0, len(legacyItems))
for index, item := range legacyItems {
question, _ := item["question"].(string)
if strings.TrimSpace(question) == "" {
question, _ = item["title"].(string)
}
answer, _ := item["answer"].(string)
if strings.TrimSpace(answer) == "" {
answer, _ = item["content"].(string)
}
if strings.TrimSpace(question) == "" || strings.TrimSpace(answer) == "" {
return "", fmt.Errorf("FAQ entry %d is missing a question or answer", index)
}
items = append(items, map[string]any{"question": question, "answer": answer})
}
if len(items) > 50 {
items = items[:50]
}
encoded, err := common.Marshal(items)
if err != nil {
return "", err
}
result := string(encoded)
if err := console_setting.ValidateConsoleSettings(result, "FAQ"); err != nil {
return "", err
}
return result, nil
}
func migrateLegacyUptimeOptions() error {
return DB.Transaction(func(tx *gorm.DB) error {
var urlOption Option
urlErr := tx.Where(&Option{Key: "UptimeKumaUrl"}).First(&urlOption).Error
if urlErr != nil && !errors.Is(urlErr, gorm.ErrRecordNotFound) {
return fmt.Errorf("read legacy option UptimeKumaUrl: %w", urlErr)
}
var slugOption Option
slugErr := tx.Where(&Option{Key: "UptimeKumaSlug"}).First(&slugOption).Error
if slugErr != nil && !errors.Is(slugErr, gorm.ErrRecordNotFound) {
return fmt.Errorf("read legacy option UptimeKumaSlug: %w", slugErr)
}
if errors.Is(urlErr, gorm.ErrRecordNotFound) && errors.Is(slugErr, gorm.ErrRecordNotFound) {
return nil
}
var target Option
targetErr := tx.Where(&Option{Key: "console_setting.uptime_kuma_groups"}).First(&target).Error
if targetErr != nil && !errors.Is(targetErr, gorm.ErrRecordNotFound) {
return fmt.Errorf("read target option console_setting.uptime_kuma_groups: %w", targetErr)
}
if targetErr == nil {
if urlErr == nil {
if err := tx.Delete(&urlOption).Error; err != nil {
return err
}
}
if slugErr == nil {
return tx.Delete(&slugOption).Error
}
return nil
}
if urlErr != nil || slugErr != nil || strings.TrimSpace(urlOption.Value) == "" || strings.TrimSpace(slugOption.Value) == "" {
common.SysError("legacy Uptime Kuma options were not migrated: both URL and slug are required")
return nil
}
groups := []map[string]any{{
"id": 1,
"categoryName": "old",
"url": urlOption.Value,
"slug": slugOption.Value,
"description": "",
}}
encoded, err := common.Marshal(groups)
if err != nil {
return err
}
value := string(encoded)
if err := console_setting.ValidateConsoleSettings(value, "UptimeKumaGroups"); err != nil {
common.SysError(fmt.Sprintf("legacy Uptime Kuma options were not migrated: %v", err))
return nil
}
if errors.Is(targetErr, gorm.ErrRecordNotFound) {
target = Option{Key: "console_setting.uptime_kuma_groups"}
}
target.Value = value
if err := tx.Save(&target).Error; err != nil {
return fmt.Errorf("write target option console_setting.uptime_kuma_groups: %w", err)
}
if err := tx.Delete(&urlOption).Error; err != nil {
return err
}
return tx.Delete(&slugOption).Error
})
}