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
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user