Compare commits

..
15 Commits
Author SHA1 Message Date
Akizon 64683bc50a Merge pull request #27 from komari-monitor/dev
feat: #25 添加批量修改weight权重的接口
feat: #24 添加logout登出
feat: 添加OAuth功能及相关API接口
2025-05-12 16:23:11 +08:00
Akizon77 62b0dff1f4 feat: #25 添加批量修改weight权重的接口
fix: #24 logout 接口
2025-05-12 16:21:31 +08:00
Akizon77 5accc9d258 feat: #24 添加logout登出 2025-05-12 16:12:20 +08:00
Akizon77 8535b4ba7a fix: # 26add client 可以指定名称
fix: 修改日志记录方式,避免在读取index.html失败时导致程序崩溃
2025-05-12 16:09:47 +08:00
Akizon77 cc3cda12cf 改回来了www 2025-05-08 23:02:19 +08:00
Akizon77 4ecf1fccd7 npm -> pnpm 2025-05-08 23:00:54 +08:00
Akizon77 74b9b8f9c8 fix: 修复OAuth回调中cookie设置逻辑,确保状态验证后正确清除cookie 2025-05-08 20:41:42 +08:00
Akizon77 9b2a33824a fix: 修复OAuth未启用时的HTTP状态码返回 2025-05-08 20:28:53 +08:00
Akizon77 2c3b5f7a47 feat: 添加OAuth功能及相关API接口
修改API路径
2025-05-08 20:13:31 +08:00
Akizon d81ec0eee6 非常抱歉!!漏了一个 '\' 2025-05-08 16:56:57 +08:00
Akizon 0ccda568ed Update README.md 2025-05-08 16:52:08 +08:00
Akizon 2cf38a729a Merge pull request #23 from komari-monitor/dev
feat: 添加获取公共设置的API接口
2025-05-08 16:21:16 +08:00
Akizon77 77aa490696 feat: 添加获取公共设置的API接口 2025-05-08 16:20:44 +08:00
Akizon77 2e524fda42 fix: 修复配置保存时的时间戳和ID设置逻辑 2025-05-08 16:07:26 +08:00
Akizon77 9a6b2b8a35 feat: #19 1分钟的历史记录 2025-05-07 23:19:08 +08:00
20 changed files with 683 additions and 49 deletions

No files matched your search

