refactor: 重写Config配置管理

This commit is contained in:
Akizon77
2025-11-15 17:18:21 +08:00
parent 0c8095ae11
commit 3729269fa3
52 changed files with 1178 additions and 695 deletions
+2 -2
View File
@@ -101,10 +101,10 @@ jobs:
fi
VERSION="${{ github.sha }}"
VERSION_HASH="${{ github.sha }}"
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}" -o $BINARY_NAME
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}" -o $BINARY_NAME
- name: Upload binary as artifact
uses: actions/upload-artifact@v4
with:
name: komari-${{ matrix.goos }}-${{ matrix.goarch }}
path: komari-${{ matrix.goos }}-${{ matrix.goarch }}*
path: komari-${{ matrix.goos }}-${{ matrix.goarch }}*
+4 -4
View File
@@ -12,13 +12,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v4
#- name: Create dummy index.html
# run: |
# mkdir -p public/dist
# echo "<html><body><h1>This is komari development server.</h1></body></html>" > public/dist/index.html
- name: Clone and build frontend
run: |
git clone https://github.com/komari-monitor/komari-web web
@@ -40,7 +40,7 @@ jobs:
GOARCH: amd64
run: |
VERSION_SHA="${{ github.sha }}"
go build -ldflags "-X github.com/komari-monitor/komari/internal/version.CurrentVersion=dev -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_SHA}" -o komari .
go build -ldflags "-X github.com/komari-monitor/komari/internal/conf.Version=dev -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_SHA}" -o komari .
- name: Upload binary as artifact
uses: actions/upload-artifact@v4
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
run: |
VERSION="${{ github.ref_name }}"
VERSION_HASH="${{ github.sha }}"
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}"
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}"
echo "Building for linux/amd64..."
GOARCH=amd64 CC="zig cc -target x86_64-linux-musl" go build -trimpath -ldflags="$LDFLAGS" -o komari-linux-amd64
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION_HASH="${{ github.sha }}"
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}"
LDFLAGS="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}"
echo "Building for linux/amd64..."
GOARCH=amd64 CC="zig cc -target x86_64-linux-musl" go build -trimpath -ldflags="$LDFLAGS" -o komari-linux-amd64
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
fi
VERSION="${{ github.event.release.tag_name }}"
VERSION_HASH="${{ github.sha }}" # Use commit SHA for hash
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/version.CurrentVersion=${VERSION} -X github.com/komari-monitor/komari/internal/version.VersionHash=${VERSION_HASH}" -o $BINARY_NAME
go build -trimpath -ldflags="-s -w -X github.com/komari-monitor/komari/internal/conf.Version=${VERSION} -X github.com/komari-monitor/komari/internal/conf.CommitHash=${VERSION_HASH}" -o $BINARY_NAME
- name: Upload binary to release
env:
+2 -2
View File
@@ -9,6 +9,6 @@ var (
DatabaseUser string // MySQL/其他数据库用户名
DatabasePass string // MySQL/其他数据库密码
DatabaseName string // MySQL/其他数据库名称
Listen string
ConfigFile string // 配置文件路径
Listen string
)
+3 -3
View File
@@ -13,8 +13,8 @@ import (
apiClient "github.com/komari-monitor/komari/internal/api_v1/client"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/geoip"
@@ -370,7 +370,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
if in != nil && in.Ip != nil {
if v4 := strings.TrimSpace(in.Ip.Ipv4); v4 != "" {
updates["ipv4"] = v4
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
if ip := net.ParseIP(v4); ip != nil {
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
iso = gi.ISOCode
@@ -381,7 +381,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
if v6 := strings.TrimSpace(in.Ip.Ipv6); v6 != "" {
updates["ipv6"] = v6
if iso == "" { // 优先使用 v4 的国家码
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
if ip := net.ParseIP(v6); ip != nil {
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
iso = gi.ISOCode
+2 -2
View File
@@ -3,8 +3,8 @@ package cmd
import (
"os"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/spf13/cobra"
"gorm.io/gorm"
)
@@ -16,7 +16,7 @@ var PermitPasswordLoginCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
db := dbcore.GetDBInstance()
err := db.Transaction(func(tx *gorm.DB) error {
return tx.Model(&models.Config{}).Where("id = ?", 1).
return tx.Model(&conf.V1Struct{}).Where("id = ?", 1).
Update("disable_password_login", false).Error
})
if err != nil {
+9 -7
View File
@@ -21,13 +21,14 @@ func GetEnv(key, defaultValue string) string {
// 从环境变量获取默认值
var (
dbTypeEnv = GetEnv("KOMARI_DB_TYPE", "sqlite")
dbFileEnv = GetEnv("KOMARI_DB_FILE", "./data/komari.db")
dbHostEnv = GetEnv("KOMARI_DB_HOST", "localhost")
dbPortEnv = GetEnv("KOMARI_DB_PORT", "3306")
dbUserEnv = GetEnv("KOMARI_DB_USER", "root")
dbPassEnv = GetEnv("KOMARI_DB_PASS", "")
dbNameEnv = GetEnv("KOMARI_DB_NAME", "komari")
dbTypeEnv = GetEnv("KOMARI_DB_TYPE", "sqlite")
dbFileEnv = GetEnv("KOMARI_DB_FILE", "./data/komari.db")
dbHostEnv = GetEnv("KOMARI_DB_HOST", "localhost")
dbPortEnv = GetEnv("KOMARI_DB_PORT", "3306")
dbUserEnv = GetEnv("KOMARI_DB_USER", "root")
dbPassEnv = GetEnv("KOMARI_DB_PASS", "")
dbNameEnv = GetEnv("KOMARI_DB_NAME", "komari")
configFileEnv = GetEnv("KOMARI_CONFIG_FILE", "./data/komari.json")
)
var RootCmd = &cobra.Command{
@@ -59,4 +60,5 @@ func init() {
RootCmd.PersistentFlags().StringVar(&flags.DatabaseUser, "db-user", dbUserEnv, "MySQL/Other database username [env: KOMARI_DB_USER]")
RootCmd.PersistentFlags().StringVar(&flags.DatabasePass, "db-pass", dbPassEnv, "MySQL/Other database password [env: KOMARI_DB_PASS]")
RootCmd.PersistentFlags().StringVar(&flags.DatabaseName, "db-name", dbNameEnv, "MySQL/Other database name [env: KOMARI_DB_NAME]")
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "c", configFileEnv, "Configuration file path [env: KOMARI_CONFIG_FILE]")
}
+26 -19
View File
@@ -15,10 +15,10 @@ import (
"github.com/gookit/event"
"github.com/komari-monitor/komari/cmd/flags"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
d_notification "github.com/komari-monitor/komari/internal/database/notification"
@@ -30,7 +30,8 @@ import (
"github.com/komari-monitor/komari/internal/messageSender"
"github.com/komari-monitor/komari/internal/notifier"
"github.com/komari-monitor/komari/internal/oauth"
"github.com/komari-monitor/komari/internal/version"
"github.com/komari-monitor/komari/internal/patch"
"github.com/komari-monitor/komari/internal/restore"
"github.com/komari-monitor/komari/pkg/cloudflared"
"github.com/komari-monitor/komari/server"
"github.com/spf13/cobra"
@@ -58,13 +59,19 @@ func RunServer() {
if err := os.MkdirAll("./data/theme", os.ModePerm); err != nil {
log.Fatalf("Failed to create theme directory: %v", err)
}
// 进行备份恢复
if restore.NeedBackupRestore() {
restore.RestoreBackup()
}
conf.Load()
InitDatabase()
patch.ApplyPatch()
if version.VersionHash != "unknown" {
if conf.Version != conf.Version_Development {
gin.SetMode(gin.ReleaseMode)
}
conf, err := config.Get()
config, err := conf.GetWithV1Format()
if err != nil {
log.Fatal(err)
}
@@ -73,21 +80,20 @@ func RunServer() {
r.Use(logutil.GinLogger())
r.Use(logutil.GinRecovery())
event.Trigger(eventType.ServerInitializeStart, event.M{"config": conf, "engine": r})
defer event.Trigger(eventType.ServerInitializeDone, event.M{"config": conf})
event.Trigger(eventType.ServerInitializeStart, event.M{"config": config, "engine": r})
go geoip.InitGeoIp()
go DoScheduledWork()
go messageSender.Initialize()
go oauth.Initialize()
server.StartNezhaGRPCServer(conf.NezhaCompatListen)
server.StartNezhaGRPCServer(config.NezhaCompatListen)
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
newConf := e.Get("new").(models.Config)
oldConf := e.Get("old").(models.Config)
if newConf.OAuthProvider != oldConf.OAuthProvider {
oidcProvider, err := database.GetOidcConfigByName(newConf.OAuthProvider)
newConf := e.Get("new").(conf.Config)
oldConf := e.Get("old").(conf.Config)
if newConf.Login.OAuthProvider != oldConf.Login.OAuthProvider {
oidcProvider, err := database.GetOidcConfigByName(newConf.Login.OAuthProvider)
if err != nil {
log.Printf("Failed to get OIDC provider config: %v", err)
} else {
@@ -98,7 +104,7 @@ func RunServer() {
auditlog.EventLog("error", fmt.Sprintf("Failed to load OIDC provider: %v", err))
}
}
if newConf.NotificationMethod != oldConf.NotificationMethod {
if newConf.Notification.NotificationMethod != oldConf.Notification.NotificationMethod {
messageSender.Initialize()
}
return nil
@@ -118,13 +124,14 @@ func RunServer() {
Addr: flags.Listen,
Handler: r,
}
event.Trigger(eventType.ServerInitializeDone, event.M{"config": config})
log.Printf("Starting server on %s ...", flags.Listen)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
OnFatal(err)
log.Fatalf("listen: %s\n", err)
}
}()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
OnFatal(err)
log.Fatalf("listen: %s\n", err)
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
@@ -168,7 +175,7 @@ func DoScheduledWork() {
minute := time.NewTicker(60 * time.Second)
//records.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 30))
records.CompactRecord()
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
go notifier.CheckExpireScheduledWork()
for {
select {
+3 -4
View File
@@ -11,10 +11,9 @@ import (
"sync"
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/ws"
"github.com/komari-monitor/komari/pkg/rpc"
)
@@ -26,7 +25,7 @@ func RegisterRouters(path string, r *gin.Engine) {
// Json Rpc2 over websocket, /api/rpc2
func OnRpcRequest(c *gin.Context) {
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
// GET -> WebSocket
if c.Request.Method == http.MethodGet {
@@ -123,7 +122,7 @@ func OnRpcRequest(c *gin.Context) {
}
// detectPermissionGroup 提取权限分组,与原逻辑保持一致
func detectPermissionGroup(c *gin.Context, cfg models.Config) string {
func detectPermissionGroup(c *gin.Context, cfg conf.V1Struct) string {
permissionGroup := "guest"
token := c.Query("Authorization")
if _, err := clients.GetClientUUIDByToken(token); err == nil {
+4 -5
View File
@@ -10,13 +10,12 @@ import (
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/database/tasks"
"github.com/komari-monitor/komari/internal/version"
"github.com/komari-monitor/komari/internal/ws"
"github.com/komari-monitor/komari/pkg/rpc"
@@ -229,7 +228,7 @@ func getNodes(ctx context.Context, req *rpc.JsonRpcRequest) (any, *rpc.JsonRpcEr
}
meta := rpc.MetaFromContext(ctx)
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
if meta.Permission != "admin" {
// 过滤 Hidden 节点并隐藏敏感字段
filtered := make([]models.Client, 0, len(cinfo))
@@ -455,8 +454,8 @@ func getVersion(_ context.Context, _ *rpc.JsonRpcRequest) (any, *rpc.JsonRpcErro
Version string `json:"version"`
Hash string `json:"hash"`
}{
Version: version.CurrentVersion,
Hash: version.VersionHash,
Version: conf.Version,
Hash: conf.CommitHash,
}, nil
}
+4 -5
View File
@@ -9,12 +9,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/version"
"github.com/patrickmn/go-cache"
"strconv"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/pkg/utils"
@@ -141,13 +140,13 @@ func RespondError(c *gin.Context, httpStatus int, message string) {
}
func GetVersion(c *gin.Context) {
RespondSuccess(c, gin.H{
"version": version.CurrentVersion,
"hash": version.VersionHash,
"version": conf.Version,
"hash": conf.CommitHash,
})
}
func isApiKeyValid(apiKey string) bool {
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
return false
}
+2 -2
View File
@@ -3,8 +3,8 @@ package api_v1
import (
"net/http"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/gin-gonic/gin"
)
@@ -46,7 +46,7 @@ func PrivateSiteMiddleware() gin.HandlerFunc {
c.Next()
return
}
conf, err := config.Get()
conf, err := conf.GetWithV1Format()
if err != nil {
RespondError(c, http.StatusInternalServerError, "Failed to get configuration.")
c.Abort()
+2 -2
View File
@@ -3,8 +3,8 @@ package admin
import (
"github.com/gin-gonic/gin"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/messageSender"
"github.com/komari-monitor/komari/internal/messageSender/factory"
@@ -50,7 +50,7 @@ func SetMessageSenderProvider(c *gin.Context) {
api.RespondError(c, 500, "Failed to save message sender provider configuration: "+err.Error())
return
}
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
// 正在使用,重载
if cfg.NotificationMethod == senderConfig.Name {
err := messageSender.LoadProvider(senderConfig.Name, senderConfig.Addition)
+2 -2
View File
@@ -3,9 +3,9 @@ package admin
import (
"github.com/gin-gonic/gin"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/oauth"
"github.com/komari-monitor/komari/internal/oauth/factory"
@@ -80,7 +80,7 @@ func SetOidcProvider(c *gin.Context) {
api.RespondError(c, 500, "Failed to save OIDC provider configuration: "+err.Error())
return
}
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
// 正在使用,重载
if cfg.OAuthProvider == oidcConfig.Name {
err := oauth.LoadProvider(oidcConfig.Name, oidcConfig.Addition)
+5 -6
View File
@@ -4,9 +4,8 @@ import (
"database/sql"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/database/records"
"github.com/komari-monitor/komari/internal/database/tasks"
@@ -15,13 +14,13 @@ import (
// GetSettings 获取自定义配置
func GetSettings(c *gin.Context) {
cst, err := config.Get()
cst, err := conf.GetWithV1Format()
if err != nil {
if err == sql.ErrNoRows {
//override
cst = models.Config{Sitename: "Komari"}
cst = conf.V1Struct{Sitename: "Komari"}
cst.ID = 1
config.Save(cst)
conf.Save(cst)
api.RespondSuccess(c, cst)
return
}
@@ -42,7 +41,7 @@ func EditSettings(c *gin.Context) {
}
cfg["id"] = 1 // Only one record
if err := config.Update(cfg); err != nil {
if err := conf.Update(cfg); err != nil {
api.RespondError(c, 500, "Failed to update settings: "+err.Error())
return
}
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/geoip"
"github.com/komari-monitor/komari/internal/messageSender"
@@ -32,7 +32,7 @@ func TestGeoIp(c *gin.Context) {
ip = c.ClientIP()
}
}
conf, err := config.Get()
conf, err := conf.GetWithV1Format()
if err != nil {
api.RespondError(c, 500, "Failed to get configuration: "+err.Error())
return
+2 -2
View File
@@ -14,7 +14,7 @@ import (
"github.com/gin-gonic/gin"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
)
@@ -138,7 +138,7 @@ func SetTheme(c *gin.Context) {
"theme": themeName,
}
if err := config.Update(updateData); err != nil {
if err := conf.Update(updateData); err != nil {
api.RespondError(c, http.StatusInternalServerError, "更新主题设置失败: "+err.Error())
return
}
+2 -2
View File
@@ -3,8 +3,8 @@ package client
import (
"github.com/gin-gonic/gin"
api "github.com/komari-monitor/komari/internal/api_v1"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/pkg/utils"
)
@@ -14,7 +14,7 @@ func RegisterClient(c *gin.Context) {
api.RespondError(c, 403, "Invalid AutoDiscovery Key")
return
}
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
api.RespondError(c, 500, "Failed to get configuration: "+err.Error())
return
+2 -2
View File
@@ -3,8 +3,8 @@ package client
import (
"net"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/geoip"
"github.com/gin-gonic/gin"
@@ -60,7 +60,7 @@ func UploadBasicInfo(c *gin.Context) {
}
}
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
if ipv4, ok := cbi["ipv4"].(string); ok && ipv4 != "" {
ip4 := net.ParseIP(ipv4)
ip4_record, _ := geoip.GetGeoInfo(ip4)
+22 -2
View File
@@ -5,9 +5,11 @@ import (
"io"
"net/http"
"github.com/gookit/event"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/eventType"
"github.com/gin-gonic/gin"
)
@@ -19,7 +21,7 @@ type LoginRequest struct {
}
func Login(c *gin.Context) {
conf, _ := config.Get()
conf, _ := conf.GetWithV1Format()
if conf.DisablePasswordLogin {
RespondError(c, http.StatusForbidden, "Password login is disabled")
return
@@ -44,6 +46,15 @@ func Login(c *gin.Context) {
uuid, success := accounts.CheckPassword(data.Username, data.Password)
if !success {
RespondError(c, http.StatusUnauthorized, "Invalid credentials")
event.Trigger(eventType.LoginFailed, event.M{
"username": data.Username,
"method": "password",
"ip": c.ClientIP(),
"ua": c.Request.UserAgent(),
"header": c.Request.Header,
"referrer": c.Request.Referer(),
"host": c.Request.Host,
})
return
}
// 2FA
@@ -67,6 +78,15 @@ func Login(c *gin.Context) {
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
auditlog.Log(c.ClientIP(), uuid, "logged in (password)", "login")
RespondSuccess(c, gin.H{"set-cookie": gin.H{"session_token": session}})
event.Trigger(eventType.UserLogin, event.M{
"username": data.Username,
"method": "password",
"ip": c.ClientIP(),
"ua": c.Request.UserAgent(),
"header": c.Request.Header,
"referrer": c.Request.Referer(),
"host": c.Request.Host,
})
}
func Logout(c *gin.Context) {
session, _ := c.Cookie("session_token")
+36 -2
View File
@@ -5,16 +5,18 @@ import (
"slices"
"github.com/gin-gonic/gin"
"github.com/gookit/event"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/eventType"
"github.com/komari-monitor/komari/internal/oauth"
"github.com/komari-monitor/komari/pkg/utils"
)
// /api/oauth
func OAuth(c *gin.Context) {
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
if !cfg.OAuthEnabled {
c.JSON(403, gin.H{"status": "error", "error": "OAuth is not enabled"})
return
@@ -97,6 +99,17 @@ func OAuthCallback(c *gin.Context) {
"status": "error",
"message": "please log in and bind your external account first.",
})
event.Trigger(eventType.UserLogin, event.M{
"username": user.Username,
"method": "oauth",
"ip": c.ClientIP(),
"ua": c.Request.UserAgent(),
"header": c.Request.Header,
"referrer": c.Request.Referer(),
"host": c.Request.Host,
"error": "no linked account",
"sso_id": sso_id,
})
return
}
@@ -104,11 +117,32 @@ func OAuthCallback(c *gin.Context) {
session, err := accounts.CreateSession(user.UUID, 2592000, c.Request.UserAgent(), c.ClientIP(), "oauth")
if err != nil {
c.JSON(500, gin.H{"status": "error", "message": err.Error()})
event.Trigger(eventType.LoginFailed, event.M{
"username": user.Username,
"method": "oauth",
"ip": c.ClientIP(),
"ua": c.Request.UserAgent(),
"header": c.Request.Header,
"referrer": c.Request.Referer(),
"host": c.Request.Host,
"error": err.Error(),
"sso_id": sso_id,
})
return
}
// 设置cookie并返回
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
auditlog.Log(c.ClientIP(), user.UUID, "logged in (OAuth)", "login")
event.Trigger(eventType.UserLogin, event.M{
"username": user.Username,
"method": "oauth",
"ip": c.ClientIP(),
"ua": c.Request.UserAgent(),
"header": c.Request.Header,
"referrer": c.Request.Referer(),
"host": c.Request.Host,
})
c.Redirect(302, "/admin")
}
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/accounts"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/ws"
@@ -20,7 +20,7 @@ func GetClients(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
return
}
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if cfg.AllowCors {
+251
View File
@@ -0,0 +1,251 @@
package conf
import (
"encoding/json"
"os"
"github.com/gookit/event"
"github.com/komari-monitor/komari/cmd/flags"
"github.com/komari-monitor/komari/internal/eventType"
)
func Default() Config {
return Config{
Site: Site{
Sitename: "Komari",
Description: "Komari Monitor, a simple server monitoring tool.",
AllowCors: false,
Theme: "default",
},
GeoIp: GeoIp{
GeoIpEnabled: true,
GeoIpProvider: GeoIp_IPInfo,
},
Notification: Notification{
NotificationEnabled: true,
TrafficLimitPercentage: 80.00,
},
Record: Record{
RecordEnabled: true,
RecordPreserveTime: 720,
PingRecordPreserveTime: 24,
},
}
}
func Override(cst Config) error {
b, err := json.MarshalIndent(cst, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(flags.ConfigFile, b, 0644); err != nil {
return err
}
oldConf := *Conf
Conf = &cst
event.Trigger(eventType.ConfigUpdated, event.M{
"old": oldConf,
"new": cst,
})
return nil
}
func SavePartial(cst map[string]interface{}) error {
// 将当前内存中的配置转换为通用 map,便于合并
baseBytes, err := json.Marshal(Conf)
if err != nil {
return err
}
var base map[string]interface{}
if err := json.Unmarshal(baseBytes, &base); err != nil {
return err
}
// 兼容旧版扁平字段:把扁平键映射到新版分组结构
normalized := normalizePartialMap(cst)
// 深度合并(normalized 覆盖 base
merged := deepMerge(base, normalized)
// 回写到强类型 Config
mergedBytes, err := json.Marshal(merged)
if err != nil {
return err
}
var newConf Config
if err := json.Unmarshal(mergedBytes, &newConf); err != nil {
return err
}
// 更新内存并落盘
return Override(newConf)
}
func EditAndTrigger(fn func()) error {
oldConf := *Conf
fn()
event.Trigger(eventType.ConfigUpdated, event.M{
"old": oldConf,
"new": *Conf,
})
return nil
}
func SaveFull(cst Config) error {
return Override(cst)
}
func Load() (*Config, error) {
b, err := os.ReadFile(flags.ConfigFile)
if err != nil {
return nil, err
}
cst := &Config{}
if err := json.Unmarshal(b, cst); err != nil {
return nil, err
}
Conf = cst
return cst, nil
}
// GetWithV1Format 以 v1 API 格式获取配置对象,使用 Conf 直接获取对象引用
func GetWithV1Format() (V1Struct, error) {
return Conf.ToV1Format(), nil
}
func Save(cst V1Struct) error {
cfg := cst.ToConfig()
return Override(cfg)
}
func Update(cst map[string]interface{}) error {
// Update 的语义等同于 SavePartial,保持对旧数据格式兼容
return SavePartial(cst)
}
// normalizePartialMap 将可能包含旧版扁平字段的输入映射为新版分组结构。
// 若已是分组结构(包含 site/login/...),则原样保留并与映射结果合并。
func normalizePartialMap(in map[string]interface{}) map[string]interface{} {
if in == nil {
return map[string]interface{}{}
}
// 先复制一份,避免修改入参
out := make(map[string]interface{})
for k, v := range in {
out[k] = v
}
// 准备确保分组 map 存在
ensureGroup := func(name string) map[string]interface{} {
v, ok := out[name]
if !ok || v == nil {
m := map[string]interface{}{}
out[name] = m
return m
}
if m, ok := v.(map[string]interface{}); ok {
return m
}
// 若类型非 map,则覆盖为 map
m := map[string]interface{}{}
out[name] = m
return m
}
site := ensureGroup("site")
login := ensureGroup("login")
geo := ensureGroup("geo_ip")
notif := ensureGroup("notification")
record := ensureGroup("record")
compact := ensureGroup("compact")
nezha := func() map[string]interface{} {
v, ok := compact["nezha"]
if !ok || v == nil {
m := map[string]interface{}{}
compact["nezha"] = m
return m
}
if m, ok := v.(map[string]interface{}); ok {
return m
}
m := map[string]interface{}{}
compact["nezha"] = m
return m
}()
// 扁平 -> 分组字段映射表
move := func(flatKey string, group map[string]interface{}, groupKey string) {
if v, ok := out[flatKey]; ok {
group[groupKey] = v
delete(out, flatKey)
}
}
// Site
move("sitename", site, "sitename")
move("description", site, "description")
move("allow_cors", site, "allow_cors")
move("theme", site, "theme")
move("private_site", site, "private_site")
move("script_domain", site, "script_domain")
move("send_ip_addr_to_guest", site, "send_ip_addr_to_guest")
move("eula_accepted", site, "eula_accepted")
move("custom_head", site, "custom_head")
move("custom_body", site, "custom_body")
// Login
move("api_key", login, "api_key")
move("auto_discovery_key", login, "auto_discovery_key")
move("o_auth_enabled", login, "o_auth_enabled")
move("o_auth_provider", login, "o_auth_provider")
move("disable_password_login", login, "disable_password_login")
// GeoIP
move("geo_ip_enabled", geo, "geo_ip_enabled")
move("geo_ip_provider", geo, "geo_ip_provider")
// Notification
move("notification_enabled", notif, "notification_enabled")
move("notification_method", notif, "notification_method")
move("notification_template", notif, "notification_template")
move("expire_notification_enabled", notif, "expire_notification_enabled")
move("expire_notification_lead_days", notif, "expire_notification_lead_days")
move("login_notification", notif, "login_notification")
move("traffic_limit_percentage", notif, "traffic_limit_percentage")
// Record
move("record_enabled", record, "record_enabled")
move("record_preserve_time", record, "record_preserve_time")
move("ping_record_preserve_time", record, "ping_record_preserve_time")
// Compact.Nezha
move("nezha_compat_enabled", nezha, "nezha_compat_enabled")
move("nezha_compat_listen", nezha, "nezha_compat_listen")
return out
}
// deepMerge 以 dst 为基础,将 src 合并覆盖到 dst。仅对 map[string]interface{} 递归。
func deepMerge(dst, src map[string]interface{}) map[string]interface{} {
if dst == nil {
dst = map[string]interface{}{}
}
for k, v := range src {
if v == nil {
// 忽略空覆盖,避免误删值
continue
}
if dv, ok := dst[k]; ok {
dm, dIsMap := dv.(map[string]interface{})
sm, sIsMap := v.(map[string]interface{})
if dIsMap && sIsMap {
dst[k] = deepMerge(dm, sm)
continue
}
}
dst[k] = v
}
return dst
}
+10
View File
@@ -0,0 +1,10 @@
package conf
const (
GeoIp_MMDB = "mmdb"
GeoIp_IPAPI = "ip-api"
GeoIp_GeoJS = "geojs"
GeoIp_IPInfo = "ipinfo"
GeoIp_Empty = "empty"
Version_Development = "development"
)
+203
View File
@@ -0,0 +1,203 @@
package conf
import "time"
var (
Version = Version_Development
CommitHash = "unknown"
)
var (
Conf *Config // 当直接修改时,请手动触发 eventType.ConfigUpdated 事件,或者使用 EditAndTrigger(func() { ... } 包裹
)
type Config struct {
Site Site `json:"site"`
Login Login `json:"login"`
GeoIp GeoIp `json:"geo_ip"`
Notification Notification `json:"notification"`
Record Record `json:"record"`
Compact Compact `json:"compact"`
}
type Site struct {
Sitename string `json:"sitename"`
Description string `json:"description"`
AllowCors bool `json:"allow_cors"`
PrivateSite bool `json:"private_site"` // 是否为私有站点,默认 false
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest"` // 是否向访客页面发送 IP 地址,默认 false
ScriptDomain string `json:"script_domain"` // 自定义脚本域名
EulaAccepted bool `json:"eula_accepted"`
// 自定义美化
CustomHead string `json:"custom_head"`
CustomBody string `json:"custom_body"`
Theme string `json:"theme"` // 主题名称,默认 'default'
}
type Login struct {
ApiKey string `json:"api_key"`
AutoDiscoveryKey string `json:"auto_discovery_key"` // 自动发现密钥
// OAuth 配置
OAuthEnabled bool `json:"o_auth_enabled"`
OAuthProvider string `json:"o_auth_provider"`
DisablePasswordLogin bool `json:"disable_password_login"`
}
type GeoIp struct {
GeoIpEnabled bool `json:"geo_ip_enabled"`
GeoIpProvider string `json:"geo_ip_provider"` // empty, mmdb, ip-api, geojs
}
type Notification struct {
NotificationEnabled bool `json:"notification_enabled"` // 通知总开关
NotificationMethod string `json:"notification_method"`
NotificationTemplate string `json:"notification_template"`
ExpireNotificationEnabled bool `json:"expire_notification_enabled"` // 是否启用过期通知
ExpireNotificationLeadDays int `json:"expire_notification_lead_days"` // 过期前多少天通知,默认7天
LoginNotification bool `json:"login_notification"` // 登录通知
TrafficLimitPercentage float64 `json:"traffic_limit_percentage"` // 流量限制百分比,默认80.00%
}
type Record struct {
RecordEnabled bool `json:"record_enabled"` // 是否启用记录功能
RecordPreserveTime int `json:"record_preserve_time"` // 记录保留时间,单位小时,默认30天
PingRecordPreserveTime int `json:"ping_record_preserve_time"` // Ping 记录保留时间,单位小时,默认1天
}
type Compact struct {
Nezha Nezha `json:"nezha"`
}
type Nezha struct {
// Nezha 兼容(Agent gRPC
NezhaCompatEnabled bool `json:"nezha_compat_enabled"`
NezhaCompatListen string `json:"nezha_compat_listen"` // 例如 0.0.0.0:5555
}
// [DEPRECATED] 旧的数据结构,将不再维护,请考虑使用 conf.Config 结构体
type V1Struct struct {
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
Description string `json:"description" gorm:"type:text"`
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
// GeoIP 配置
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
// Nezha 兼容(Agent gRPC
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
// OAuth 配置
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
// 自定义美化
CustomHead string `json:"custom_head" gorm:"type:longtext"`
CustomBody string `json:"custom_body" gorm:"type:longtext"`
// 通知
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
// Record
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
CreatedAt time.Time
UpdatedAt time.Time
}
func (cst *V1Struct) ToConfig() Config {
return Config{
Site: Site{
Sitename: cst.Sitename,
Description: cst.Description,
AllowCors: cst.AllowCors,
PrivateSite: cst.PrivateSite,
SendIpAddrToGuest: cst.SendIpAddrToGuest,
ScriptDomain: cst.ScriptDomain,
EulaAccepted: cst.EulaAccepted,
CustomHead: cst.CustomHead,
CustomBody: cst.CustomBody,
Theme: cst.Theme,
},
Login: Login{
ApiKey: cst.ApiKey,
AutoDiscoveryKey: cst.AutoDiscoveryKey,
OAuthEnabled: cst.OAuthEnabled,
OAuthProvider: cst.OAuthProvider,
DisablePasswordLogin: cst.DisablePasswordLogin,
},
GeoIp: GeoIp{
GeoIpEnabled: cst.GeoIpEnabled,
GeoIpProvider: cst.GeoIpProvider,
},
Notification: Notification{
NotificationEnabled: cst.NotificationEnabled,
NotificationMethod: cst.NotificationMethod,
NotificationTemplate: cst.NotificationTemplate,
ExpireNotificationEnabled: cst.ExpireNotificationEnabled,
ExpireNotificationLeadDays: cst.ExpireNotificationLeadDays,
LoginNotification: cst.LoginNotification,
TrafficLimitPercentage: cst.TrafficLimitPercentage,
},
Record: Record{
RecordEnabled: cst.RecordEnabled,
RecordPreserveTime: cst.RecordPreserveTime,
PingRecordPreserveTime: cst.PingRecordPreserveTime,
},
Compact: Compact{
Nezha: Nezha{
NezhaCompatEnabled: cst.NezhaCompatEnabled,
NezhaCompatListen: cst.NezhaCompatListen,
},
},
}
}
func (cfg *Config) ToV1Format() V1Struct {
return V1Struct{
ID: 1,
Sitename: cfg.Site.Sitename,
Description: cfg.Site.Description,
AllowCors: cfg.Site.AllowCors,
Theme: cfg.Site.Theme,
PrivateSite: cfg.Site.PrivateSite,
ApiKey: cfg.Login.ApiKey,
AutoDiscoveryKey: cfg.Login.AutoDiscoveryKey,
ScriptDomain: cfg.Site.ScriptDomain,
SendIpAddrToGuest: cfg.Site.SendIpAddrToGuest,
EulaAccepted: cfg.Site.EulaAccepted,
GeoIpEnabled: cfg.GeoIp.GeoIpEnabled,
GeoIpProvider: cfg.GeoIp.GeoIpProvider,
NezhaCompatEnabled: cfg.Compact.Nezha.NezhaCompatEnabled,
NezhaCompatListen: cfg.Compact.Nezha.NezhaCompatListen,
OAuthEnabled: cfg.Login.OAuthEnabled,
OAuthProvider: cfg.Login.OAuthProvider,
DisablePasswordLogin: cfg.Login.DisablePasswordLogin,
CustomHead: cfg.Site.CustomHead,
CustomBody: cfg.Site.CustomBody,
NotificationEnabled: cfg.Notification.NotificationEnabled,
NotificationMethod: cfg.Notification.NotificationMethod,
NotificationTemplate: cfg.Notification.NotificationTemplate,
ExpireNotificationEnabled: cfg.Notification.ExpireNotificationEnabled,
ExpireNotificationLeadDays: cfg.Notification.ExpireNotificationLeadDays,
LoginNotification: cfg.Notification.LoginNotification,
TrafficLimitPercentage: cfg.Notification.TrafficLimitPercentage,
RecordEnabled: cfg.Record.RecordEnabled,
RecordPreserveTime: cfg.Record.RecordPreserveTime,
PingRecordPreserveTime: cfg.Record.PingRecordPreserveTime,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"net"
"time"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
@@ -39,7 +39,7 @@ func CreateSession(uuid string, expires int, userAgent, ip, login_method string)
LoginMethod: login_method,
LatestOnline: models.FromTime(time.Now()),
}
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
if cfg.LoginNotification {
ipAddr := net.ParseIP(ip)
ipinfo, _ := geoip.GetGeoInfo(ipAddr)
-115
View File
@@ -1,115 +0,0 @@
package config
import (
"errors"
"log"
"sync"
"time"
"github.com/gookit/event"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/eventType"
"gorm.io/gorm"
)
var mu sync.Mutex
func init() {
mu = sync.Mutex{}
}
func Get() (models.Config, error) {
mu.Lock()
defer mu.Unlock()
db := dbcore.GetDBInstance()
var config models.Config
if err := db.First(&config).Error; err != nil {
if err == gorm.ErrRecordNotFound {
config = models.Config{
ID: 1,
Sitename: "Komari",
Description: "Komari Monitor, a simple server monitoring tool.",
AllowCors: false,
OAuthEnabled: false,
GeoIpEnabled: true,
GeoIpProvider: "ipinfo",
NezhaCompatEnabled: false,
NezhaCompatListen: "",
NotificationTemplate: "{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}",
UpdatedAt: models.FromTime(time.Now()),
CreatedAt: models.FromTime(time.Now()),
}
if err := db.Create(&config).Error; err != nil {
log.Fatal("Failed to create default config:", err)
}
return config, nil
}
return config, err
}
return config, nil
}
func Save(cst models.Config) error {
db := dbcore.GetDBInstance()
oldConfig, _ := Get()
// Only one records
cst.ID = 1
cst.UpdatedAt = models.FromTime(time.Now())
if err := db.Model(&models.Config{}).Where("id = ?", cst.ID).
Select("*").
Updates(cst).Error; err != nil {
return err
}
newConfig, _ := Get()
event.Trigger(eventType.ConfigUpdated, event.M{"old": oldConfig, "new": newConfig})
return nil
}
func Update(cst map[string]interface{}) error {
db := dbcore.GetDBInstance()
oldConfig, _ := Get()
// Proceed with update
cst["id"] = 1
cst["updated_at"] = time.Now()
delete(cst, "created_at")
delete(cst, "CreatedAt")
// 至少有一种登录方式启用
newDisablePasswordLogin := oldConfig.DisablePasswordLogin
newOAuthEnabled := oldConfig.OAuthEnabled
if val, exists := cst["disable_password_login"]; exists {
newDisablePasswordLogin = val.(bool)
}
if val, exists := cst["o_auth_enabled"]; exists {
newOAuthEnabled = val.(bool)
}
if newDisablePasswordLogin && !newOAuthEnabled {
return errors.New("at least one login method must be enabled (password/oauth)")
}
// 没绑定账号也不能禁用
if newDisablePasswordLogin {
usr := &models.User{}
if err := db.Model(&models.User{}).First(usr).Error; err != nil {
return errors.Join(err, errors.New("failed to retrieve user"))
}
if usr.SSOID == "" {
return errors.New("cannot disable password login when no SSO-bound account exists")
}
}
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&models.Config{}).Where("id = ?", oldConfig.ID).Updates(cst).Error; err != nil {
return errors.Join(err, errors.New("failed to update configuration"))
}
newConfig := &models.Config{}
if err := tx.Where("id = ?", oldConfig.ID).First(newConfig).Error; err != nil {
return errors.Join(err, errors.New("failed to retrieve updated configuration"))
}
event.Trigger(eventType.ConfigUpdated, event.M{"old": oldConfig, "new": *newConfig})
return nil
})
if err != nil {
return err
}
return nil
}
+1 -386
View File
@@ -1,19 +1,11 @@
package dbcore
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/komari-monitor/komari/cmd/flags"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/database/models"
logutil "github.com/komari-monitor/komari/internal/log"
"gorm.io/driver/mysql"
@@ -21,338 +13,6 @@ import (
"gorm.io/gorm"
)
// zipDirectoryExcluding 将 srcDir 打包为 dstZipexclude 是绝对路径集合需要排除
func zipDirectoryExcluding(srcDir, dstZip string, exclude map[string]struct{}) error {
// 规范化排除路径为绝对路径
normExclude := make(map[string]struct{}, len(exclude))
for p := range exclude {
abs, _ := filepath.Abs(p)
normExclude[abs] = struct{}{}
}
out, err := os.Create(dstZip)
if err != nil {
return err
}
defer out.Close()
zw := zip.NewWriter(out)
defer zw.Close()
absSrc, _ := filepath.Abs(srcDir)
walkErr := filepath.Walk(absSrc, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 排除 backup.zip 本身
if _, ok := normExclude[path]; ok {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// 计算 zip 内相对路径
rel, err := filepath.Rel(absSrc, path)
if err != nil {
return err
}
// 根目录跳过
if rel == "." {
return nil
}
// 替换为正斜杠
zipName := filepath.ToSlash(rel)
if info.IsDir() {
_, err := zw.Create(zipName + "/")
return err
}
// 普通文件
fh, err := os.Open(path)
if err != nil {
return err
}
w, err := zw.Create(zipName)
if err != nil {
fh.Close()
return err
}
if _, err := io.Copy(w, fh); err != nil {
fh.Close()
return err
}
fh.Close()
return nil
})
if walkErr != nil {
return walkErr
}
return zw.Close()
}
// removeAllInDirExcept 删除 dir 下除 exclude 指定绝对路径外的所有文件和文件夹
func removeAllInDirExcept(dir string, exclude map[string]struct{}) error {
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
normExclude := make(map[string]struct{}, len(exclude))
for p := range exclude {
abs, _ := filepath.Abs(p)
normExclude[abs] = struct{}{}
}
entries, err := os.ReadDir(absDir)
if err != nil {
return err
}
for _, e := range entries {
full := filepath.Join(absDir, e.Name())
if _, ok := normExclude[full]; ok {
continue
}
if err := os.RemoveAll(full); err != nil {
return err
}
}
return nil
}
// unzipToDir 将 zipPath 解压到 dstDir,包含路径遍历保护
func unzipToDir(zipPath, dstDir string) error {
zr, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer zr.Close()
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
absDst, _ := filepath.Abs(dstDir)
for _, f := range zr.File {
// 构造目标路径并做路径遍历保护
cleanName := filepath.Clean(f.Name)
targetPath := filepath.Join(absDst, cleanName)
if !strings.HasPrefix(targetPath, absDst+string(os.PathSeparator)) && targetPath != absDst {
return fmt.Errorf("illegal file path in zip: %s", f.Name)
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(targetPath, 0755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.Create(targetPath)
if err != nil {
rc.Close()
return err
}
if _, err := io.Copy(out, rc); err != nil {
out.Close()
rc.Close()
return err
}
out.Close()
rc.Close()
}
return nil
}
// mergeClientInfo 将旧版ClientInfo数据迁移到新版Client表
func mergeClientInfo(db *gorm.DB) {
var clientInfos []common.ClientInfo
if err := db.Find(&clientInfos).Error; err != nil {
log.Printf("Failed to read ClientInfo table: %v", err)
return
}
for _, info := range clientInfos {
var client models.Client
if err := db.Where("uuid = ?", info.UUID).First(&client).Error; err != nil {
log.Printf("Could not find Client record with UUID %s: %v", info.UUID, err)
continue
}
// 更新Client记录
client.Name = info.Name
client.CpuName = info.CpuName
client.Virtualization = info.Virtualization
client.Arch = info.Arch
client.CpuCores = info.CpuCores
client.OS = info.OS
client.GpuName = info.GpuName
client.IPv4 = info.IPv4
client.IPv6 = info.IPv6
client.Region = info.Region
client.Remark = info.Remark
client.PublicRemark = info.PublicRemark
client.MemTotal = info.MemTotal
client.SwapTotal = info.SwapTotal
client.DiskTotal = info.DiskTotal
client.Version = info.Version
client.Weight = info.Weight
client.Price = info.Price
client.BillingCycle = info.BillingCycle
client.ExpiredAt = models.FromTime(info.ExpiredAt)
// Save updated Client record
if err := db.Save(&client).Error; err != nil {
log.Printf("Failed to update Client record: %v", err)
continue
}
}
// Backup and rename old table after migration
if err := db.Migrator().RenameTable("client_infos", "client_infos_backup"); err != nil {
log.Printf("Failed to backup ClientInfo table: %v", err)
return
}
log.Println("Data migration completed, old table has been backed up as client_infos_backup")
}
func MergeDatabase(db *gorm.DB) {
if db.Migrator().HasTable("client_infos") {
log.Println("[>0.0.5] Legacy ClientInfo table detected, starting data migration...")
mergeClientInfo(db)
}
if db.Migrator().HasColumn(&models.Config{}, "allow_cros") {
log.Println("[>0.0.5a] Renaming column 'allow_cros' to 'allow_cors' in config table...")
db.Migrator().RenameColumn(&models.Config{}, "allow_cros", "allow_cors")
}
if db.Migrator().HasColumn(&models.LoadNotification{}, "client") {
log.Println("[>0.1.4] Rebuilding LoadNotification table....")
db.Migrator().DropTable(&models.LoadNotification{})
}
if !db.Migrator().HasTable(&models.OidcProvider{}) && db.Migrator().HasTable(&models.Config{}) {
log.Println("[>1.0.2] Merge OidcProvider table....")
var config struct {
OAuthClientID string `json:"o_auth_client_id" gorm:"type:varchar(255)"`
OAuthClientSecret string `json:"o_auth_client_secret" gorm:"type:varchar(255)"`
}
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
log.Println("Failed to get config for OIDC provider migration:", err)
}
db.AutoMigrate(&models.OidcProvider{})
j, err := json.Marshal(&map[string]string{
"client_id": config.OAuthClientID,
"client_secret": config.OAuthClientSecret,
})
if err != nil {
log.Println("Failed to marshal OIDC provider config:", err)
return
}
db.Save(&models.OidcProvider{
Name: "github",
Addition: string(j),
})
db.AutoMigrate(&models.Config{})
db.Model(&models.Config{}).Where("id = 1").Update("o_auth_provider", "github")
}
if !db.Migrator().HasTable(&models.MessageSenderProvider{}) && db.Migrator().HasTable(&models.Config{}) {
log.Println("[>1.0.2] Migrate MessageSender configuration....")
var config struct {
TelegramBotToken string `json:"telegram_bot_token" gorm:"type:varchar(255)"`
TelegramChatID string `json:"telegram_chat_id" gorm:"type:varchar(255)"`
TelegramEndpoint string `json:"telegram_endpoint" gorm:"type:varchar(255)"`
EmailHost string `json:"email_host" gorm:"type:varchar(255)"`
EmailPort int `json:"email_port" gorm:"type:int"`
EmailUsername string `json:"email_username" gorm:"type:varchar(255)"`
EmailPassword string `json:"email_password" gorm:"type:varchar(255)"`
EmailSender string `json:"email_sender" gorm:"type:varchar(255)"`
EmailReceiver string `json:"email_receiver" gorm:"type:varchar(255)"`
EmailUseSSL bool `json:"email_use_ssl" gorm:"type:boolean"`
NotificationMethod string `json:"notification_method" gorm:"type:varchar(50)"`
}
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
log.Println("Failed to get config for MessageSender migration:", err)
}
db.AutoMigrate(&models.MessageSenderProvider{})
// 迁移Telegram配置
if config.NotificationMethod == "telegram" && config.TelegramBotToken != "" {
telegramConfig := map[string]interface{}{
"bot_token": config.TelegramBotToken,
"chat_id": config.TelegramChatID,
"endpoint": config.TelegramEndpoint,
}
if telegramConfig["endpoint"] == "" {
telegramConfig["endpoint"] = "https://api.telegram.org/bot"
}
telegramConfigJSON, err := json.Marshal(telegramConfig)
if err != nil {
log.Println("Failed to marshal Telegram config:", err)
} else {
db.Save(&models.MessageSenderProvider{
Name: "telegram",
Addition: string(telegramConfigJSON),
})
}
}
// 迁移Email配置
if config.NotificationMethod == "email" && config.EmailHost != "" {
emailConfig := map[string]interface{}{
"host": config.EmailHost,
"port": config.EmailPort,
"username": config.EmailUsername,
"password": config.EmailPassword,
"sender": config.EmailSender,
"receiver": config.EmailReceiver,
"use_ssl": config.EmailUseSSL,
}
emailConfigJSON, err := json.Marshal(emailConfig)
if err != nil {
log.Println("Failed to marshal Email config:", err)
} else {
db.Save(&models.MessageSenderProvider{
Name: "email",
Addition: string(emailConfigJSON),
})
}
}
// 删除旧的配置字段
if db.Migrator().HasColumn(&models.Config{}, "telegram_bot_token") {
db.Migrator().DropColumn(&models.Config{}, "telegram_bot_token")
}
if db.Migrator().HasColumn(&models.Config{}, "telegram_chat_id") {
db.Migrator().DropColumn(&models.Config{}, "telegram_chat_id")
}
if db.Migrator().HasColumn(&models.Config{}, "telegram_endpoint") {
db.Migrator().DropColumn(&models.Config{}, "telegram_endpoint")
}
if db.Migrator().HasColumn(&models.Config{}, "email_host") {
db.Migrator().DropColumn(&models.Config{}, "email_host")
}
if db.Migrator().HasColumn(&models.Config{}, "email_port") {
db.Migrator().DropColumn(&models.Config{}, "email_port")
}
if db.Migrator().HasColumn(&models.Config{}, "email_username") {
db.Migrator().DropColumn(&models.Config{}, "email_username")
}
if db.Migrator().HasColumn(&models.Config{}, "email_password") {
db.Migrator().DropColumn(&models.Config{}, "email_password")
}
if db.Migrator().HasColumn(&models.Config{}, "email_sender") {
db.Migrator().DropColumn(&models.Config{}, "email_sender")
}
if db.Migrator().HasColumn(&models.Config{}, "email_receiver") {
db.Migrator().DropColumn(&models.Config{}, "email_receiver")
}
if db.Migrator().HasColumn(&models.Config{}, "email_use_ssl") {
db.Migrator().DropColumn(&models.Config{}, "email_use_ssl")
}
}
}
var (
instance *gorm.DB
once sync.Once
@@ -362,50 +22,6 @@ func GetDBInstance() *gorm.DB {
once.Do(func() {
var err error
// 在数据库初始化前执行:如果存在 ./data/backup.zip,则进行恢复逻辑
func() {
backupZipPath := filepath.Join(".", "data", "backup.zip")
if _, statErr := os.Stat(backupZipPath); statErr == nil {
// 4. 把除了 ./data/backup.zip 之外的所有文件压缩到 ./backup/{time}.zip
if err := os.MkdirAll("./backup", 0755); err != nil {
log.Printf("[restore] failed to create backup dir: %v", err)
} else {
tsName := time.Now().Format("20060102-150405")
bakPath := filepath.Join("./backup", fmt.Sprintf("%s.zip", tsName))
if zipErr := zipDirectoryExcluding("./data", bakPath, map[string]struct{}{backupZipPath: {}}); zipErr != nil {
log.Printf("[restore] failed to zip current data: %v", zipErr)
} else {
log.Printf("[restore] current data zipped to %s", bakPath)
}
}
// 5. 删除除了 ./data/backup.zip 之外的所有文件
if delErr := removeAllInDirExcept("./data", map[string]struct{}{backupZipPath: {}}); delErr != nil {
log.Printf("[restore] failed to cleanup data dir: %v", delErr)
}
// 6. 解压 ./data/backup.zip 到 ./data
if unzipErr := unzipToDir(backupZipPath, "./data"); unzipErr != nil {
log.Printf("[restore] failed to unzip backup into data: %v", unzipErr)
} else {
log.Printf("[restore] backup.zip extracted to ./data")
}
// 7. 删除 ./data/backup.zip
if rmErr := os.Remove(backupZipPath); rmErr != nil {
log.Printf("[restore] failed to remove backup.zip: %v", rmErr)
} else {
log.Printf("[restore] backup.zip removed")
}
// 8. 删除标记
if rmErr := os.Remove("./data/komari-backup-markup"); rmErr != nil {
log.Printf("[restore] failed to remove komari-backup-markup: %v", rmErr)
} else {
log.Printf("[restore] komari-backup-markup removed")
}
}
}()
logConfig := &gorm.Config{
Logger: logutil.NewGormLogger(),
}
@@ -441,14 +57,13 @@ func GetDBInstance() *gorm.DB {
default:
log.Fatalf("Unsupported database type: %s", flags.DatabaseType)
}
MergeDatabase(instance)
// 自动迁移模型
err = instance.AutoMigrate(
&models.User{},
&models.Client{},
&models.Record{},
&models.GPURecord{},
&models.Config{},
//&conf.V1Struct{},
&models.Log{},
&models.Clipboard{},
&models.LoadNotification{},
-38
View File
@@ -1,42 +1,4 @@
package models
type Config struct {
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
Description string `json:"description" gorm:"type:text"`
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
// GeoIP 配置
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
// Nezha 兼容(Agent gRPC
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
// OAuth 配置
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
// 自定义美化
CustomHead string `json:"custom_head" gorm:"type:longtext"`
CustomBody string `json:"custom_body" gorm:"type:longtext"`
// 通知
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
// Record
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
CreatedAt LocalTime
UpdatedAt LocalTime
}
-9
View File
@@ -8,7 +8,6 @@ import (
"gorm.io/gorm"
"github.com/komari-monitor/komari/cmd/flags"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
)
@@ -171,14 +170,6 @@ func CompactRecord() error {
log.Printf("Error migrating GPU records: %v", err)
return err
}
if flags.DatabaseType == "sqlite" {
if err := db.Exec("VACUUM").Error; err != nil {
log.Printf("Error vacuuming database: %v", err)
}
db.Exec("PRAGMA wal_checkpoint(TRUNCATE);")
}
//log.Printf("Record compaction completed")
return nil
}
+2 -2
View File
@@ -8,13 +8,13 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
)
func GetPublicInfo() (any, error) {
cst, err := config.Get()
cst, err := conf.GetWithV1Format()
if err != nil {
return nil, err
}
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"time"
"unicode"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/conf"
"github.com/patrickmn/go-cache"
)
@@ -52,14 +52,14 @@ func GetRegionUnicodeEmoji(isoCode string) string {
}
func InitGeoIp() {
conf, err := config.Get()
config, err := conf.GetWithV1Format()
if err != nil {
panic("Failed to get configuration for GeoIP: " + err.Error())
}
if !conf.GeoIpEnabled {
if !config.GeoIpEnabled {
return
}
switch conf.GeoIpProvider {
switch config.GeoIpProvider {
case "mmdb":
NewCurrentProvider, err := NewMaxMindGeoIPService()
if err != nil {
+2 -2
View File
@@ -12,10 +12,10 @@ import (
"github.com/komari-monitor/komari/internal/api_v1/client"
"github.com/komari-monitor/komari/internal/api_v1/record"
"github.com/komari-monitor/komari/internal/api_v1/task"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/conf"
)
func LoadApiV1Routes(r *gin.Engine, conf models.Config) {
func LoadApiV1Routes(r *gin.Engine, conf conf.V1Struct) {
r.Use(func(c *gin.Context) {
if len(c.Request.URL.Path) >= 4 && c.Request.URL.Path[:4] == "/api" {
c.Header("Cache-Control", "no-store")
+1 -7
View File
@@ -7,7 +7,6 @@ import (
"log/slog"
"time"
"github.com/komari-monitor/komari/internal/version"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
"gorm.io/gorm/utils"
@@ -25,12 +24,7 @@ func NewGormLogger() *GormLogger {
return &GormLogger{
SlowThreshold: 200 * time.Millisecond,
IgnoreRecordNotFoundError: true,
LogLevel: func(hash string) gormlogger.LogLevel {
if hash == "unknown" {
return gormlogger.Info
}
return gormlogger.Silent
}(version.VersionHash),
LogLevel: gormlogger.Warn,
}
}
+1 -1
View File
@@ -152,7 +152,7 @@ func SetupGlobalLogger(level slog.Level) {
stdlog.SetPrefix("")
// 替换标准库 log 的输出为 slog handler
stdlog.SetOutput(&writerAdapter{handler: handler, level: level})
stdlog.SetOutput(&writerAdapter{handler: handler, level: slog.LevelInfo})
}
// writerAdapter 将标准库 log 的输出适配到 slog
+4 -4
View File
@@ -8,9 +8,9 @@ import (
"sync"
"time"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/messageSender/factory"
)
@@ -52,7 +52,7 @@ func Initialize() {
}
})
}()
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
if cfg.NotificationMethod == "" || cfg.NotificationMethod == "none" {
LoadProvider("empty", "{}")
@@ -74,7 +74,7 @@ func SendTextMessage(message string, title string) error {
return fmt.Errorf("message sender provider is not initialized")
}
var err error
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
return err
}
@@ -96,7 +96,7 @@ func SendEvent(event models.EventMessage) error {
return fmt.Errorf("message sender provider is not initialized")
}
var err error
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
return err
}
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"math"
"time"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
"github.com/komari-monitor/komari/internal/messageSender"
@@ -23,7 +23,7 @@ func CheckExpireScheduledWork() {
duration := next.Sub(now)
time.Sleep(duration)
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
time.Sleep(time.Second)
continue
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"sync"
"time"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
messageevent "github.com/komari-monitor/komari/internal/database/models/messageEvent"
@@ -32,7 +32,7 @@ var clientStates sync.Map
// getNotificationConfig 获取指定客户端的通知配置。
// 返回配置对象和一个布尔值,指示全局和该客户端是否启用通知。
func getNotificationConfig(clientID string) (*models.OfflineNotification, bool) {
conf, err := config.Get()
conf, err := conf.GetWithV1Format()
if err != nil || !conf.NotificationEnabled {
return nil, false
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"strings"
"time"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/clients"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/messageSender"
"github.com/komari-monitor/komari/internal/ws"
@@ -27,7 +27,7 @@ func CheckTraffic() {
return
}
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
return
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"log"
"sync"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/oauth/factory"
)
@@ -70,7 +70,7 @@ func Initialize() error {
}
}
})
cfg, _ := config.Get()
cfg, _ := conf.GetWithV1Format()
if cfg.OAuthProvider == "" || cfg.OAuthProvider == "none" {
LoadProvider("empty", "{}")
return nil
+38
View File
@@ -0,0 +1,38 @@
package patch
import (
"log"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
)
func ApplyPatch() {
db := dbcore.GetDBInstance()
// 0.0.5 迁移ClientInfo
if db.Migrator().HasTable("client_infos") {
v0_0_5(db)
}
// 0.0.5a 修正cors拼写错误
if db.Migrator().HasColumn(&conf.V1Struct{}, "allow_cros") {
v0_0_5a(db)
}
// 0.1.4 重建LoadNotification表
if db.Migrator().HasColumn(&models.LoadNotification{}, "client") {
log.Println("[>0.1.4] Rebuilding LoadNotification table....")
db.Migrator().DropTable(&models.LoadNotification{})
}
// 1.0.2 合并OIDC提供商表
if !db.Migrator().HasTable(&models.OidcProvider{}) && db.Migrator().HasTable(&conf.V1Struct{}) {
v1_0_2_Oidc(db)
}
// 1.0.2 迁移消息发送配置到单独的表
if !db.Migrator().HasTable(&models.MessageSenderProvider{}) && db.Migrator().HasTable(&conf.V1Struct{}) {
v1_0_2_MessageSender(db)
}
// 1.1.4 迁移配置表
if db.Migrator().HasColumn(&conf.V1Struct{}, "id") {
v1_1_4(db)
}
}
+66
View File
@@ -0,0 +1,66 @@
package patch
import (
"log"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/models"
"gorm.io/gorm"
)
func v0_0_5(db *gorm.DB) {
log.Println("[>0.0.5] Legacy ClientInfo table detected, starting data migration...")
var clientInfos []common.ClientInfo
if err := db.Find(&clientInfos).Error; err != nil {
log.Printf("Failed to read ClientInfo table: %v", err)
return
}
for _, info := range clientInfos {
var client models.Client
if err := db.Where("uuid = ?", info.UUID).First(&client).Error; err != nil {
log.Printf("Could not find Client record with UUID %s: %v", info.UUID, err)
continue
}
// 更新Client记录
client.Name = info.Name
client.CpuName = info.CpuName
client.Virtualization = info.Virtualization
client.Arch = info.Arch
client.CpuCores = info.CpuCores
client.OS = info.OS
client.GpuName = info.GpuName
client.IPv4 = info.IPv4
client.IPv6 = info.IPv6
client.Region = info.Region
client.Remark = info.Remark
client.PublicRemark = info.PublicRemark
client.MemTotal = info.MemTotal
client.SwapTotal = info.SwapTotal
client.DiskTotal = info.DiskTotal
client.Version = info.Version
client.Weight = info.Weight
client.Price = info.Price
client.BillingCycle = info.BillingCycle
client.ExpiredAt = models.FromTime(info.ExpiredAt)
// Save updated Client record
if err := db.Save(&client).Error; err != nil {
log.Printf("Failed to update Client record: %v", err)
continue
}
}
// Backup and rename old table after migration
if err := db.Migrator().RenameTable("client_infos", "client_infos_backup"); err != nil {
log.Printf("Failed to backup ClientInfo table: %v", err)
return
}
log.Println("Data migration completed, old table has been backed up as client_infos_backup")
}
func v0_0_5a(db *gorm.DB) {
log.Println("[>0.0.5a] Renaming column 'allow_cros' to 'allow_cors' in config table...")
db.Migrator().RenameColumn(&conf.V1Struct{}, "allow_cros", "allow_cors")
}
+133
View File
@@ -0,0 +1,133 @@
package patch
import (
"encoding/json"
"log"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/models"
"gorm.io/gorm"
)
func v1_0_2_Oidc(db *gorm.DB) {
log.Println("[>1.0.2] Merge OidcProvider table....")
var config struct {
OAuthClientID string `json:"o_auth_client_id" gorm:"type:varchar(255)"`
OAuthClientSecret string `json:"o_auth_client_secret" gorm:"type:varchar(255)"`
}
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
log.Println("Failed to get config for OIDC provider migration:", err)
}
db.AutoMigrate(&models.OidcProvider{})
j, err := json.Marshal(&map[string]string{
"client_id": config.OAuthClientID,
"client_secret": config.OAuthClientSecret,
})
if err != nil {
log.Println("Failed to marshal OIDC provider config:", err)
return
}
db.Save(&models.OidcProvider{
Name: "github",
Addition: string(j),
})
db.AutoMigrate(&conf.V1Struct{})
db.Model(&conf.V1Struct{}).Where("id = 1").Update("o_auth_provider", "github")
}
func v1_0_2_MessageSender(db *gorm.DB) {
log.Println("[>1.0.2] Migrate MessageSender configuration....")
var config struct {
TelegramBotToken string `json:"telegram_bot_token" gorm:"type:varchar(255)"`
TelegramChatID string `json:"telegram_chat_id" gorm:"type:varchar(255)"`
TelegramEndpoint string `json:"telegram_endpoint" gorm:"type:varchar(255)"`
EmailHost string `json:"email_host" gorm:"type:varchar(255)"`
EmailPort int `json:"email_port" gorm:"type:int"`
EmailUsername string `json:"email_username" gorm:"type:varchar(255)"`
EmailPassword string `json:"email_password" gorm:"type:varchar(255)"`
EmailSender string `json:"email_sender" gorm:"type:varchar(255)"`
EmailReceiver string `json:"email_receiver" gorm:"type:varchar(255)"`
EmailUseSSL bool `json:"email_use_ssl" gorm:"type:boolean"`
NotificationMethod string `json:"notification_method" gorm:"type:varchar(50)"`
}
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&config).Error; err != nil {
log.Println("Failed to get config for MessageSender migration:", err)
}
db.AutoMigrate(&models.MessageSenderProvider{})
// 迁移Telegram配置
if config.NotificationMethod == "telegram" && config.TelegramBotToken != "" {
telegramConfig := map[string]interface{}{
"bot_token": config.TelegramBotToken,
"chat_id": config.TelegramChatID,
"endpoint": config.TelegramEndpoint,
}
if telegramConfig["endpoint"] == "" {
telegramConfig["endpoint"] = "https://api.telegram.org/bot"
}
telegramConfigJSON, err := json.Marshal(telegramConfig)
if err != nil {
log.Println("Failed to marshal Telegram config:", err)
} else {
db.Save(&models.MessageSenderProvider{
Name: "telegram",
Addition: string(telegramConfigJSON),
})
}
}
// 迁移Email配置
if config.NotificationMethod == "email" && config.EmailHost != "" {
emailConfig := map[string]interface{}{
"host": config.EmailHost,
"port": config.EmailPort,
"username": config.EmailUsername,
"password": config.EmailPassword,
"sender": config.EmailSender,
"receiver": config.EmailReceiver,
"use_ssl": config.EmailUseSSL,
}
emailConfigJSON, err := json.Marshal(emailConfig)
if err != nil {
log.Println("Failed to marshal Email config:", err)
} else {
db.Save(&models.MessageSenderProvider{
Name: "email",
Addition: string(emailConfigJSON),
})
}
}
// 删除旧的配置字段
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_bot_token") {
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_bot_token")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_chat_id") {
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_chat_id")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "telegram_endpoint") {
db.Migrator().DropColumn(&conf.V1Struct{}, "telegram_endpoint")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_host") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_host")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_port") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_port")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_username") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_username")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_password") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_password")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_sender") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_sender")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_receiver") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_receiver")
}
if db.Migrator().HasColumn(&conf.V1Struct{}, "email_use_ssl") {
db.Migrator().DropColumn(&conf.V1Struct{}, "email_use_ssl")
}
}
+77
View File
@@ -0,0 +1,77 @@
package patch
import (
"encoding/json"
"log/slog"
"time"
"github.com/komari-monitor/komari/internal/conf"
"gorm.io/gorm"
)
func v1_1_4(db *gorm.DB) {
slog.Info("[>1.1.4] Migrating config table to file config...")
var old_config struct {
ID uint `json:"id,omitempty" gorm:"primaryKey;autoIncrement"` // 1
Sitename string `json:"sitename" gorm:"type:varchar(100);not null"`
Description string `json:"description" gorm:"type:text"`
AllowCors bool `json:"allow_cors" gorm:"column:allow_cors;default:false"`
Theme string `json:"theme" gorm:"type:varchar(100);default:'default'"` // 主题名称,默认 'default'
PrivateSite bool `json:"private_site" gorm:"default:false"` // 是否为私有站点,默认 false
ApiKey string `json:"api_key" gorm:"type:varchar(255);default:''"`
AutoDiscoveryKey string `json:"auto_discovery_key" gorm:"type:varchar(255);default:''"` // 自动发现密钥
ScriptDomain string `json:"script_domain" gorm:"type:varchar(255);default:''"` // 自定义脚本域名
SendIpAddrToGuest bool `json:"send_ip_addr_to_guest" gorm:"default:false"` // 是否向访客页面发送 IP 地址,默认 false
EulaAccepted bool `json:"eula_accepted" gorm:"default:false"`
// GeoIP 配置
GeoIpEnabled bool `json:"geo_ip_enabled" gorm:"default:true"`
GeoIpProvider string `json:"geo_ip_provider" gorm:"type:varchar(20);default:'ip-api'"` // empty, mmdb, ip-api, geojs
// Nezha 兼容(Agent gRPC
NezhaCompatEnabled bool `json:"nezha_compat_enabled" gorm:"default:false"`
NezhaCompatListen string `json:"nezha_compat_listen" gorm:"type:varchar(100);default:''"` // 例如 0.0.0.0:5555
// OAuth 配置
OAuthEnabled bool `json:"o_auth_enabled" gorm:"default:false"`
OAuthProvider string `json:"o_auth_provider" gorm:"type:varchar(50);default:'github'"`
DisablePasswordLogin bool `json:"disable_password_login" gorm:"default:false"`
// 自定义美化
CustomHead string `json:"custom_head" gorm:"type:longtext"`
CustomBody string `json:"custom_body" gorm:"type:longtext"`
// 通知
NotificationEnabled bool `json:"notification_enabled" gorm:"default:false"` // 通知总开关
NotificationMethod string `json:"notification_method" gorm:"type:varchar(64);default:'none'"`
NotificationTemplate string `json:"notification_template" gorm:"type:longtext;default:'{{emoji}}{{emoji}}{{emoji}}\nEvent: {{event}}\nClients: {{client}}\nMessage: {{message}}\nTime: {{time}}'"`
ExpireNotificationEnabled bool `json:"expire_notification_enabled" gorm:"default:false"` // 是否启用过期通知
ExpireNotificationLeadDays int `json:"expire_notification_lead_days" gorm:"default:7"` // 过期前多少天通知,默认7天
LoginNotification bool `json:"login_notification" gorm:"default:false"` // 登录通知
TrafficLimitPercentage float64 `json:"traffic_limit_percentage" gorm:"default:80.00"` // 流量限制百分比,默认80.00%
// Record
RecordEnabled bool `json:"record_enabled" gorm:"default:true"` // 是否启用记录功能
RecordPreserveTime int `json:"record_preserve_time" gorm:"default:720"` // 记录保留时间,单位小时,默认30天
PingRecordPreserveTime int `json:"ping_record_preserve_time" gorm:"default:24"` // Ping 记录保留时间,单位小时,默认1天
CreatedAt time.Time
UpdatedAt time.Time
}
if err := db.Raw("SELECT * FROM configs LIMIT 1").Scan(&old_config).Error; err != nil {
slog.Error("Failed to get config.", slog.Any("error", err))
return
}
// 将旧结构转为 map[string]interface{},键为扁平 json 标签
bytes, err := json.Marshal(&old_config)
if err != nil {
slog.Error("Failed to marshal old config.", slog.Any("error", err))
return
}
var flat map[string]interface{}
if err := json.Unmarshal(bytes, &flat); err != nil {
slog.Error("Failed to unmarshal to map.", slog.Any("error", err))
return
}
// 使用 conf.Update 以 SavePartial 语义合并,并兼容旧键名
if err := conf.Update(flat); err != nil {
slog.Error("Failed to write new file config.", slog.Any("error", err))
return
}
db.Migrator().DropTable(&conf.V1Struct{})
slog.Info("[>1.1.4] Config migration finished.")
}
+207
View File
@@ -0,0 +1,207 @@
package restore
import (
"archive/zip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
)
var backupZipPath = filepath.Join(".", "data", "backup.zip")
func NeedBackupRestore() bool {
if _, statErr := os.Stat(backupZipPath); statErr == nil {
return true
}
return false
}
func RestoreBackup() {
// 4. 把除了 ./data/backup.zip 之外的所有文件压缩到 ./backup/{time}.zip
if err := os.MkdirAll("./backup", 0755); err != nil {
log.Printf("[restore] failed to create backup dir: %v", err)
} else {
tsName := time.Now().Format("20060102-150405")
bakPath := filepath.Join("./backup", fmt.Sprintf("%s.zip", tsName))
if zipErr := zipDirectoryExcluding("./data", bakPath, map[string]struct{}{backupZipPath: {}}); zipErr != nil {
log.Printf("[restore] failed to zip current data: %v", zipErr)
} else {
log.Printf("[restore] current data zipped to %s", bakPath)
}
}
// 5. 删除除了 ./data/backup.zip 之外的所有文件
if delErr := removeAllInDirExcept("./data", map[string]struct{}{backupZipPath: {}}); delErr != nil {
log.Printf("[restore] failed to cleanup data dir: %v", delErr)
}
// 6. 解压 ./data/backup.zip 到 ./data
if unzipErr := unzipToDir(backupZipPath, "./data"); unzipErr != nil {
log.Printf("[restore] failed to unzip backup into data: %v", unzipErr)
} else {
log.Printf("[restore] backup.zip extracted to ./data")
}
// 7. 删除 ./data/backup.zip
if rmErr := os.Remove(backupZipPath); rmErr != nil {
log.Printf("[restore] failed to remove backup.zip: %v", rmErr)
} else {
log.Printf("[restore] backup.zip removed")
}
// 8. 删除标记
if rmErr := os.Remove("./data/komari-backup-markup"); rmErr != nil {
log.Printf("[restore] failed to remove komari-backup-markup: %v", rmErr)
} else {
log.Printf("[restore] komari-backup-markup removed")
}
}
// zipDirectoryExcluding 将 srcDir 打包为 dstZipexclude 是绝对路径集合需要排除
func zipDirectoryExcluding(srcDir, dstZip string, exclude map[string]struct{}) error {
// 规范化排除路径为绝对路径
normExclude := make(map[string]struct{}, len(exclude))
for p := range exclude {
abs, _ := filepath.Abs(p)
normExclude[abs] = struct{}{}
}
out, err := os.Create(dstZip)
if err != nil {
return err
}
defer out.Close()
zw := zip.NewWriter(out)
defer zw.Close()
absSrc, _ := filepath.Abs(srcDir)
walkErr := filepath.Walk(absSrc, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 排除 backup.zip 本身
if _, ok := normExclude[path]; ok {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// 计算 zip 内相对路径
rel, err := filepath.Rel(absSrc, path)
if err != nil {
return err
}
// 根目录跳过
if rel == "." {
return nil
}
// 替换为正斜杠
zipName := filepath.ToSlash(rel)
if info.IsDir() {
_, err := zw.Create(zipName + "/")
return err
}
// 普通文件
fh, err := os.Open(path)
if err != nil {
return err
}
w, err := zw.Create(zipName)
if err != nil {
fh.Close()
return err
}
if _, err := io.Copy(w, fh); err != nil {
fh.Close()
return err
}
fh.Close()
return nil
})
if walkErr != nil {
return walkErr
}
return zw.Close()
}
// removeAllInDirExcept 删除 dir 下除 exclude 指定绝对路径外的所有文件和文件夹
func removeAllInDirExcept(dir string, exclude map[string]struct{}) error {
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
normExclude := make(map[string]struct{}, len(exclude))
for p := range exclude {
abs, _ := filepath.Abs(p)
normExclude[abs] = struct{}{}
}
entries, err := os.ReadDir(absDir)
if err != nil {
return err
}
for _, e := range entries {
full := filepath.Join(absDir, e.Name())
if _, ok := normExclude[full]; ok {
continue
}
if err := os.RemoveAll(full); err != nil {
return err
}
}
return nil
}
// unzipToDir 将 zipPath 解压到 dstDir,包含路径遍历保护
func unzipToDir(zipPath, dstDir string) error {
zr, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer zr.Close()
if err := os.MkdirAll(dstDir, 0755); err != nil {
return err
}
absDst, _ := filepath.Abs(dstDir)
for _, f := range zr.File {
// 构造目标路径并做路径遍历保护
cleanName := filepath.Clean(f.Name)
targetPath := filepath.Join(absDst, cleanName)
if !strings.HasPrefix(targetPath, absDst+string(os.PathSeparator)) && targetPath != absDst {
return fmt.Errorf("illegal file path in zip: %s", f.Name)
}
if f.FileInfo().IsDir() {
if err := os.MkdirAll(targetPath, 0755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.Create(targetPath)
if err != nil {
rc.Close()
return err
}
if _, err := io.Copy(out, rc); err != nil {
out.Close()
rc.Close()
return err
}
out.Close()
rc.Close()
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
package version
var (
CurrentVersion = "0.0.1"
VersionHash = "unknown"
)
+3 -3
View File
@@ -5,18 +5,18 @@ import (
"log/slog"
"github.com/komari-monitor/komari/cmd"
"github.com/komari-monitor/komari/internal/conf"
logutil "github.com/komari-monitor/komari/internal/log"
"github.com/komari-monitor/komari/internal/version"
)
func main() {
if version.VersionHash == "unknown" {
if conf.Version == conf.Version_Development {
logutil.SetupGlobalLogger(slog.LevelDebug)
} else {
logutil.SetupGlobalLogger(slog.LevelInfo)
}
log.Printf("Komari Monitor %s (hash: %s)", version.CurrentVersion, version.VersionHash)
log.Printf("Komari Monitor %s (hash: %s)", conf.Version, conf.CommitHash)
cmd.Execute()
}
+5 -6
View File
@@ -14,8 +14,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/conf"
)
//go:embed dist
@@ -54,12 +53,12 @@ func initIndex() {
}
RawIndexFile = string(index)
}
func UpdateIndex(cfg models.Config) {
func UpdateIndex(cfg conf.V1Struct) {
IndexFile = applyCustomizations(RawIndexFile, cfg)
}
// applyCustomizations 应用自定义内容到HTML字符串
func applyCustomizations(htmlContent string, cfg models.Config) string {
func applyCustomizations(htmlContent string, cfg conf.V1Struct) string {
var titleReplacement string
if cfg.Sitename == "Komari" {
titleReplacement = "<title>Komari Monitor</title>"
@@ -144,7 +143,7 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
}
// 获取当前主题配置
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil || cfg.Theme == "default" || cfg.Theme == "" {
// 使用默认主题(embedded文件)
serveFromEmbedded(c, path)
@@ -269,7 +268,7 @@ func serveThemeIndexWithCustomizations(c *gin.Context, indexPath string) {
}
// 获取配置以应用自定义内容
cfg, err := config.Get()
cfg, err := conf.GetWithV1Format()
if err != nil {
// 如果获取配置失败,直接返回原始文件
c.Header("Content-Type", "text/html")
+8 -8
View File
@@ -17,8 +17,8 @@ import (
"github.com/gookit/event"
apiClient "github.com/komari-monitor/komari/internal/api_v1/client"
"github.com/komari-monitor/komari/internal/common"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/database/auditlog"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/dbcore"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/eventType"
@@ -35,11 +35,11 @@ import (
func StartNezhaGRPCServer(listen string) {
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
New := e.Get("new").(models.Config)
Old := e.Get("old").(models.Config)
if New.NezhaCompatEnabled != Old.NezhaCompatEnabled {
if New.NezhaCompatEnabled {
if err := StartNezhaCompat(New.NezhaCompatListen); err != nil {
New := e.Get("new").(conf.Config)
Old := e.Get("old").(conf.Config)
if New.Compact.Nezha.NezhaCompatEnabled != Old.Compact.Nezha.NezhaCompatEnabled {
if New.Compact.Nezha.NezhaCompatEnabled {
if err := StartNezhaCompat(New.Compact.Nezha.NezhaCompatListen); err != nil {
log.Printf("start Nezha compat server error: %v", err)
auditlog.EventLog("error", fmt.Sprintf("start Nezha compat server error: %v", err))
}
@@ -387,7 +387,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
if in != nil && in.Ip != nil {
if v4 := strings.TrimSpace(in.Ip.Ipv4); v4 != "" {
updates["ipv4"] = v4
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
if ip := net.ParseIP(v4); ip != nil {
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
iso = gi.ISOCode
@@ -398,7 +398,7 @@ func (s *nezhaCompatServer) ReportGeoIP(ctx context.Context, in *proto.GeoIP) (*
if v6 := strings.TrimSpace(in.Ip.Ipv6); v6 != "" {
updates["ipv6"] = v6
if iso == "" { // 优先使用 v4 的国家码
if cfg, err := config.Get(); err == nil && cfg.GeoIpEnabled {
if cfg, err := conf.GetWithV1Format(); err == nil && cfg.GeoIpEnabled {
if ip := net.ParseIP(v6); ip != nil {
if gi, _ := geoip.GetGeoInfo(ip); gi != nil {
iso = gi.ISOCode
+11 -12
View File
@@ -8,8 +8,7 @@ import (
"github.com/gookit/event"
"github.com/komari-monitor/komari/internal"
"github.com/komari-monitor/komari/internal/api_rpc"
"github.com/komari-monitor/komari/internal/database/config"
"github.com/komari-monitor/komari/internal/database/models"
"github.com/komari-monitor/komari/internal/conf"
"github.com/komari-monitor/komari/internal/eventType"
"github.com/komari-monitor/komari/internal/geoip"
"github.com/komari-monitor/komari/internal/messageSender"
@@ -21,18 +20,18 @@ var (
)
func Init(r *gin.Engine) {
conf, _ := config.Get()
AllowCors = conf.AllowCors
config, _ := conf.GetWithV1Format()
AllowCors = config.AllowCors
event.On(eventType.ConfigUpdated, event.ListenerFunc(func(e event.Event) error {
newConf := e.Get("new").(models.Config)
oldConf := e.Get("old").(models.Config)
AllowCors = newConf.AllowCors
public.UpdateIndex(newConf)
if newConf.GeoIpProvider != oldConf.GeoIpProvider {
newConf := e.Get("new").(conf.Config)
oldConf := e.Get("old").(conf.Config)
AllowCors = newConf.Site.AllowCors
public.UpdateIndex(newConf.ToV1Format())
if newConf.GeoIp.GeoIpProvider != oldConf.GeoIp.GeoIpProvider {
go geoip.InitGeoIp()
}
if newConf.NotificationMethod != oldConf.NotificationMethod {
if newConf.Notification.NotificationMethod != oldConf.Notification.NotificationMethod {
go messageSender.Initialize()
}
return nil
@@ -58,9 +57,9 @@ func Init(r *gin.Engine) {
r.NoRoute(handlers...)
})
// #region 静态文件服务
public.UpdateIndex(conf)
public.UpdateIndex(config)
internal.LoadApiV1Routes(r, conf)
internal.LoadApiV1Routes(r, config)
api_rpc.RegisterRouters("/api/rpc2", r)
}