mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 01:06:36 +08:00
fix: preserve license state across panel upgrades (#544)
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
|||||||
"go-backend/internal/health"
|
"go-backend/internal/health"
|
||||||
"go-backend/internal/http/middleware"
|
"go-backend/internal/http/middleware"
|
||||||
"go-backend/internal/http/response"
|
"go-backend/internal/http/response"
|
||||||
"go-backend/internal/license"
|
|
||||||
"go-backend/internal/metrics"
|
"go-backend/internal/metrics"
|
||||||
"go-backend/internal/monitoring"
|
"go-backend/internal/monitoring"
|
||||||
runtimenft "go-backend/internal/runtime/nftables"
|
runtimenft "go-backend/internal/runtime/nftables"
|
||||||
@@ -909,38 +908,15 @@ func (h *Handler) licenseActivate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := "1bc96cac-09de-4cf4-af34-26afdad63a90"
|
valResp, err := h.validateLicenseForMachine(key)
|
||||||
|
|
||||||
fingerprint, err := h.getOrCreateMachineFingerprint()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.ErrDefault("生成设备指纹失败"))
|
response.WriteJSON(w, response.ErrDefault("授权校验失败: "+err.Error()))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := license.NewKeygenClient(accountID, "")
|
|
||||||
valResp, err := client.ValidateKeyWithFingerprint(key, fingerprint)
|
|
||||||
if err != nil {
|
|
||||||
response.WriteJSON(w, response.ErrDefault("连接授权服务器失败: "+err.Error()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !valResp.Meta.Valid {
|
if !valResp.Meta.Valid {
|
||||||
if valResp.Meta.Code == "NO_MACHINES" || valResp.Meta.Code == "NO_MACHINE" || valResp.Meta.Code == "MACHINE_SCOPE_REQUIRED" || valResp.Meta.Code == "FINGERPRINT_SCOPE_MISMATCH" {
|
response.WriteJSON(w, response.ErrDefault("授权码无效或已过期 (Code: "+valResp.Meta.Code+")"))
|
||||||
// Needs machine activation
|
return
|
||||||
client.Token = key
|
|
||||||
err = client.ActivateMachine(valResp.Data.ID, fingerprint)
|
|
||||||
if err != nil {
|
|
||||||
// Translate specific error messages or log them
|
|
||||||
response.WriteJSON(w, response.ErrDefault("设备绑定失败: "+err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation might still fail with scope if we don't query via machine id, but since activate machine succeeded
|
|
||||||
// we can consider the license valid for our simple usecase
|
|
||||||
} else {
|
|
||||||
response.WriteJSON(w, response.ErrDefault("授权码无效或已过期 (Code: "+valResp.Meta.Code+")"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-backend/internal/license"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var nftablesTrafficCollectInterval = 30 * time.Second
|
var nftablesTrafficCollectInterval = 30 * time.Second
|
||||||
@@ -56,8 +54,6 @@ func (h *Handler) validateLicenseJob() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := "1bc96cac-09de-4cf4-af34-26afdad63a90"
|
|
||||||
|
|
||||||
key, _ := h.repo.GetViteConfigValue("license_key")
|
key, _ := h.repo.GetViteConfigValue("license_key")
|
||||||
isCommercial, _ := h.repo.GetViteConfigValue("is_commercial")
|
isCommercial, _ := h.repo.GetViteConfigValue("is_commercial")
|
||||||
|
|
||||||
@@ -65,12 +61,16 @@ func (h *Handler) validateLicenseJob() {
|
|||||||
return // Nothing to validate
|
return // Nothing to validate
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprint, _ := h.repo.GetViteConfigValue("machine_fingerprint")
|
valResp, err := h.validateLicenseForMachine(key)
|
||||||
client := license.NewKeygenClient(accountID, "")
|
|
||||||
valResp, err := client.ValidateKeyWithFingerprint(key, fingerprint)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Network error or timeout. Grace period by not revoking immediately here.
|
// Network and decode failures have no validation response, so retain the
|
||||||
|
// current state as a grace period. A rejected machine binding still has
|
||||||
|
// the original invalid response and must not stay commercially enabled.
|
||||||
|
if valResp != nil && !valResp.Meta.Valid {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
_ = h.repo.UpsertConfig("is_commercial", "false", now)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go-backend/internal/license"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keygenAccountID = "1bc96cac-09de-4cf4-af34-26afdad63a90"
|
||||||
|
|
||||||
|
var newLicenseClient = license.NewKeygenClient
|
||||||
|
|
||||||
|
func licenseNeedsMachineActivation(code string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(code)) {
|
||||||
|
case "NO_MACHINES", "NO_MACHINE", "MACHINE_SCOPE_REQUIRED", "FINGERPRINT_SCOPE_MISMATCH":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) validateLicenseForMachine(key string) (*license.ValidateResponse, error) {
|
||||||
|
fingerprint, err := h.getOrCreateMachineFingerprint()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare machine fingerprint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := newLicenseClient(keygenAccountID, "")
|
||||||
|
validation, err := client.ValidateKeyWithFingerprint(key, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if validation.Meta.Valid || !licenseNeedsMachineActivation(validation.Meta.Code) {
|
||||||
|
return validation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Token = key
|
||||||
|
if err := client.ActivateMachine(validation.Data.ID, fingerprint); err != nil {
|
||||||
|
return validation, err
|
||||||
|
}
|
||||||
|
|
||||||
|
validation, err = client.ValidateKeyWithFingerprint(key, fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return validation, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend/internal/license"
|
||||||
|
"go-backend/internal/store/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateLicenseJobRepairsMissingMachineBinding(t *testing.T) {
|
||||||
|
r := openLicenseTestRepository(t)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
seedLicenseConfig(t, r, "license_key", "license-secret", now)
|
||||||
|
seedLicenseConfig(t, r, "is_commercial", "true", now)
|
||||||
|
|
||||||
|
var validations atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/licenses/actions/validate-key"):
|
||||||
|
if validations.Add(1) == 1 {
|
||||||
|
_, _ = fmt.Fprint(w, `{"meta":{"valid":false,"code":"NO_MACHINE"},"data":{"id":"license-id","attributes":{}}}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprint(w, `{"meta":{"valid":true,"code":"VALID"},"data":{"id":"license-id","attributes":{"expiry":"2030-01-02T00:00:00.000Z"}}}`)
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/machines"):
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = fmt.Fprint(w, `{}`)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
restoreLicenseClientFactory(t, server.URL)
|
||||||
|
|
||||||
|
h := &Handler{repo: r}
|
||||||
|
h.validateLicenseJob()
|
||||||
|
|
||||||
|
assertLicenseConfig(t, r, "is_commercial", "true")
|
||||||
|
assertLicenseConfig(t, r, "license_expiry", "2030-01-02T00:00:00.000Z")
|
||||||
|
fingerprint, err := r.GetViteConfigValue("machine_fingerprint")
|
||||||
|
if err != nil || strings.TrimSpace(fingerprint) == "" {
|
||||||
|
t.Fatalf("expected persisted machine fingerprint, got value=%q err=%v", fingerprint, err)
|
||||||
|
}
|
||||||
|
if got := validations.Load(); got != 2 {
|
||||||
|
t.Fatalf("validation calls = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLicenseActivateRequiresSuccessfulPostActivationValidation(t *testing.T) {
|
||||||
|
r := openLicenseTestRepository(t)
|
||||||
|
|
||||||
|
var validations atomic.Int32
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/licenses/actions/validate-key"):
|
||||||
|
code := "NO_MACHINE"
|
||||||
|
if validations.Add(1) > 1 {
|
||||||
|
code = "FINGERPRINT_SCOPE_MISMATCH"
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `{"meta":{"valid":false,"code":%q},"data":{"id":"license-id","attributes":{}}}`, code)
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/machines"):
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = fmt.Fprint(w, `{}`)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
restoreLicenseClientFactory(t, server.URL)
|
||||||
|
|
||||||
|
h := &Handler{repo: r}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/license/activate", bytes.NewBufferString(`{"license_key":"license-secret"}`))
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
h.licenseActivate(res, req)
|
||||||
|
|
||||||
|
if !strings.Contains(res.Body.String(), "FINGERPRINT_SCOPE_MISMATCH") {
|
||||||
|
t.Fatalf("expected post-activation validation failure, got %s", res.Body.String())
|
||||||
|
}
|
||||||
|
if value, err := r.GetViteConfigValue("is_commercial"); err == nil || value != "" {
|
||||||
|
t.Fatalf("commercial status should not be persisted, got value=%q err=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateLicenseJobDowngradesWhenMachineBindingIsRejected(t *testing.T) {
|
||||||
|
r := openLicenseTestRepository(t)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
seedLicenseConfig(t, r, "license_key", "license-secret", now)
|
||||||
|
seedLicenseConfig(t, r, "is_commercial", "true", now)
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/licenses/actions/validate-key"):
|
||||||
|
_, _ = fmt.Fprint(w, `{"meta":{"valid":false,"code":"NO_MACHINE"},"data":{"id":"license-id","attributes":{}}}`)
|
||||||
|
case strings.HasSuffix(req.URL.Path, "/machines"):
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
_, _ = fmt.Fprint(w, `{"errors":[{"code":"MACHINE_LIMIT_EXCEEDED"}]}`)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
restoreLicenseClientFactory(t, server.URL)
|
||||||
|
|
||||||
|
h := &Handler{repo: r}
|
||||||
|
h.validateLicenseJob()
|
||||||
|
|
||||||
|
assertLicenseConfig(t, r, "is_commercial", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
func openLicenseTestRepository(t *testing.T) *repo.Repository {
|
||||||
|
t.Helper()
|
||||||
|
r, err := repo.Open(filepath.Join(t.TempDir(), "license.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("repo.Open() error = %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedLicenseConfig(t *testing.T, r *repo.Repository, name, value string, now int64) {
|
||||||
|
t.Helper()
|
||||||
|
if err := r.UpsertConfig(name, value, now); err != nil {
|
||||||
|
t.Fatalf("UpsertConfig(%q) error = %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertLicenseConfig(t *testing.T, r *repo.Repository, name, want string) {
|
||||||
|
t.Helper()
|
||||||
|
got, err := r.GetViteConfigValue(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetViteConfigValue(%q) error = %v", name, err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("config %q = %q, want %q", name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreLicenseClientFactory(t *testing.T, baseURL string) {
|
||||||
|
t.Helper()
|
||||||
|
previous := newLicenseClient
|
||||||
|
newLicenseClient = func(accountID, token string) *license.KeygenClient {
|
||||||
|
client := license.NewKeygenClient(accountID, token)
|
||||||
|
client.BaseURL = baseURL
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { newLicenseClient = previous })
|
||||||
|
}
|
||||||
@@ -13,17 +13,29 @@ import (
|
|||||||
type KeygenClient struct {
|
type KeygenClient struct {
|
||||||
AccountID string
|
AccountID string
|
||||||
Token string
|
Token string
|
||||||
|
BaseURL string
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultAPIBaseURL = "https://api.keygen.sh/v1"
|
||||||
|
|
||||||
func NewKeygenClient(accountID, token string) *KeygenClient {
|
func NewKeygenClient(accountID, token string) *KeygenClient {
|
||||||
return &KeygenClient{
|
return &KeygenClient{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
BaseURL: defaultAPIBaseURL,
|
||||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *KeygenClient) apiURL(path string) string {
|
||||||
|
baseURL := strings.TrimRight(c.BaseURL, "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = defaultAPIBaseURL
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/accounts/%s/%s", baseURL, c.AccountID, strings.TrimLeft(path, "/"))
|
||||||
|
}
|
||||||
|
|
||||||
type ValidateResponse struct {
|
type ValidateResponse struct {
|
||||||
Meta struct {
|
Meta struct {
|
||||||
Valid bool `json:"valid"`
|
Valid bool `json:"valid"`
|
||||||
@@ -55,7 +67,7 @@ type ActivateMachineRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *KeygenClient) ValidateKeyWithFingerprint(key string, fingerprint string) (*ValidateResponse, error) {
|
func (c *KeygenClient) ValidateKeyWithFingerprint(key string, fingerprint string) (*ValidateResponse, error) {
|
||||||
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/licenses/actions/validate-key", c.AccountID)
|
url := c.apiURL("licenses/actions/validate-key")
|
||||||
|
|
||||||
meta := map[string]interface{}{
|
meta := map[string]interface{}{
|
||||||
"key": key,
|
"key": key,
|
||||||
@@ -103,7 +115,7 @@ func (c *KeygenClient) ValidateKeyWithFingerprint(key string, fingerprint string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *KeygenClient) ValidateKey(key string) (*ValidateResponse, error) {
|
func (c *KeygenClient) ValidateKey(key string) (*ValidateResponse, error) {
|
||||||
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/licenses/actions/validate-key", c.AccountID)
|
url := c.apiURL("licenses/actions/validate-key")
|
||||||
|
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]interface{}{
|
||||||
"meta": map[string]string{
|
"meta": map[string]string{
|
||||||
@@ -142,7 +154,7 @@ func (c *KeygenClient) ValidateKey(key string) (*ValidateResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *KeygenClient) ActivateMachine(licenseID, fingerprint string) error {
|
func (c *KeygenClient) ActivateMachine(licenseID, fingerprint string) error {
|
||||||
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/machines", c.AccountID)
|
url := c.apiURL("machines")
|
||||||
|
|
||||||
var reqBody ActivateMachineRequest
|
var reqBody ActivateMachineRequest
|
||||||
reqBody.Data.Type = "machines"
|
reqBody.Data.Type = "machines"
|
||||||
@@ -174,14 +186,6 @@ func (c *KeygenClient) ActivateMachine(licenseID, fingerprint string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusConflict || resp.StatusCode == http.StatusUnprocessableEntity {
|
|
||||||
if strings.Contains(string(body), "FINGERPRINT_TAKEN") || strings.Contains(string(body), "MACHINE_LIMIT_EXCEEDED") {
|
|
||||||
// Machine already registered to this license or limit reached because it's already us.
|
|
||||||
// The subsequent ValidateKey check will determine if the existing machine is actually us.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to activate machine: status %d, response: %s", resp.StatusCode, string(body))
|
return fmt.Errorf("failed to activate machine: status %d, response: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,30 @@ const readCachedConfigs = (keys: readonly string[]) => {
|
|||||||
return { cachedConfigs, hasCachedData };
|
return { cachedConfigs, hasCachedData };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readAllCachedSafeConfigs = () => {
|
||||||
|
const cachedConfigs: Record<string, string> = {};
|
||||||
|
|
||||||
|
Object.keys(localStorage).forEach((storageKey) => {
|
||||||
|
if (!storageKey.startsWith(CACHE_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = storageKey.slice(CACHE_PREFIX.length).trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!key || SENSITIVE_CONFIG_KEYS.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (value !== null) {
|
||||||
|
cachedConfigs[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cachedConfigs;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchPublicBrandConfigs = async (): Promise<Record<string, string>> => {
|
const fetchPublicBrandConfigs = async (): Promise<Record<string, string>> => {
|
||||||
const publicConfigMap: Record<string, string> = {};
|
const publicConfigMap: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -206,20 +230,24 @@ export const getCachedConfig = async (key: string): Promise<string | null> => {
|
|||||||
|
|
||||||
// 获取所有配置(优先从缓存)
|
// 获取所有配置(优先从缓存)
|
||||||
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
||||||
const { cachedConfigs, hasCachedData } = readCachedConfigs(
|
const {
|
||||||
PUBLIC_BRAND_CONFIG_KEYS,
|
cachedConfigs: publicCachedConfigs,
|
||||||
);
|
hasCachedData: hasPublicCachedData,
|
||||||
|
} = readCachedConfigs(PUBLIC_BRAND_CONFIG_KEYS);
|
||||||
|
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
const publicConfigs = await fetchPublicBrandConfigs();
|
const publicConfigs = await fetchPublicBrandConfigs();
|
||||||
|
|
||||||
if (Object.keys(publicConfigs).length > 0) {
|
if (Object.keys(publicConfigs).length > 0) {
|
||||||
return { ...cachedConfigs, ...publicConfigs };
|
return { ...publicCachedConfigs, ...publicConfigs };
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedConfigs;
|
return publicCachedConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedConfigs = readAllCachedSafeConfigs();
|
||||||
|
const hasCachedData = Object.keys(cachedConfigs).length > 0;
|
||||||
|
|
||||||
// 从API获取最新配置
|
// 从API获取最新配置
|
||||||
try {
|
try {
|
||||||
const response = await getConfigs();
|
const response = await getConfigs();
|
||||||
@@ -249,14 +277,20 @@ export const getCachedConfigs = async (): Promise<Record<string, string>> => {
|
|||||||
return cachedConfigs;
|
return cachedConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetchPublicBrandConfigs();
|
const publicConfigs = await fetchPublicBrandConfigs();
|
||||||
|
|
||||||
|
return { ...publicCachedConfigs, ...publicConfigs };
|
||||||
} catch {
|
} catch {
|
||||||
// API失败时返回缓存的数据
|
// API失败时返回缓存的数据
|
||||||
if (hasCachedData) {
|
if (hasCachedData) {
|
||||||
return cachedConfigs;
|
return cachedConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetchPublicBrandConfigs();
|
const publicConfigs = await fetchPublicBrandConfigs();
|
||||||
|
|
||||||
|
return hasPublicCachedData
|
||||||
|
? { ...publicCachedConfigs, ...publicConfigs }
|
||||||
|
: publicConfigs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -365,8 +399,17 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
|
|||||||
siteConfig.app_logo = appLogo;
|
siteConfig.app_logo = appLogo;
|
||||||
siteConfig.app_favicon = appFavicon;
|
siteConfig.app_favicon = appFavicon;
|
||||||
siteConfig.app_bg_image = appBgImage;
|
siteConfig.app_bg_image = appBgImage;
|
||||||
siteConfig.is_commercial = resolvedConfigMap.is_commercial === "true";
|
if (
|
||||||
siteConfig.hide_footer_brand = resolvedConfigMap.hide_footer_brand === "true";
|
Object.prototype.hasOwnProperty.call(resolvedConfigMap, "is_commercial")
|
||||||
|
) {
|
||||||
|
siteConfig.is_commercial = resolvedConfigMap.is_commercial === "true";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(resolvedConfigMap, "hide_footer_brand")
|
||||||
|
) {
|
||||||
|
siteConfig.hide_footer_brand =
|
||||||
|
resolvedConfigMap.hide_footer_brand === "true";
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.title = siteConfig.name;
|
document.title = siteConfig.name;
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ const getInitialConfigs = (): Record<string, string> => {
|
|||||||
"github_proxy_enabled",
|
"github_proxy_enabled",
|
||||||
"github_proxy_url",
|
"github_proxy_url",
|
||||||
"allow_local_remote_addr",
|
"allow_local_remote_addr",
|
||||||
|
"is_commercial",
|
||||||
|
"license_expiry",
|
||||||
|
"hide_footer_brand",
|
||||||
];
|
];
|
||||||
const initialConfigs: Record<string, string> = {};
|
const initialConfigs: Record<string, string> = {};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user