Spaces:
Runtime error
Runtime error
Upload 16 files
Browse files- .dockerignore +9 -0
- .env +12 -0
- .env.example +10 -0
- Dockerfile +37 -0
- controllers/auth.go +55 -0
- controllers/monitor.go +111 -0
- database/database.go +36 -0
- database/redis.go +45 -0
- go.mod +58 -0
- go.sum +125 -0
- main.go +98 -0
- middleware/requireAuth.go +56 -0
- models/latency.go +9 -0
- models/monitor.go +22 -0
- models/user.go +9 -0
- services/monitor.go +148 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
README.md
|
| 5 |
+
Dockerfile
|
| 6 |
+
.dockerignore
|
| 7 |
+
test.db
|
| 8 |
+
*.log
|
| 9 |
+
.DS_Store
|
.env
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# For local development, use sqlite
|
| 2 |
+
DB_DIALECT=sqlite
|
| 3 |
+
|
| 4 |
+
# For production, comment out the line above and use mysql
|
| 5 |
+
# DB_DIALECT=mysql
|
| 6 |
+
# DATABASE_URI="user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
|
| 7 |
+
SECRET="asupersecretkey"
|
| 8 |
+
|
| 9 |
+
ADMIN_USERNAME="admin"
|
| 10 |
+
ADMIN_PASSWORD="password"
|
| 11 |
+
|
| 12 |
+
CORS_ALLOWED_ORIGINS="http://localhost:3000,http://192.168.2.4:3000"
|
.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_DIALECT=mysql
|
| 2 |
+
DATABASE_URI="user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
|
| 3 |
+
|
| 4 |
+
REDIS_URI="rediss://user:password@host:port/db"
|
| 5 |
+
SECRET="asupersecretkey"
|
| 6 |
+
|
| 7 |
+
ADMIN_USERNAME="admin"
|
| 8 |
+
ADMIN_PASSWORD="password"
|
| 9 |
+
|
| 10 |
+
CORS_ALLOWED_ORIGINS="http://localhost:3000,http://192.168.2.4:3000"
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用官方 Go 镜像作为构建环境
|
| 2 |
+
FROM golang:1.21-alpine AS builder
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 复制 go mod 文件
|
| 8 |
+
COPY go.mod go.sum ./
|
| 9 |
+
|
| 10 |
+
# 下载依赖
|
| 11 |
+
RUN go mod download
|
| 12 |
+
|
| 13 |
+
# 复制源代码
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# 构建应用
|
| 17 |
+
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o main .
|
| 18 |
+
|
| 19 |
+
# 使用轻量级的 Alpine Linux 作为运行环境
|
| 20 |
+
FROM alpine:latest
|
| 21 |
+
|
| 22 |
+
# 安装必要的包
|
| 23 |
+
RUN apk --no-cache add ca-certificates sqlite
|
| 24 |
+
|
| 25 |
+
# 设置工作目录
|
| 26 |
+
WORKDIR /root/
|
| 27 |
+
|
| 28 |
+
# 从构建阶段复制可执行文件
|
| 29 |
+
COPY --from=builder /app/main .
|
| 30 |
+
|
| 31 |
+
# 环境变量将在运行时传入,而不是在构建时。
|
| 32 |
+
|
| 33 |
+
# 暴露端口
|
| 34 |
+
EXPOSE 8080
|
| 35 |
+
|
| 36 |
+
# 运行应用
|
| 37 |
+
CMD ["./main"]
|
controllers/auth.go
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"os"
|
| 6 |
+
"time"
|
| 7 |
+
"uptime/backend/database"
|
| 8 |
+
"uptime/backend/models"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
"github.com/golang-jwt/jwt/v5"
|
| 12 |
+
"golang.org/x/crypto/bcrypt"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func Login(c *gin.Context) {
|
| 16 |
+
var body struct {
|
| 17 |
+
Username string `json:"username"`
|
| 18 |
+
Password string `json:"password"`
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if err := c.BindJSON(&body); err != nil {
|
| 22 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
|
| 23 |
+
return
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
var user models.User
|
| 27 |
+
database.DB.First(&user, "username = ?", body.Username)
|
| 28 |
+
|
| 29 |
+
if user.ID == 0 {
|
| 30 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid username or password"})
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password))
|
| 35 |
+
if err != nil {
|
| 36 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid username or password"})
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
| 41 |
+
"sub": user.ID,
|
| 42 |
+
"exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
tokenString, err := token.SignedString([]byte(os.Getenv("SECRET")))
|
| 46 |
+
if err != nil {
|
| 47 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to create token"})
|
| 48 |
+
return
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
c.SetSameSite(http.SameSiteLaxMode)
|
| 52 |
+
c.SetCookie("Authorization", tokenString, 3600*24*30, "/", "", false, true)
|
| 53 |
+
|
| 54 |
+
c.JSON(http.StatusOK, gin.H{})
|
| 55 |
+
}
|
controllers/monitor.go
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controllers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"uptime/backend/database"
|
| 6 |
+
"uptime/backend/models"
|
| 7 |
+
"uptime/backend/services"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// CreateMonitor creates a new monitor
|
| 13 |
+
func CreateMonitor(c *gin.Context) {
|
| 14 |
+
var monitor models.Monitor
|
| 15 |
+
if err := c.ShouldBindJSON(&monitor); err != nil {
|
| 16 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 17 |
+
return
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
database.DB.Create(&monitor)
|
| 21 |
+
c.JSON(http.StatusOK, monitor)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// GetMonitors retrieves all monitors
|
| 25 |
+
func GetMonitors(c *gin.Context) {
|
| 26 |
+
var monitors []models.Monitor
|
| 27 |
+
database.DB.Find(&monitors)
|
| 28 |
+
|
| 29 |
+
type MonitorWithLatency struct {
|
| 30 |
+
models.Monitor
|
| 31 |
+
Latency uint `json:"latency"`
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
var monitorsWithLatency []MonitorWithLatency
|
| 35 |
+
|
| 36 |
+
for _, m := range monitors {
|
| 37 |
+
var latency models.Latency
|
| 38 |
+
database.DB.Where("monitor_id = ?", m.ID).Order("created_at desc").First(&latency)
|
| 39 |
+
monitorsWithLatency = append(monitorsWithLatency, MonitorWithLatency{
|
| 40 |
+
Monitor: m,
|
| 41 |
+
Latency: latency.Latency,
|
| 42 |
+
})
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
c.JSON(http.StatusOK, monitorsWithLatency)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// GetMonitor retrieves a single monitor by ID
|
| 49 |
+
func GetMonitor(c *gin.Context) {
|
| 50 |
+
id := c.Params.ByName("id")
|
| 51 |
+
var monitor models.Monitor
|
| 52 |
+
if err := database.DB.First(&monitor, id).Error; err != nil {
|
| 53 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "Monitor not found"})
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
var latencies []models.Latency
|
| 58 |
+
database.DB.Where("monitor_id = ?", monitor.ID).Order("created_at desc").Limit(100).Find(&latencies)
|
| 59 |
+
|
| 60 |
+
c.JSON(http.StatusOK, gin.H{
|
| 61 |
+
"monitor": monitor,
|
| 62 |
+
"latencies": latencies,
|
| 63 |
+
})
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// UpdateMonitor updates an existing monitor
|
| 67 |
+
func UpdateMonitor(c *gin.Context) {
|
| 68 |
+
id := c.Params.ByName("id")
|
| 69 |
+
var monitor models.Monitor
|
| 70 |
+
if err := database.DB.First(&monitor, id).Error; err != nil {
|
| 71 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "Monitor not found"})
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if err := c.ShouldBindJSON(&monitor); err != nil {
|
| 76 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
database.DB.Save(&monitor)
|
| 81 |
+
c.JSON(http.StatusOK, monitor)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// DeleteMonitor deletes a monitor
|
| 85 |
+
func DeleteMonitor(c *gin.Context) {
|
| 86 |
+
id := c.Params.ByName("id")
|
| 87 |
+
var monitor models.Monitor
|
| 88 |
+
if err := database.DB.First(&monitor, id).Error; err != nil {
|
| 89 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "Monitor not found"})
|
| 90 |
+
return
|
| 91 |
+
}
|
| 92 |
+
database.DB.Unscoped().Delete(&monitor)
|
| 93 |
+
services.DeletedMonitorIDs <- monitor.ID
|
| 94 |
+
c.JSON(http.StatusOK, gin.H{"message": "Monitor deleted successfully"})
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// ManualCheck triggers a manual check for a specific monitor
|
| 98 |
+
func ManualCheck(c *gin.Context) {
|
| 99 |
+
id := c.Params.ByName("id")
|
| 100 |
+
var monitor models.Monitor
|
| 101 |
+
if err := database.DB.First(&monitor, id).Error; err != nil {
|
| 102 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "Monitor not found"})
|
| 103 |
+
return
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// TODO: Trigger manual check - for now just update status to pending
|
| 107 |
+
monitor.Status = "pending"
|
| 108 |
+
database.DB.Save(&monitor)
|
| 109 |
+
|
| 110 |
+
c.JSON(http.StatusOK, gin.H{"message": "Manual check triggered", "monitor": monitor})
|
| 111 |
+
}
|
database/database.go
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package database
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"strings"
|
| 7 |
+
"uptime/backend/models"
|
| 8 |
+
|
| 9 |
+
"github.com/glebarez/sqlite"
|
| 10 |
+
"gorm.io/driver/mysql"
|
| 11 |
+
"gorm.io/gorm"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var DB *gorm.DB
|
| 15 |
+
|
| 16 |
+
func ConnectDatabase() {
|
| 17 |
+
var err error
|
| 18 |
+
dsn := os.Getenv("DATABASE_URI")
|
| 19 |
+
if dsn == "" {
|
| 20 |
+
log.Println("DATABASE_URI is not set, falling back to sqlite.")
|
| 21 |
+
DB, err = gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
| 22 |
+
} else if strings.HasPrefix(dsn, "mysql://") {
|
| 23 |
+
// GORM's MySQL driver expects a DSN without the scheme.
|
| 24 |
+
// Example: user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
| 25 |
+
mysqlDSN := strings.TrimPrefix(dsn, "mysql://")
|
| 26 |
+
DB, err = gorm.Open(mysql.Open(mysqlDSN), &gorm.Config{})
|
| 27 |
+
} else {
|
| 28 |
+
log.Fatalf("Unsupported database URI scheme: %s", dsn)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if err != nil {
|
| 32 |
+
log.Fatal("Failed to connect to database!", err)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
DB.AutoMigrate(&models.Monitor{}, &models.Latency{}, &models.User{})
|
| 36 |
+
}
|
database/redis.go
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package database
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"crypto/tls"
|
| 6 |
+
"log"
|
| 7 |
+
"os"
|
| 8 |
+
"strings"
|
| 9 |
+
|
| 10 |
+
"github.com/go-redis/redis/v8"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var RDB *redis.Client
|
| 14 |
+
|
| 15 |
+
func ConnectRedis() {
|
| 16 |
+
redisURI := os.Getenv("REDIS_URI")
|
| 17 |
+
if redisURI == "" {
|
| 18 |
+
log.Println("REDIS_URI is not set, skipping Redis connection.")
|
| 19 |
+
return
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
opt, err := redis.ParseURL(redisURI)
|
| 23 |
+
if err != nil {
|
| 24 |
+
log.Fatalf("Failed to parse Redis URI: %v", err)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// The `rediss` scheme implies TLS. The go-redis library handles this automatically
|
| 28 |
+
// if the URI scheme is "rediss". For explicit clarity or custom TLS configs,
|
| 29 |
+
// you could add a TLS config to opt.TLSConfig.
|
| 30 |
+
if strings.HasPrefix(redisURI, "rediss://") {
|
| 31 |
+
opt.TLSConfig = &tls.Config{
|
| 32 |
+
MinVersion: tls.VersionTLS12,
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
RDB = redis.NewClient(opt)
|
| 37 |
+
|
| 38 |
+
ctx := context.Background()
|
| 39 |
+
_, err = RDB.Ping(ctx).Result()
|
| 40 |
+
if err != nil {
|
| 41 |
+
log.Fatalf("Failed to connect to Redis: %v", err)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
log.Println("Successfully connected to Redis.")
|
| 45 |
+
}
|
go.mod
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module uptime/backend
|
| 2 |
+
|
| 3 |
+
go 1.24.1
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
filippo.io/edwards25519 v1.1.0 // indirect
|
| 7 |
+
github.com/bytedance/sonic v1.13.3 // indirect
|
| 8 |
+
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
| 9 |
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
| 10 |
+
github.com/cloudwego/base64x v0.1.5 // indirect
|
| 11 |
+
github.com/cloudwego/iasm v0.2.0 // indirect
|
| 12 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
| 13 |
+
github.com/dustin/go-humanize v1.0.1 // indirect
|
| 14 |
+
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
| 15 |
+
github.com/gin-contrib/cors v1.7.6 // indirect
|
| 16 |
+
github.com/gin-contrib/sse v1.1.0 // indirect
|
| 17 |
+
github.com/gin-gonic/gin v1.10.1 // indirect
|
| 18 |
+
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
| 19 |
+
github.com/glebarez/sqlite v1.11.0 // indirect
|
| 20 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 21 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 22 |
+
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
| 23 |
+
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
| 24 |
+
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
| 25 |
+
github.com/goccy/go-json v0.10.5 // indirect
|
| 26 |
+
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
| 27 |
+
github.com/google/uuid v1.6.0 // indirect
|
| 28 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 29 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 30 |
+
github.com/joho/godotenv v1.5.1 // indirect
|
| 31 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 32 |
+
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
| 33 |
+
github.com/leodido/go-urn v1.4.0 // indirect
|
| 34 |
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 35 |
+
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
| 36 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 37 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 38 |
+
github.com/ncruces/go-strftime v0.1.9 // indirect
|
| 39 |
+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
| 40 |
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
| 41 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 42 |
+
github.com/ugorji/go/codec v1.3.0 // indirect
|
| 43 |
+
golang.org/x/arch v0.19.0 // indirect
|
| 44 |
+
golang.org/x/crypto v0.40.0 // indirect
|
| 45 |
+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
| 46 |
+
golang.org/x/net v0.42.0 // indirect
|
| 47 |
+
golang.org/x/sys v0.34.0 // indirect
|
| 48 |
+
golang.org/x/text v0.27.0 // indirect
|
| 49 |
+
google.golang.org/protobuf v1.36.6 // indirect
|
| 50 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 51 |
+
gorm.io/driver/mysql v1.6.0 // indirect
|
| 52 |
+
gorm.io/driver/sqlite v1.6.0 // indirect
|
| 53 |
+
gorm.io/gorm v1.30.0 // indirect
|
| 54 |
+
modernc.org/libc v1.65.10 // indirect
|
| 55 |
+
modernc.org/mathutil v1.7.1 // indirect
|
| 56 |
+
modernc.org/memory v1.11.0 // indirect
|
| 57 |
+
modernc.org/sqlite v1.38.0 // indirect
|
| 58 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
| 2 |
+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
| 3 |
+
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
| 4 |
+
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
| 5 |
+
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 6 |
+
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
| 7 |
+
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
| 8 |
+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
| 9 |
+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 10 |
+
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
| 11 |
+
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
| 12 |
+
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
| 13 |
+
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
| 14 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 15 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 16 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
| 17 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
| 18 |
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
| 19 |
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
| 20 |
+
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
| 21 |
+
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
| 22 |
+
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
| 23 |
+
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
| 24 |
+
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
| 25 |
+
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
| 26 |
+
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
| 27 |
+
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
| 28 |
+
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
| 29 |
+
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
| 30 |
+
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
| 31 |
+
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
| 32 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 33 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 34 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 35 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 36 |
+
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
| 37 |
+
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
| 38 |
+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
| 39 |
+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
| 40 |
+
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
| 41 |
+
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
| 42 |
+
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
| 43 |
+
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 44 |
+
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
| 45 |
+
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
| 46 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 47 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 48 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 49 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 50 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 51 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 52 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 53 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 54 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 55 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 56 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 57 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 58 |
+
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
| 59 |
+
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
| 60 |
+
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
| 61 |
+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 62 |
+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
| 63 |
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 64 |
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 65 |
+
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
| 66 |
+
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
| 67 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 68 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 69 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 70 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 71 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 72 |
+
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
| 73 |
+
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
| 74 |
+
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
| 75 |
+
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
| 76 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 77 |
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
| 78 |
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
| 79 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 80 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 81 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 82 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 83 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 84 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 85 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 86 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 87 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 88 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 89 |
+
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
| 90 |
+
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
| 91 |
+
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
| 92 |
+
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
| 93 |
+
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
| 94 |
+
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
| 95 |
+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
| 96 |
+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
| 97 |
+
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
| 98 |
+
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
| 99 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 100 |
+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
| 101 |
+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
| 102 |
+
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
| 103 |
+
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
| 104 |
+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
| 105 |
+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
| 106 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 107 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 108 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 109 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 110 |
+
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
| 111 |
+
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
| 112 |
+
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
| 113 |
+
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
| 114 |
+
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
| 115 |
+
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
| 116 |
+
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
| 117 |
+
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
| 118 |
+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
| 119 |
+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
| 120 |
+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
| 121 |
+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
| 122 |
+
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
| 123 |
+
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
| 124 |
+
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
| 125 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
main.go
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"strings"
|
| 7 |
+
"uptime/backend/controllers"
|
| 8 |
+
"uptime/backend/database"
|
| 9 |
+
"uptime/backend/middleware"
|
| 10 |
+
"uptime/backend/models"
|
| 11 |
+
"uptime/backend/services"
|
| 12 |
+
|
| 13 |
+
"github.com/gin-contrib/cors"
|
| 14 |
+
"github.com/gin-gonic/gin"
|
| 15 |
+
"github.com/joho/godotenv"
|
| 16 |
+
"golang.org/x/crypto/bcrypt"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
func main() {
|
| 20 |
+
// 在开发环境中加载 .env 文件,如果文件不存在则忽略错误
|
| 21 |
+
// 在生产环境中,变量将通过环境变量直接提供
|
| 22 |
+
if err := godotenv.Load(); err != nil {
|
| 23 |
+
log.Println("No .env file found, using environment variables")
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
database.ConnectDatabase()
|
| 27 |
+
database.ConnectRedis()
|
| 28 |
+
|
| 29 |
+
createAdminUser()
|
| 30 |
+
|
| 31 |
+
go services.StartMonitoring()
|
| 32 |
+
|
| 33 |
+
r := gin.Default()
|
| 34 |
+
config := cors.DefaultConfig()
|
| 35 |
+
origins := os.Getenv("CORS_ALLOWED_ORIGINS")
|
| 36 |
+
if origins == "" {
|
| 37 |
+
origins = "http://localhost:3000" // Default for local development
|
| 38 |
+
}
|
| 39 |
+
config.AllowOrigins = strings.Split(origins, ",")
|
| 40 |
+
config.AllowCredentials = true
|
| 41 |
+
r.Use(cors.New(config))
|
| 42 |
+
|
| 43 |
+
api := r.Group("/api")
|
| 44 |
+
{
|
| 45 |
+
api.POST("/login", controllers.Login)
|
| 46 |
+
|
| 47 |
+
authorized := api.Group("/")
|
| 48 |
+
authorized.Use(middleware.RequireAuth)
|
| 49 |
+
{
|
| 50 |
+
authorized.POST("/monitors", controllers.CreateMonitor)
|
| 51 |
+
authorized.GET("/monitors", controllers.GetMonitors)
|
| 52 |
+
authorized.GET("/monitors/:id", controllers.GetMonitor)
|
| 53 |
+
authorized.PUT("/monitors/:id", controllers.UpdateMonitor)
|
| 54 |
+
authorized.DELETE("/monitors/:id", controllers.DeleteMonitor)
|
| 55 |
+
authorized.POST("/monitors/:id/check", controllers.ManualCheck)
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
r.GET("/ping", func(c *gin.Context) {
|
| 60 |
+
c.JSON(200, gin.H{
|
| 61 |
+
"message": "pong",
|
| 62 |
+
})
|
| 63 |
+
})
|
| 64 |
+
r.Run() // listen and serve on 0.0.0.0:8080
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func createAdminUser() {
|
| 68 |
+
username := os.Getenv("ADMIN_USERNAME")
|
| 69 |
+
password := os.Getenv("ADMIN_PASSWORD")
|
| 70 |
+
|
| 71 |
+
if username == "" || password == "" {
|
| 72 |
+
log.Println("ADMIN_USERNAME or ADMIN_PASSWORD not set, skipping admin user creation.")
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
var user models.User
|
| 77 |
+
database.DB.First(&user, "username = ?", username)
|
| 78 |
+
|
| 79 |
+
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
| 80 |
+
if err != nil {
|
| 81 |
+
log.Fatalf("Failed to hash password: %v", err)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if user.ID == 0 {
|
| 85 |
+
// Create new admin user
|
| 86 |
+
admin := models.User{Username: username, Password: string(hashedPassword)}
|
| 87 |
+
if err := database.DB.Create(&admin).Error; err != nil {
|
| 88 |
+
log.Fatalf("Failed to create admin user: %v", err)
|
| 89 |
+
}
|
| 90 |
+
log.Println("Admin user created successfully.")
|
| 91 |
+
} else {
|
| 92 |
+
// Update existing admin user's password
|
| 93 |
+
if err := database.DB.Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
| 94 |
+
log.Fatalf("Failed to update admin user: %v", err)
|
| 95 |
+
}
|
| 96 |
+
log.Println("Admin user password updated successfully.")
|
| 97 |
+
}
|
| 98 |
+
}
|
middleware/requireAuth.go
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"net/http"
|
| 6 |
+
"os"
|
| 7 |
+
"time"
|
| 8 |
+
"uptime/backend/database"
|
| 9 |
+
"uptime/backend/models"
|
| 10 |
+
|
| 11 |
+
"github.com/gin-gonic/gin"
|
| 12 |
+
"github.com/golang-jwt/jwt/v5"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
func RequireAuth(c *gin.Context) {
|
| 16 |
+
tokenString, err := c.Cookie("Authorization")
|
| 17 |
+
|
| 18 |
+
if err != nil {
|
| 19 |
+
c.AbortWithStatus(http.StatusUnauthorized)
|
| 20 |
+
return
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
| 24 |
+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
| 25 |
+
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return []byte(os.Getenv("SECRET")), nil
|
| 29 |
+
})
|
| 30 |
+
|
| 31 |
+
if err != nil {
|
| 32 |
+
c.AbortWithStatus(http.StatusUnauthorized)
|
| 33 |
+
return
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
| 37 |
+
if float64(time.Now().Unix()) > claims["exp"].(float64) {
|
| 38 |
+
c.AbortWithStatus(http.StatusUnauthorized)
|
| 39 |
+
return
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
var user models.User
|
| 43 |
+
database.DB.First(&user, claims["sub"])
|
| 44 |
+
|
| 45 |
+
if user.ID == 0 {
|
| 46 |
+
c.AbortWithStatus(http.StatusUnauthorized)
|
| 47 |
+
return
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
c.Set("user", user)
|
| 51 |
+
|
| 52 |
+
c.Next()
|
| 53 |
+
} else {
|
| 54 |
+
c.AbortWithStatus(http.StatusUnauthorized)
|
| 55 |
+
}
|
| 56 |
+
}
|
models/latency.go
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import "gorm.io/gorm"
|
| 4 |
+
|
| 5 |
+
type Latency struct {
|
| 6 |
+
gorm.Model
|
| 7 |
+
MonitorID uint `json:"monitorId"`
|
| 8 |
+
Latency uint `json:"latency"` // Latency in milliseconds
|
| 9 |
+
}
|
models/monitor.go
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import "gorm.io/gorm"
|
| 4 |
+
|
| 5 |
+
type Monitor struct {
|
| 6 |
+
gorm.Model
|
| 7 |
+
Name string `json:"name"`
|
| 8 |
+
URL string `json:"url"`
|
| 9 |
+
Interval uint `json:"interval"` // Interval in seconds
|
| 10 |
+
Status string `json:"status"` // "up", "down", "pending"
|
| 11 |
+
Timeout uint `json:"timeout"`
|
| 12 |
+
Retries uint `json:"retries"`
|
| 13 |
+
RetryInterval uint `json:"retryInterval"`
|
| 14 |
+
IgnoreTLS bool `json:"ignoreTls"`
|
| 15 |
+
HttpMethod string `json:"httpMethod"`
|
| 16 |
+
HttpBody string `json:"httpBody"`
|
| 17 |
+
HttpHeaders string `json:"httpHeaders"`
|
| 18 |
+
AuthMethod string `json:"authMethod"`
|
| 19 |
+
AuthUsername string `json:"authUsername"`
|
| 20 |
+
AuthPassword string `json:"authPassword"`
|
| 21 |
+
Delay uint `json:"delay"` // Delay in seconds before check
|
| 22 |
+
}
|
models/user.go
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import "gorm.io/gorm"
|
| 4 |
+
|
| 5 |
+
type User struct {
|
| 6 |
+
gorm.Model
|
| 7 |
+
Username string `gorm:"unique"`
|
| 8 |
+
Password string
|
| 9 |
+
}
|
services/monitor.go
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"crypto/tls"
|
| 5 |
+
"log"
|
| 6 |
+
"net/http"
|
| 7 |
+
"strings"
|
| 8 |
+
"time"
|
| 9 |
+
"uptime/backend/database"
|
| 10 |
+
"uptime/backend/models"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
var DeletedMonitorIDs = make(chan uint, 100)
|
| 14 |
+
|
| 15 |
+
func StartMonitoring() {
|
| 16 |
+
log.Printf("[INFO] Monitoring service started at %v", time.Now())
|
| 17 |
+
ticker := time.NewTicker(10 * time.Second) // Check for new monitors every 10 seconds
|
| 18 |
+
monitorTimers := make(map[uint]*time.Timer)
|
| 19 |
+
|
| 20 |
+
for {
|
| 21 |
+
select {
|
| 22 |
+
case <-ticker.C:
|
| 23 |
+
var monitors []models.Monitor
|
| 24 |
+
database.DB.Find(&monitors)
|
| 25 |
+
|
| 26 |
+
for i := range monitors {
|
| 27 |
+
monitor := &monitors[i]
|
| 28 |
+
|
| 29 |
+
// Skip if timer already exists for this monitor
|
| 30 |
+
if _, exists := monitorTimers[monitor.ID]; exists {
|
| 31 |
+
continue
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Create individual timer for each monitor based on its interval
|
| 35 |
+
interval := time.Duration(monitor.Interval) * time.Second
|
| 36 |
+
log.Printf("[INFO] Creating timer for monitor ID=%d Name=%s Interval=%v", monitor.ID, monitor.Name, interval)
|
| 37 |
+
monitorTimers[monitor.ID] = time.AfterFunc(0, func() {
|
| 38 |
+
scheduleMonitorCheck(monitor, monitorTimers, interval)
|
| 39 |
+
})
|
| 40 |
+
}
|
| 41 |
+
case id := <-DeletedMonitorIDs:
|
| 42 |
+
if timer, exists := monitorTimers[id]; exists {
|
| 43 |
+
timer.Stop()
|
| 44 |
+
delete(monitorTimers, id)
|
| 45 |
+
log.Printf("[INFO] Stopped and removed timer for monitor ID=%d", id)
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func scheduleMonitorCheck(monitor *models.Monitor, timers map[uint]*time.Timer, interval time.Duration) {
|
| 52 |
+
// Add a delay before the check if specified
|
| 53 |
+
if monitor.Delay > 0 {
|
| 54 |
+
delay := time.Duration(monitor.Delay) * time.Second
|
| 55 |
+
log.Printf("[INFO] Monitor ID=%d Name=%s: Delaying check by %v", monitor.ID, monitor.Name, delay)
|
| 56 |
+
time.Sleep(delay)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
checkMonitorWithRetries(monitor)
|
| 60 |
+
|
| 61 |
+
// Schedule next check
|
| 62 |
+
timers[monitor.ID] = time.AfterFunc(interval, func() {
|
| 63 |
+
scheduleMonitorCheck(monitor, timers, interval)
|
| 64 |
+
})
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func checkMonitorWithRetries(monitor *models.Monitor) {
|
| 68 |
+
maxRetries := int(monitor.Retries)
|
| 69 |
+
retryInterval := time.Duration(monitor.RetryInterval) * time.Second
|
| 70 |
+
|
| 71 |
+
for attempt := 0; attempt <= maxRetries; attempt++ {
|
| 72 |
+
success := checkMonitor(monitor)
|
| 73 |
+
if success || attempt == maxRetries {
|
| 74 |
+
break
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if attempt < maxRetries {
|
| 78 |
+
log.Printf("[WARN] Monitor ID=%d Name=%s failed (attempt %d/%d), retrying in %v",
|
| 79 |
+
monitor.ID, monitor.Name, attempt+1, maxRetries+1, retryInterval)
|
| 80 |
+
time.Sleep(retryInterval)
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func checkMonitor(monitor *models.Monitor) bool {
|
| 86 |
+
client := &http.Client{
|
| 87 |
+
Timeout: time.Duration(monitor.Timeout) * time.Second,
|
| 88 |
+
Transport: &http.Transport{
|
| 89 |
+
TLSClientConfig: &tls.Config{InsecureSkipVerify: monitor.IgnoreTLS},
|
| 90 |
+
},
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
req, err := http.NewRequest(monitor.HttpMethod, monitor.URL, strings.NewReader(monitor.HttpBody))
|
| 94 |
+
if err != nil {
|
| 95 |
+
log.Printf("[ERROR] Monitor ID=%d Name=%s: Error creating request: %s", monitor.ID, monitor.Name, err.Error())
|
| 96 |
+
updateMonitorStatus(monitor, "down")
|
| 97 |
+
return false
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Add headers
|
| 101 |
+
// For simplicity, assuming headers are in "Key:Value\nKey2:Value2" format
|
| 102 |
+
if monitor.HttpHeaders != "" {
|
| 103 |
+
headers := strings.Split(monitor.HttpHeaders, "\n")
|
| 104 |
+
for _, header := range headers {
|
| 105 |
+
parts := strings.SplitN(header, ":", 2)
|
| 106 |
+
if len(parts) == 2 {
|
| 107 |
+
req.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Add basic auth
|
| 113 |
+
if monitor.AuthMethod == "basic" && monitor.AuthUsername != "" {
|
| 114 |
+
req.SetBasicAuth(monitor.AuthUsername, monitor.AuthPassword)
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
start := time.Now()
|
| 118 |
+
resp, err := client.Do(req)
|
| 119 |
+
latency := time.Since(start).Milliseconds()
|
| 120 |
+
|
| 121 |
+
if err != nil {
|
| 122 |
+
log.Printf("[ERROR] Monitor ID=%d Name=%s: HTTP request failed: %s", monitor.ID, monitor.Name, err.Error())
|
| 123 |
+
updateMonitorStatus(monitor, "down")
|
| 124 |
+
return false
|
| 125 |
+
}
|
| 126 |
+
defer resp.Body.Close()
|
| 127 |
+
|
| 128 |
+
// Store latency
|
| 129 |
+
latencyRecord := models.Latency{MonitorID: monitor.ID, Latency: uint(latency)}
|
| 130 |
+
database.DB.Create(&latencyRecord)
|
| 131 |
+
|
| 132 |
+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
| 133 |
+
log.Printf("[INFO] Monitor ID=%d Name=%s: Status UP (HTTP %d), Latency: %dms", monitor.ID, monitor.Name, resp.StatusCode, latency)
|
| 134 |
+
updateMonitorStatus(monitor, "up")
|
| 135 |
+
return true
|
| 136 |
+
} else {
|
| 137 |
+
log.Printf("[WARN] Monitor ID=%d Name=%s: Status DOWN (HTTP %d), Latency: %dms", monitor.ID, monitor.Name, resp.StatusCode, latency)
|
| 138 |
+
updateMonitorStatus(monitor, "down")
|
| 139 |
+
return false
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
func updateMonitorStatus(monitor *models.Monitor, status string) {
|
| 144 |
+
oldStatus := monitor.Status
|
| 145 |
+
monitor.Status = status
|
| 146 |
+
database.DB.Save(monitor)
|
| 147 |
+
log.Printf("[INFO] Monitor ID=%d Name=%s: Status changed from %s to %s", monitor.ID, monitor.Name, oldStatus, status)
|
| 148 |
+
}
|