mirror of
https://github.com/komari-monitor/komari.git
synced 2026-11-11 17:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64683bc50a | ||
|
|
62b0dff1f4 | ||
|
|
5accc9d258 | ||
|
|
8535b4ba7a | ||
|
|
cc3cda12cf | ||
|
|
4ecf1fccd7 | ||
|
|
74b9b8f9c8 | ||
|
|
9b2a33824a | ||
|
|
2c3b5f7a47 | ||
|
|
d81ec0eee6 | ||
|
|
0ccda568ed | ||
|
|
2cf38a729a | ||
|
|
77aa490696 | ||
|
|
2e524fda42 | ||
|
|
9a6b2b8a35 |
No files matched your search
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"})
|
||||
|
||||
@@ -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, "/")
|
||||
}
|
||||
@@ -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, "/")
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user