package handlers import ( "fmt" "net/http" "strconv" "time" "simple_portal/middleware" "simple_portal/models" "simple_portal/session" "github.com/gin-gonic/gin" ) // HomeHandler renders the portal home page with enabled cards and search engine. // 同时记录主页访问日志。 func HomeHandler(c *gin.Context) { cards, err := models.GetEnabledCards() if err != nil { c.HTML(http.StatusInternalServerError, "home.html", gin.H{"Error": "Failed to load cards"}) return } searchEngine, err := models.GetSetting(models.SettingKeySearchEngine) if err != nil { searchEngine = models.SearchEngineGoogle } if searchEngine == "" { searchEngine = models.SearchEngineGoogle } // Fetch homepage configuration siteTitle, _ := models.GetSetting(models.SettingKeyHomepageTitle) if siteTitle == "" { siteTitle = models.DefaultHomepageTitle } siteSubtitle, _ := models.GetSetting(models.SettingKeyHomepageSubtitle) backgroundImage, _ := models.GetSetting(models.SettingKeyHomepageBackground) // 记录主页访问日志(异步,不影响页面渲染) ip := c.ClientIP() ua := c.Request.UserAgent() referer := c.Request.Referer() go func() { _ = models.CreateAccessLog(ip, ua, models.ActionTypeVisit, "", referer) }() c.HTML(http.StatusOK, "home.html", gin.H{ "Title": siteTitle, "Cards": cards, "SearchEngine": searchEngine, "SiteTitle": siteTitle, "SiteSubtitle": siteSubtitle, "BackgroundImage": backgroundImage, }) } // CardClickHandler 处理卡片点击,记录访问日志后重定向到目标URL。 func CardClickHandler(c *gin.Context) { idStr := c.Param("id") id, err := strconv.Atoi(idStr) if err != nil { c.Redirect(http.StatusFound, "/") return } card, err := models.GetCardByID(id) if err != nil || card == nil { c.Redirect(http.StatusFound, "/") return } // 记录卡片点击日志 ip := c.ClientIP() ua := c.Request.UserAgent() referer := c.Request.Referer() cardTitle := card.Title go func() { _ = models.CreateAccessLog(ip, ua, models.ActionTypeClick, cardTitle, referer) }() c.Redirect(http.StatusFound, card.URL) } // SearchHandler 处理搜索请求,记录搜索日志后重定向到搜索引擎。 func SearchHandler(c *gin.Context) { query := c.Query("q") if query == "" { c.Redirect(http.StatusFound, "/") return } searchEngine, err := models.GetSetting(models.SettingKeySearchEngine) if err != nil || searchEngine == "" { searchEngine = models.SearchEngineGoogle } // 记录搜索日志 ip := c.ClientIP() ua := c.Request.UserAgent() referer := c.Request.Referer() go func() { _ = models.CreateAccessLog(ip, ua, models.ActionTypeSearch, query, referer) }() // 重定向到搜索引擎 targetURL := fmt.Sprintf(searchEngine, query) c.Redirect(http.StatusFound, targetURL) } // LoginGet renders the admin login page. func LoginGet(c *gin.Context) { // If already logged in, redirect to admin dashboard if sessionID, err := c.Cookie("session_id"); err == nil && sessionID != "" { store := c.MustGet("sessionStore").(*session.SessionStore) if store.Get(sessionID) != nil { c.Redirect(http.StatusFound, "/admin") return } } c.HTML(http.StatusOK, "admin/login.html", gin.H{ "Title": "登录", "Error": "", }) } // LoginPost handles the login form submission. // It includes login logging, IP ban checking, and automatic IP banning on repeated failures. func LoginPost(c *gin.Context) { username := c.PostForm("username") password := c.PostForm("password") clientIP := c.ClientIP() userAgent := c.Request.UserAgent() if username == "" || password == "" { _ = models.CreateLoginLog(nil, username, clientIP, userAgent, false) c.HTML(http.StatusOK, "admin/login.html", gin.H{ "Title": "登录", "Error": "请输入用户名和密码", }) return } // Check if IP is whitelisted — whitelist IPs bypass all ban checks isWhitelisted, _ := models.IsIPWhitelisted(clientIP) // Check if IP is currently banned (skip for whitelisted IPs) if !isWhitelisted { banned, ban, _ := models.IsIPBanned(clientIP) if banned && ban != nil { _ = models.CreateLoginLog(nil, username, clientIP, userAgent, false) c.HTML(http.StatusOK, "admin/login.html", gin.H{ "Title": "登录", "Error": fmt.Sprintf("您的IP已被封禁,解封时间:%s", ban.BannedUntil.Format("2006-01-02 15:04:05")), }) return } } matched, admin, err := models.VerifyPassword(username, password) if err != nil { _ = models.CreateLoginLog(nil, username, clientIP, userAgent, false) c.HTML(http.StatusOK, "admin/login.html", gin.H{ "Title": "登录", "Error": "登录失败,请重试", }) return } if !matched || admin == nil { _ = models.CreateLoginLog(nil, username, clientIP, userAgent, false) // Record failure in IPBanGuard (skip for whitelisted IPs) if !isWhitelisted { guard := c.MustGet("ipBanGuard").(*middleware.IPBanGuard) if guard.RecordFail(clientIP) { // Auto-ban this IP for 30 minutes _ = models.CreateIPBan(clientIP, "登录失败次数过多(自动封禁)", 5, time.Now().Add(30*time.Minute)) } } c.HTML(http.StatusOK, "admin/login.html", gin.H{ "Title": "登录", "Error": "用户名或密码错误", }) return } // Successful login adminID := admin.ID _ = models.CreateLoginLog(&adminID, username, clientIP, userAgent, true) // Reset fail counter for this IP on successful login if !isWhitelisted { guard := c.MustGet("ipBanGuard").(*middleware.IPBanGuard) guard.ResetFail(clientIP) } // Create session store := c.MustGet("sessionStore").(*session.SessionStore) sessionID := store.Create(admin.ID, admin.Username) // Set cookie c.SetCookie("session_id", sessionID, 86400, "/", "", false, true) c.Redirect(http.StatusFound, "/admin") } // Logout handles logging out the current admin. func Logout(c *gin.Context) { sessionID, _ := c.Cookie("session_id") if sessionID != "" { store := c.MustGet("sessionStore").(*session.SessionStore) store.Delete(sessionID) } // Clear cookie c.SetCookie("session_id", "", -1, "/", "", false, true) c.Redirect(http.StatusFound, "/admin/login") } // AdminIndex renders the admin dashboard. // 包含访问统计数据和实时流量信息。 func AdminIndex(c *gin.Context) { username, _ := c.Get("username") // 获取访问统计 stats, err := models.GetAccessLogStats() if err != nil { stats = &models.AccessLogStats{} } // 获取最近访问记录(20条) recentLogs, err := models.GetRecentAccessLogs(20) if err != nil { recentLogs = []models.AccessLog{} } // 获取IP统计(Top 10) ipStats, err := models.GetAccessLogStatsByIP(10) if err != nil { ipStats = nil } c.HTML(http.StatusOK, "admin/index.html", gin.H{ "Title": "管理后台", "Username": username, "Stats": stats, "RecentLogs": recentLogs, "IPStats": ipStats, }) }