mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
498a9ed3ff
- 启动时按 of_options.GeoIPProvider 初始化 pkg/geoip(bootstrap + runtime) - mmdb 模式从内置 GeoLite2 种子到 data/;保存归属方式后热刷新 Provider - Agent/Relay 心跳在服务端根据 IP 写入 geo 字段,尊重 geo_manual_override - ipinfo 归属名称改为 City, Region, Country 可读格式
252 lines
7.2 KiB
Go
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")
|
|
}
|