- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
256 lines
6.8 KiB
Go
256 lines
6.8 KiB
Go
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,
|
||
})
|
||
}
|