+87 -1
View File
@@ -1 +1,87 @@
# komari
# Komari
Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单、高效的服务器性能监控解决方案。它支持通过 Web 界面查看服务器状态,并通过轻量级 Agent 收集数据。
## 特性
- **轻量高效**:低资源占用,适合各种规模的服务器。
- **自托管**:完全掌控数据隐私,部署简单。
- **Web 界面**:直观的监控仪表盘,易于使用。
## 快速开始
### 依赖
- Docker(快速部署)
- 或者 Go 1.18+ 和 Node.js 20+(手工构建)
### Docker 部署
1. 创建数据目录:
```bash
mkdir -p ./data
```
2. 运行 Docker 容器:
```bash
docker run -d \
-p 25774:25774 \
-v $(pwd)/data:/app/data \
--name komari \
ghcr.io/komari-monitor/komari:latest
```
3. 查看默认账号和密码:
```bash
docker logs komari
```
4. 在浏览器中访问 `http://<your_server_ip>:25774`。
> [!NOTE]
> 你也可以通过环境变量 `ADMIN_USERNAME` 和 `ADMIN_PASSWORD` 自定义初始用户名和密码。
### 二进制文件部署
1. 访问 Komari 的 [GitHub Release 页面](https://github.com/komari-monitor/komari/releases) 下载适用于你操作系统的最新二进制文件。
2. 运行 Komari:
```bash
./komari server -l 0.0.0.0:25774
```
3. 在浏览器中访问 `http://<your_server_ip>:25774`,默认监听 `25774` 端口。
4. 默认账号和密码可在启动日志中查看,或通过环境变量 `ADMIN_USERNAME` 和 `ADMIN_PASSWORD` 设置。
> [!NOTE]
> 确保二进制文件具有可执行权限(`chmod +x komari`)。数据将保存在运行目录下的 `data` 文件夹中。
### 手工构建
1. 构建前端静态文件:
```bash
git clone https://github.com/komari-monitor/komari-web
cd komari-web
npm install
npm run build
```
2. 构建后端:
```bash
git clone https://github.com/komari-monitor/komari
cd komari
```
将步骤1中生成的静态文件复制到 `komari` 项目根目录下的 `/public/dist` 文件夹。
```bash
go build -o komari
```
4. 运行:
```bash
./komari server -l 0.0.0.0:25774
```
默认监听 `25774` 端口,访问 `http://localhost:25774`。
## 前端开发指南
这个坑晚点再填吧 ヽ( ̄ω ̄( ̄ω ̄〃)ゝ
## 客户端 Agent 开发指南
这个坑晚点再填吧 (o゜▽゜)o☆
## 贡献
欢迎提交 Issue 或 Pull Request!
## 许可证
[MIT License](LICENSE)
+42
View File
@@ -0,0 +1,42 @@
package api
import (
"time"
"github.com/komari-monitor/komari/common"
"github.com/komari-monitor/komari/database/dbcore"
"github.com/komari-monitor/komari/database/models"
"github.com/komari-monitor/komari/utils"
)
var (
Records map[string][]common.Report = make(map[string][]common.Report)
)
func SaveClientReportToDB() error {
lastMinute := time.Now().Add(-time.Minute * 1).Unix()
record := []models.Record{}
// 遍历所有的客户端记录
for uuid, reports := range Records {
// 删除一分钟前的记录
filtered := reports[:0]
for _, r := range reports {
if r.UpdatedAt.Unix() >= lastMinute {
filtered = append(filtered, r)
}
}
Records[uuid] = filtered
r := utils.AverageReport(uuid, time.Now(), filtered)
record = append(record, r)
db := dbcore.GetDBInstance()
err := db.Model(&models.Record{}).Where("client = ?", uuid).Create(record).Error
if err != nil {
return err
}
}
return nil
}
+25 -21
View File
@@ -13,24 +13,33 @@ import (
)
func AddClient(c *gin.Context) {
uuid, token, err := clients.CreateClient()
var req struct {
name string
}
if err := c.ShouldBindJSON(&req); err != nil {
uuid, token, err := clients.CreateClient()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "success", "uuid": uuid, "token": token})
return
}
uuid, token, err := clients.CreateClientWithName(req.name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "success", "uuid": uuid, "token": token})
}
func EditClient(c *gin.Context) {
var req struct {
UUID string `json:"uuid" binding:"required"`
ClientName string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
Weigth int `json:"weight,omitempty"`
}
uuid := c.Param("uuid")
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": err.Error()})
return
@@ -38,7 +47,7 @@ func EditClient(c *gin.Context) {
db := dbcore.GetDBInstance()
var err error
if req.ClientName != "" {
err = db.Model(&common.ClientInfo{}).Where("uuid = ?", req.UUID).
err = db.Model(&common.ClientInfo{}).Where("uuid = ?", uuid).
Updates(map[string]interface{}{"name": req.ClientName, "updated_at": time.Now()}).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
@@ -46,7 +55,7 @@ func EditClient(c *gin.Context) {
}
}
if req.Token != "" {
err = db.Model(&models.Client{}).Where("uuid = ?", req.UUID).
err = db.Model(&models.Client{}).Where("uuid = ?", uuid).
Updates(map[string]interface{}{"token": req.Token, "updated_at": time.Now()}).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
@@ -54,7 +63,7 @@ func EditClient(c *gin.Context) {
}
}
if req.Weigth != 0 {
err = db.Model(&common.ClientInfo{}).Where("uuid = ?", req.UUID).
err = db.Model(&common.ClientInfo{}).Where("uuid = ?", uuid).
Updates(map[string]interface{}{"weigth": req.Weigth, "updated_at": time.Now()}).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
@@ -66,16 +75,8 @@ func EditClient(c *gin.Context) {
}
func RemoveClient(c *gin.Context) {
var req struct {
UUID string `json:"uuid" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{
"status": "error",
"error": "Invalid or missing UUID",
})
}
err := clients.DeleteClient(req.UUID)
uuid := c.Param("uuid")
err := clients.DeleteClient(uuid)
if err != nil {
c.JSON(500, gin.H{
"status": "error",
@@ -98,7 +99,7 @@ func ClearRecord(c *gin.Context) {
}
func GetClient(c *gin.Context) {
uuid := c.Query("uuid")
uuid := c.Param("uuid")
if uuid == "" {
c.JSON(400, gin.H{
"status": "error",
@@ -109,7 +110,10 @@ func GetClient(c *gin.Context) {
result, err := clients.GetClientByUUID(uuid)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{
"status": "error",
"error": err.Error(),
})
return
}
@@ -127,7 +131,7 @@ func ListClients(c *gin.Context) {
}
func GetClientToken(c *gin.Context) {
uuid := c.Query("uuid")
uuid := c.Param("uuid")
if uuid == "" {
c.JSON(400, gin.H{
"status": "error",
+33
View File
@@ -0,0 +1,33 @@
package admin
import (
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/common"
"github.com/komari-monitor/komari/database/dbcore"
)
func OrderWeight(c *gin.Context) {
var req = make(map[string]int)
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{
"status": "error",
"error": "Invalid or missing request body",
})
return
}
db := dbcore.GetDBInstance()
for uuid, weight := range req {
err := db.Model(&common.ClientInfo{}).Where("uuid = ?", uuid).Update("weight", weight).Error
if err != nil {
c.JSON(500, gin.H{
"status": "error",
"error": "Failed to update client weight",
})
return
}
}
c.JSON(200, gin.H{
"status": "success",
"message": "Client weight updated successfully",
})
}
+34
View File
@@ -0,0 +1,34 @@
package admin
import (
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/database/accounts"
)
func BindingExternalAccount(c *gin.Context) {
session, _ := c.Cookie("session_token")
user, err := accounts.GetUserBySession(session)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": "No user found"})
return
}
c.SetCookie("binding_external_account", user.UUID, 3600, "/", "", false, true)
c.Redirect(302, "/api/oauth")
}
func UnbindExternalAccount(c *gin.Context) {
session, _ := c.Cookie("session_token")
user, err := accounts.GetUserBySession(session)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": "No user found"})
return
}
err = accounts.UnbindExternalAccount(user.UUID)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": "Failed to unbind external account"})
return
}
c.JSON(200, gin.H{"status": "success", "message": "External account unbound"})
}
+19 -1
View File
@@ -16,7 +16,8 @@ func GetSessions(c *gin.Context) {
})
return
}
c.JSON(200, ss)
current, _ := c.Cookie("session_token")
c.JSON(200, gin.H{"status": "success", "current": current, "data": ss})
}
func DeleteSession(c *gin.Context) {
@@ -44,3 +45,20 @@ func DeleteSession(c *gin.Context) {
"message": "Session deleted successfully",
})
}
func DeleteAllSession(c *gin.Context) {
err := accounts.DeleteAllSessions()
if err != nil {
c.JSON(500, gin.H{
"status": "error",
"error": "Failed to delete session",
})
return
}
c.JSON(200, gin.H{
"status": "success",
"message": "Session deleted successfully",
})
}
+16 -5
View File
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/komari-monitor/komari/api"
"github.com/komari-monitor/komari/common"
"github.com/komari-monitor/komari/database/clients"
"github.com/komari-monitor/komari/ws"
@@ -38,13 +39,14 @@ func UploadReport(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = clients.SaveClientReport(report.UUID, report)
report.UpdatedAt = time.Now()
err = SaveClientReport(report.UUID, report)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)})
return
}
// Update report with method and token
report.UpdatedAt = time.Now()
ws.LatestReport[report.UUID] = &report
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore the body for further use
@@ -124,13 +126,22 @@ func WebSocketReport(c *gin.Context) {
if err != nil {
break
}
err = clients.SaveClientReport(uuid, report)
report.UpdatedAt = time.Now()
err = SaveClientReport(uuid, report)
if err != nil {
conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
}
report.UpdatedAt = time.Now()
ws.LatestReport[uuid] = &report
}
}
func SaveClientReport(uuid string, report common.Report) error {
if api.Records[uuid] == nil {
api.Records[uuid] = []common.Report{}
}
api.Records[uuid] = append(api.Records[uuid], report)
return nil
}
-1
View File
@@ -16,7 +16,6 @@ type LoginRequest struct {
}
func Login(c *gin.Context) {
// TODO: Settings -> Disable password login when using OAuth
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Invalid request body"})
+13
View File
@@ -0,0 +1,13 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/database/accounts"
)
func Logout(c *gin.Context) {
session, _ := c.Cookie("session_token")
accounts.DeleteSession(session)
c.SetCookie("session_token", "", -1, "/", "", false, true)
c.Redirect(302, "/")
}
+94
View File
@@ -0,0 +1,94 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/database/accounts"
"github.com/komari-monitor/komari/database/config"
"github.com/komari-monitor/komari/utils/oauth"
)
// /api/oauth
func OAuth(c *gin.Context) {
cfg, _ := config.Get()
if !cfg.OAuthEnabled {
c.JSON(403, gin.H{"status": "error", "error": "OAuth is not enabled"})
return
}
authURL, state := oauth.GetOAuthUrl()
c.SetCookie("oauth_state", state, 3600, "/", "", false, true)
c.Redirect(302, authURL)
}
// /api/oauth_callback
func OAuthCallback(c *gin.Context) {
// 验证state防止CSRF攻击
state, _ := c.Cookie("oauth_state")
c.SetCookie("oauth_state", "", -1, "/", "", false, true)
if state == "" || state != c.Query("state") {
c.JSON(400, gin.H{"status": "error", "error": "Invalid state"})
return
}
// 获取code
code := c.Query("code")
if code == "" {
c.JSON(400, gin.H{"status": "error", "error": "No code provided"})
return
}
githubUser, err := oauth.OAuthCallback(code, state)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": "Failed to get user info: " + err.Error()})
return
}
// 使用GitHub ID作为SSO ID
sso_id := fmt.Sprintf("github_%d", githubUser.ID)
// 如果cookie中有binding_external_account,说明是绑定外部账号
// 否则是登录
uuid, _ := c.Cookie("binding_external_account")
c.SetCookie("binding_external_account", "", -1, "/", "", false, true)
if uuid != "" {
// 绑定外部账号
session, _ := c.Cookie("session_token")
user, err := accounts.GetUserBySession(session)
if err != nil || user.UUID != uuid {
c.JSON(500, gin.H{"status": "error", "error": "Binding failed"})
return
}
err = accounts.BindingExternalAccount(user.UUID, sso_id)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": "Binding failed"})
return
}
c.Redirect(302, "/")
return
}
// 尝试获取用户
user, err := accounts.GetUserBySSO(sso_id)
if err != nil {
c.JSON(401, gin.H{
"status": "error",
"error": "please log in and bind your GitHub account first.",
})
return
}
// 创建会话
session, err := accounts.CreateSession(user.UUID, 2592000)
if err != nil {
c.JSON(500, gin.H{"status": "error", "error": err.Error()})
return
}
// 设置cookie并返回
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
c.Redirect(302, "/")
}
+27
View File
@@ -0,0 +1,27 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/komari-monitor/komari/database/config"
)
func GetPublicSettings(c *gin.Context) {
cst, err := config.Get()
if err != nil {
c.JSON(500, gin.H{
"status": "error",
"error": "Internal Server Error: " + err.Error(),
})
return
}
c.JSON(200, gin.H{
"status": "success",
"data": gin.H{
"sitename": cst.Sitename,
"desc": cst.Description,
"custom_head": cst.CustomHead,
"oauth_enable": cst.OAuthEnabled,
"allow_cros": cst.AllowCros,
},
})
}
+45 -11
View File
@@ -65,6 +65,10 @@ var ServerCmd = &cobra.Command{
r.GET("/api/me", api.GetMe)
r.GET("/api/clients", ws.GetClients)
r.GET("/api/nodes", api.GetNodesInformation)
r.GET("/api/public", api.GetPublicSettings)
r.GET("/api/oauth", api.OAuth)
r.GET("/api/oauth_callback", api.OAuthCallback)
r.GET("/api/logout", api.Logout)
tokenAuthrized := r.Group("/api/clients", api.TokenAuthMiddleware())
{
@@ -75,17 +79,38 @@ var ServerCmd = &cobra.Command{
adminAuthrized := r.Group("/api/admin", api.AdminAuthMiddleware())
{
// clients
adminAuthrized.POST("/addClient", admin.AddClient)
adminAuthrized.POST("/editClient", admin.EditClient)
adminAuthrized.GET("/listClients", admin.ListClients)
adminAuthrized.GET("/getClient", admin.GetClient)
adminAuthrized.POST("/clearRecord", admin.ClearRecord)
adminAuthrized.POST("/removeClient", admin.RemoveClient)
adminAuthrized.GET("/clientToken", admin.GetClientToken)
// settings
adminAuthrized.GET("/settings", admin.GetSettings)
adminAuthrized.POST("/settings", admin.EditSettings)
// clients
clientGroup := adminAuthrized.Group("/client")
{
clientGroup.POST("/add", admin.AddClient)
clientGroup.GET("/list", admin.ListClients)
clientGroup.GET("/:uuid", admin.GetClient)
clientGroup.POST("/:uuid/edit", admin.EditClient)
clientGroup.POST("/:uuid/remove", admin.RemoveClient)
clientGroup.GET("/:uuid/token", admin.GetClientToken)
clientGroup.POST("/order", admin.OrderWeight)
}
// records
recordGroup := adminAuthrized.Group("/record")
{
recordGroup.POST("/clear", admin.ClearRecord)
}
// oauth2
oauth2Group := adminAuthrized.Group("/oauth2")
{
oauth2Group.GET("/bind", admin.BindingExternalAccount)
oauth2Group.POST("/unbind", admin.UnbindExternalAccount)
}
sessionGroup := adminAuthrized.Group("/session")
{
sessionGroup.GET("/get", admin.GetSessions)
sessionGroup.POST("/remove", admin.DeleteSession)
sessionGroup.POST("/remove/all", admin.DeleteAllSession)
}
}
@@ -123,10 +148,19 @@ func InitDatabase() {
func DoRecordsWork() {
ticker := time.NewTicker(time.Minute * 30)
ticker1 := time.NewTicker(60 * time.Second)
records.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 30))
records.CompactRecord()
for range ticker.C {
records.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 30))
records.CompactRecord()
for {
select {
case <-ticker.C:
records.DeleteRecordBefore(time.Now().Add(-time.Hour * 24 * 30))
records.CompactRecord()
break
case <-ticker1.C:
api.SaveClientReportToDB()
break
}
}
}
+21 -2
View File
@@ -122,7 +122,7 @@ func GetUserByUUID(uuid string) (user models.User, err error) {
}
// 通过 SSO 信息获取用户
func GetUserBySSO(ssoID, username string) (user models.User, err error) {
func GetUserBySSO(ssoID string) (user models.User, err error) {
db := dbcore.GetDBInstance()
// 首先尝试查找已存在的用户
@@ -131,5 +131,24 @@ func GetUserBySSO(ssoID, username string) (user models.User, err error) {
return user, nil
}
return user, nil
// 如果找不到用户,返回明确的错误信息
return models.User{}, fmt.Errorf("用户不存在:%s", ssoID)
}
func BindingExternalAccount(uuid string, sso_id string) error {
db := dbcore.GetDBInstance()
err := db.Model(&models.User{}).Where("uuid = ?", uuid).Update("sso_id", sso_id).Error
if err != nil {
return err
}
return nil
}
func UnbindExternalAccount(uuid string) error {
db := dbcore.GetDBInstance()
err := db.Model(&models.User{}).Where("uuid = ?", uuid).Update("sso_id", "").Error
if err != nil {
return err
}
return nil
}
+36 -2
View File
@@ -7,8 +7,11 @@ import (
"github.com/komari-monitor/komari/database/dbcore"
"github.com/komari-monitor/komari/database/models"
"github.com/komari-monitor/komari/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"fmt"
"github.com/google/uuid"
)
@@ -156,6 +159,35 @@ func CreateClient() (clientUUID, token string, err error) {
return clientUUID, token, nil
}
func CreateClientWithName(name string) (clientUUID, token string, err error) {
if name == "" {
return CreateClient()
}
db := dbcore.GetDBInstance()
token = utils.GenerateToken()
clientUUID = uuid.New().String()
client := models.Client{
UUID: clientUUID,
Token: token,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = db.Create(&client).Error
if err != nil {
return "", "", err
}
clientInfo := common.ClientInfo{
UUID: clientUUID,
Name: name,
}
err = db.Create(&clientInfo).Error
if err != nil {
return "", "", err
}
return clientUUID, token, nil
}
/*
// GetAllClients 获取所有客户端配置
@@ -182,9 +214,11 @@ func GetClientBasicInfo(uuid string) (client common.ClientInfo, err error) {
db := dbcore.GetDBInstance()
err = db.Where("uuid = ?", uuid).First(&client).Error
if err != nil {
return client, err
if err == gorm.ErrRecordNotFound {
return common.ClientInfo{}, fmt.Errorf("客户端不存在: %s", uuid)
}
return common.ClientInfo{}, err
}
return client, nil
}
+24 -3
View File
@@ -1,6 +1,8 @@
package config
import (
"time"
"github.com/komari-monitor/komari/database/dbcore"
"github.com/komari-monitor/komari/database/models"
"github.com/komari-monitor/komari/public"
@@ -13,7 +15,13 @@ func Get() (models.Config, error) {
if err := db.First(&config).Error; err != nil {
if err == gorm.ErrRecordNotFound {
config = models.Config{
ID: 1,
ID: 1,
Sitename: "Komari",
Description: "Komari Monitor, a simple server monitoring tool.",
AllowCros: false,
OAuthEnabled: false,
UpdatedAt: time.Now(),
CreatedAt: time.Now(),
}
Save(config)
return config, nil
@@ -27,8 +35,21 @@ func Save(cst models.Config) error {
db := dbcore.GetDBInstance()
var config models.Config
// Only one records
config.ID = 1
if err := db.Save(&config).Error; err != nil {
cst.ID = 1
cst.UpdatedAt = time.Now()
// Do not update CreatedAt
if err := db.Model(&models.Config{}).Where("id = ?", cst.ID).
Select("sitename",
"description",
"allow_cros",
"geo_ip_enabled",
"geo_ip_provider",
"o_auth_client_id",
"o_auth_client_secret",
"o_auth_enabled",
"custom_head",
"updated_at").
Updates(cst).Error; err != nil {
return err
}
go public.UpdateIndex(config)
+7 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/komari-monitor/komari/database/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
@@ -47,7 +48,12 @@ func InitDatabase() bool {
func GetDBInstance() *gorm.DB {
once.Do(func() {
var err error
instance, err = gorm.Open(sqlite.Open(flags.DatabaseFile), &gorm.Config{})
logConfig := &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
}
instance, err = gorm.Open(sqlite.Open(flags.DatabaseFile), logConfig)
if err != nil {
log.Fatalf("Failed to connect to SQLite3 database: %v", err)
}
+5
View File
@@ -226,3 +226,8 @@ func processTimeRange(db *gorm.DB, clientUUID string, start, end time.Time, inte
return tx.Commit().Error
}
func Save(rec models.Record) error {
db := dbcore.GetDBInstance()
return db.Create(&rec).Error
}
+1 -1
View File
@@ -41,7 +41,7 @@ func initIndex() {
}()
index, err := io.ReadAll(indexFile)
if err != nil {
log.Fatalln("Failed to read index.html:", err)
log.Println("Failed to read index.html:", err)
}
RawIndexFile = string(index)
}
+95
View File
@@ -0,0 +1,95 @@
package oauth
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/komari-monitor/komari/database/config"
"github.com/komari-monitor/komari/utils"
)
type GitHubUser struct {
ID int `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
}
// 获取OAuth的URL、state !!请务必验证state防止CSRF攻击
func GetOAuthUrl() (string, string) {
cfg, _ := config.Get()
state := utils.GenerateRandomString(16)
// 构建GitHub OAuth授权URL
authURL := fmt.Sprintf(
"https://github.com/login/oauth/authorize?client_id=%s&state=%s&scope=user:email",
url.QueryEscape(cfg.OAuthClientID),
url.QueryEscape(state),
)
return authURL, state
}
func OAuthCallback(code, state string) (GitHubUser, error) {
cfg, _ := config.Get()
// 验证state防止CSRF攻击
// state, _ := c.Cookie("oauth_state")
if state == "" {
return GitHubUser{}, fmt.Errorf("invalid state")
}
// 获取code
//code := c.Query("code")
if code == "" {
return GitHubUser{}, fmt.Errorf("no code provided")
}
// 获取访问令牌
tokenURL := "https://github.com/login/oauth/access_token"
data := url.Values{
"client_id": {cfg.OAuthClientID},
"client_secret": {cfg.OAuthClientSecret},
"code": {code},
}
req, _ := http.NewRequest("POST", tokenURL, nil)
req.URL.RawQuery = data.Encode()
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return GitHubUser{}, fmt.Errorf("failed to get access token: %v", err)
}
defer resp.Body.Close()
var tokenResp struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return GitHubUser{}, fmt.Errorf("failed to parse access token response: %v", err)
}
// 获取用户信息
userReq, _ := http.NewRequest("GET", "https://api.github.com/user", nil)
userReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken)
userReq.Header.Set("Accept", "application/json")
userResp, err := http.DefaultClient.Do(userReq)
if err != nil {
return GitHubUser{}, fmt.Errorf("failed to get user info: %v", err)
}
defer userResp.Body.Close()
var githubUser GitHubUser
if err := json.NewDecoder(userResp.Body).Decode(&githubUser); err != nil {
return GitHubUser{}, fmt.Errorf("failed to parse user info response: %v", err)
}
return githubUser, nil
}
+59
View File
@@ -0,0 +1,59 @@
package utils
import (
"time"
"github.com/komari-monitor/komari/common"
"github.com/komari-monitor/komari/database/models"
)
func AverageReport(uuid string, time time.Time, records []common.Report) models.Record {
var sumCPU, sumGPU, sumLOAD, sumTEMP float32
var sumRAM, sumRAMTotal, sumSWAP, sumSWAPTotal, sumDISK, sumDISKTotal int64
var sumNETIn, sumNETOut, sumNETTotalUp, sumNETTotalDown int64
var sumPROCESS, sumConnections, sumConnectionsUDP int
count := len(records)
if count == 0 {
return models.Record{}
}
for _, r := range records {
sumCPU += float32(r.CPU.Usage)
sumGPU += 0
sumLOAD += float32(r.Load.Load1)
sumTEMP += 0
sumRAM += r.Ram.Used
sumSWAP += r.Swap.Used
sumDISK += r.Disk.Used
sumNETIn += r.Network.Down
sumNETOut += r.Network.Up
sumNETTotalUp += r.Network.TotalUp
sumNETTotalDown += r.Network.TotalDown
sumPROCESS += r.Process
sumConnections += r.Connections.TCP
sumConnectionsUDP += r.Connections.UDP
}
// Create new aggregated record
newRecord := models.Record{
Client: uuid,
Time: time,
CPU: sumCPU / float32(count),
GPU: sumGPU / float32(count),
RAM: sumRAM / int64(count),
RAMTotal: sumRAMTotal / int64(count),
SWAP: sumSWAP / int64(count),
SWAPTotal: sumSWAPTotal / int64(count),
LOAD: sumLOAD / float32(count),
TEMP: sumTEMP / float32(count),
DISK: sumDISK / int64(count),
DISKTotal: sumDISKTotal / int64(count),
NETIn: sumNETIn / int64(count),
NETOut: sumNETOut / int64(count),
NETTotalUp: sumNETTotalUp / int64(count),
NETTotalDown: sumNETTotalDown / int64(count),
PROCESS: sumPROCESS / count,
Connections: sumConnections / count,
ConnectionsUDP: sumConnectionsUDP / count,
}
return newRecord
}