This commit is contained in:
2026-05-28 14:43:13 +08:00
parent c16a8dfbc4
commit 957a594a0f
7 changed files with 353 additions and 64 deletions
+52 -35
View File
@@ -3,10 +3,12 @@ package main
import (
"html/template"
"log"
"net"
"os"
"path/filepath"
"strings"
"simple_portal/config"
"simple_portal/database"
"simple_portal/handlers"
"simple_portal/middleware"
@@ -15,8 +17,8 @@ import (
"github.com/gin-gonic/gin"
)
// loadTemplates loads HTML templates from templates/ directory recursively.
// Custom implementation because Go's ParseGlob has issues with directories on Windows.
// loadTemplates 加载 templates/ 目录下所有 HTML 模板
// 自定义实现,因为 Go ParseGlob Windows 下有路径问题
func loadTemplates() *template.Template {
funcMap := template.FuncMap{
"hasPrefix": strings.HasPrefix,
@@ -24,7 +26,6 @@ func loadTemplates() *template.Template {
"add": func(a, b int) int { return a + b },
}
t := template.New("").Funcs(funcMap)
// 收集所有 .html 模板文件路径
var files []string
filepath.Walk("templates", func(path string, info os.FileInfo, err error) error {
if err != nil {
@@ -38,7 +39,6 @@ func loadTemplates() *template.Template {
if len(files) == 0 {
log.Fatal("No template files found in templates/")
}
// 将 Windows 反斜杠路径转为正斜杠,避免模板名问题
for i, f := range files {
files[i] = filepath.ToSlash(f)
}
@@ -51,24 +51,30 @@ func loadTemplates() *template.Template {
}
func main() {
// Initialize database
// 加载配置文件(自动生成 + 补全缺失项)
if err := config.Load(); err != nil {
log.Fatalf("加载配置失败: %v", err)
}
log.Printf("配置加载成功,数据目录: %s,数据库: %s", config.Cfg.Data.Dir, config.Cfg.Database.Type)
// 初始化数据库
if err := database.InitDB(); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
log.Fatalf("初始化数据库失败: %v", err)
}
defer database.CloseDB()
// Create uploads directory
if err := os.MkdirAll(filepath.Join(".", "data", "uploads"), 0755); err != nil {
log.Fatalf("Failed to create uploads directory: %v", err)
// 创建上传目录
if err := os.MkdirAll(config.GetUploadDir(), 0755); err != nil {
log.Fatalf("创建上传目录失败: %v", err)
}
// Create session store
// 创建 session 存储
sessionStore := session.NewSessionStore()
// Create IP ban guard (in-memory fail counter)
// 创建 IP 封禁守护
ipBanGuard := middleware.NewIPBanGuard()
// Set Gin mode
// 设置 Gin 模式
ginMode := os.Getenv("GIN_MODE")
if ginMode == "" {
gin.SetMode(gin.DebugMode)
@@ -76,43 +82,43 @@ func main() {
r := gin.Default()
// Load HTML templates (custom loader for nested directories)
// 加载 HTML 模板
r.SetHTMLTemplate(loadTemplates())
// Serve static files
// 静态文件
r.Static("/static", "./static")
// Inject session store and IP ban guard into context for handlers
// 注入 session 和 IP 封禁守护
r.Use(func(c *gin.Context) {
c.Set("sessionStore", sessionStore)
c.Set("ipBanGuard", ipBanGuard)
c.Next()
})
// Public routes (home page and uploads — no IP restriction)
// 公开路由
r.GET("/", handlers.HomeHandler)
r.GET("/click/:id", handlers.CardClickHandler)
r.GET("/search", handlers.SearchHandler)
r.GET("/uploads/:filename", handlers.ServeUploadHandler)
// Admin routes with IP whitelist check applied to all /admin/* routes
// 后台路由(IP 白名单)
adminGroup := r.Group("/admin")
adminGroup.Use(middleware.IPWhitelistRequired(func(sessionID string) bool {
return sessionStore.Get(sessionID) != nil
}))
{
// Public admin routes (login — no auth required, but IP whitelist applies)
// 登录(无需认证,受 IP 白名单限制)
adminGroup.GET("/login", handlers.LoginGet)
adminGroup.POST("/login", handlers.LoginPost)
// Protected admin routes (auth required)
// 需要认证的后台路由
protected := adminGroup.Group("")
protected.Use(middleware.AuthRequired(sessionStore))
{
protected.POST("/logout", handlers.Logout)
protected.GET("/", handlers.AdminIndex)
// Cards management
// 卡片管理
protected.GET("/cards", handlers.CardsList)
protected.GET("/cards/new", handlers.CardCreateGet)
protected.POST("/cards", handlers.CardCreatePost)
@@ -123,39 +129,50 @@ func main() {
protected.POST("/cards/:id/move-up", handlers.CardMoveUp)
protected.POST("/cards/:id/move-down", handlers.CardMoveDown)
// Image upload
// 图片上传
protected.POST("/upload", handlers.UploadHandler)
// Settings
// 设置
protected.GET("/settings", handlers.SettingsGet)
protected.POST("/settings", handlers.SettingsPost)
// Security: login logs
// 安全:登录日志
protected.GET("/logs", handlers.LoginLogsGet)
protected.POST("/logs/unban/:id", handlers.UnbanIP)
// Security: change password
// 安全:修改密码
protected.GET("/password", handlers.ChangePasswordGet)
protected.POST("/password", handlers.ChangePasswordPost)
// Security: IP whitelist management
// 安全:IP 白名单
protected.GET("/ip-whitelist", handlers.IPWhitelistGet)
protected.POST("/ip-whitelist/add", handlers.IPWhitelistAdd)
protected.POST("/ip-whitelist/:id/delete", handlers.IPWhitelistDelete)
// Analytics: access logs
// 分析:访问日志
protected.GET("/access-logs", handlers.AccessLogsGet)
}
}
// Determine port
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Starting Portal server on :%s", port)
if err := r.Run(":" + port); err != nil {
log.Fatalf("Failed to start server: %v", err)
// 启动服务器
if config.Cfg.Server.Unix != "" {
// Unix socket 模式
listener, err := net.Listen("unix", config.Cfg.Server.Unix)
if err != nil {
log.Fatalf("监听 Unix socket 失败: %v", err)
}
// 设置 socket 文件权限,允许 nginx 等其他进程访问
os.Chmod(config.Cfg.Server.Unix, 0666)
log.Printf("启动 Portal 服务器,监听 Unix socket: %s", config.Cfg.Server.Unix)
if err := r.RunListener(listener); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
} else {
// TCP 模式
addr := config.Cfg.Server.Addr
log.Printf("启动 Portal 服务器,监听: %s", addr)
if err := r.Run(addr); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}
}