Files
OpenFlare/internal/apps/openflare/option/logics.go
T
ryan 498a9ed3ff fix(openflare): Agent 上报 IP 后由 Server 自动解析节点地理位置
- 启动时按 of_options.GeoIPProvider 初始化 pkg/geoip(bootstrap + runtime)
- mmdb 模式从内置 GeoLite2 种子到 data/;保存归属方式后热刷新 Provider
- Agent/Relay 心跳在服务端根据 IP 写入 geo 字段,尊重 geo_manual_override
- ipinfo 归属名称改为 City, Region, Country 可读格式
2026-06-20 19:36:17 +08:00

252 lines
7.2 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package option
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/geoip"
oftasks "github.com/Rain-kl/Wavelet/internal/apps/openflare/tasks"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/uptimekuma"
"github.com/Rain-kl/Wavelet/internal/buildinfo"
"github.com/Rain-kl/Wavelet/internal/model"
)
var (
initOnce sync.Once
initErr error
)
// EnsureInitialized loads OptionMap from defaults and database once per process.
func EnsureInitialized(ctx context.Context) error {
initOnce.Do(func() {
initErr = model.InitOptionMap(ctx)
})
return initErr
}
// ResetInitializationForTest clears lazy-init state for unit tests.
func ResetInitializationForTest() {
initOnce = sync.Once{}
initErr = nil
model.ResetOptionMapForTest()
}
type publicAuthSourceView struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
AuthorizeURL string `json:"authorize_url"`
IconURL string `json:"icon_url"`
}
type statusView struct {
Version string `json:"version"`
StartTime int64 `json:"start_time"`
EmailVerification bool `json:"email_verification"`
GitHubOAuth bool `json:"github_oauth"`
GitHubClientID string `json:"github_client_id"`
SystemName string `json:"system_name"`
HomePageLink string `json:"home_page_link"`
FooterHTML string `json:"footer_html"`
WeChatQRCode string `json:"wechat_qrcode"`
WeChatLogin bool `json:"wechat_login"`
ServerAddress string `json:"server_address"`
PasswordRegisterEnabled bool `json:"password_register_enabled"`
CapLoginEnabled bool `json:"cap_login_enabled"`
AuthSources []publicAuthSourceView `json:"auth_sources"`
}
type geoIPLookupRequest struct {
Provider string `json:"provider"`
IP string `json:"ip"`
}
type geoIPLookupView struct {
Provider string `json:"provider"`
IP string `json:"ip"`
ISOCode string `json:"iso_code"`
Name string `json:"name"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
}
type databaseCleanupInput struct {
Target string `json:"target"`
RetentionDays *int `json:"retention_days"`
}
type databaseCleanupResult struct {
Target string `json:"target"`
TargetLabel string `json:"target_label"`
DeletedCount int64 `json:"deleted_count"`
DeleteAll bool `json:"delete_all"`
RetentionDays *int `json:"retention_days,omitempty"`
}
type optionBatchPayload struct {
Options []model.OpenFlareOption `json:"options"`
}
func listOptions(ctx context.Context) ([]model.OpenFlareOption, error) {
if err := EnsureInitialized(ctx); err != nil {
return nil, err
}
model.OptionMapRWMutex.RLock()
defer model.OptionMapRWMutex.RUnlock()
options := make([]model.OpenFlareOption, 0, len(model.OptionMap))
for key, value := range model.OptionMap {
if isSecretOptionKey(key) {
continue
}
options = append(options, model.OpenFlareOption{
Key: key,
Value: value,
})
}
return options, nil
}
func updateOption(ctx context.Context, option model.OpenFlareOption) error {
if err := EnsureInitialized(ctx); err != nil {
return err
}
return updateOptions(ctx, []model.OpenFlareOption{option})
}
func updateOptionsBatch(ctx context.Context, payload optionBatchPayload) error {
if err := EnsureInitialized(ctx); err != nil {
return err
}
if len(payload.Options) == 0 {
return errors.New(errInvalidParams)
}
return updateOptions(ctx, payload.Options)
}
func updateOptions(ctx context.Context, options []model.OpenFlareOption) error {
if err := validateOptions(options); err != nil {
return err
}
if err := model.UpdateOpenFlareOptions(ctx, options); err != nil {
return err
}
for _, item := range options {
if item.Key == "GeoIPProvider" {
return geoip.RefreshRuntimeProvider(ctx)
}
}
return nil
}
func getNotice(ctx context.Context) (string, error) {
if err := EnsureInitialized(ctx); err != nil {
return "", err
}
return model.OptionValue("Notice"), nil
}
func getStatus(ctx context.Context, baseAPIPath string) (*statusView, error) {
if err := EnsureInitialized(ctx); err != nil {
return nil, err
}
authSources, err := publicAuthSources(ctx, baseAPIPath)
if err != nil {
authSources = []publicAuthSourceView{}
}
return &statusView{
Version: buildinfo.Version,
StartTime: model.StartTime,
EmailVerification: model.EmailVerificationEnabled,
GitHubOAuth: model.GitHubOAuthEnabled,
GitHubClientID: model.GitHubClientID,
SystemName: model.SystemName,
HomePageLink: model.HomePageLink,
FooterHTML: model.Footer,
WeChatQRCode: model.WeChatAccountQRCodeImageURL,
WeChatLogin: model.WeChatAuthEnabled,
ServerAddress: model.ServerAddress,
PasswordRegisterEnabled: model.PasswordRegisterEnabled,
CapLoginEnabled: model.CapLoginEnabled,
AuthSources: authSources,
}, nil
}
func publicAuthSources(ctx context.Context, baseAPIPath string) ([]publicAuthSourceView, error) {
sources, err := model.GetActiveAuthSources(ctx)
if err != nil {
return nil, err
}
result := make([]publicAuthSourceView, 0, len(sources))
base := strings.TrimRight(baseAPIPath, "/")
for _, source := range sources {
result = append(result, publicAuthSourceView{
ID: source.ID,
Name: source.Name,
Type: source.Type,
DisplayName: source.DisplayName,
AuthorizeURL: fmt.Sprintf("%s/oauth/%s/authorize", base, source.Name),
IconURL: source.IconURL,
})
}
return result, nil
}
func lookupGeoIP(_ context.Context, provider, rawIP string) (*geoIPLookupView, error) {
view, err := geoip.Lookup(provider, rawIP)
if err != nil {
return nil, err
}
return &geoIPLookupView{
Provider: view.Provider,
IP: view.IP,
ISOCode: view.ISOCode,
Name: view.Name,
Latitude: view.Latitude,
Longitude: view.Longitude,
}, nil
}
func cleanupDatabaseObservability(ctx context.Context, input databaseCleanupInput) (*databaseCleanupResult, error) {
target := strings.TrimSpace(input.Target)
if target == "" {
return nil, errors.New(errInvalidParams)
}
result, err := oftasks.CleanupDatabaseObservability(ctx, oftasks.DatabaseCleanupInput{
Target: target,
RetentionDays: input.RetentionDays,
})
if err != nil {
return nil, err
}
return &databaseCleanupResult{
Target: result.Target,
TargetLabel: result.TargetLabel,
DeletedCount: result.DeletedCount,
DeleteAll: result.DeleteAll,
RetentionDays: result.RetentionDays,
}, nil
}
func syncUptimeKuma(ctx context.Context) error {
return uptimekuma.SyncToUptimeKuma(ctx)
}
func isSecretOptionKey(key string) bool {
return strings.Contains(key, "Token") ||
strings.Contains(key, "Secret") ||
strings.Contains(key, "Password")
}