Files
portal_page/handlers/home.go
T
kevin c16a8dfbc4 feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染
- 主页:Google 风格搜索框 + 导航卡片
- 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置
- 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1
- 安全:登录日志、修改密码、IP 自动封禁、IP 白名单
- 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计
- SQLite 存储(modernc.org/sqlite,纯 Go)
- 内存 Session + bcrypt 密码哈希
2026-05-28 13:54:07 +08:00

256 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}