Files
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

259 lines
6.4 KiB
Go

package handlers
import (
"net/http"
"strconv"
"simple_portal/models"
"simple_portal/session"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
// LoginLogsGet 渲染登录日志页面。
func LoginLogsGet(c *gin.Context) {
username, _ := c.Get("username")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
pageSize := 20
logs, total, err := models.GetLoginLogs(page, pageSize)
if err != nil {
logs = []models.LoginLog{}
total = 0
}
// 获取活跃的封禁列表
bans, _ := models.GetAllActiveBans()
if bans == nil {
bans = []models.IPBan{}
}
totalPages := (total + pageSize - 1) / pageSize
if totalPages < 1 {
totalPages = 1
}
c.HTML(http.StatusOK, "admin/logs.html", gin.H{
"Title": "登录日志",
"Username": username,
"Logs": logs,
"Bans": bans,
"Page": page,
"TotalPages": totalPages,
"Total": total,
})
}
// UnbanIP 处理手动解封IP的请求。
func UnbanIP(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.Redirect(http.StatusFound, "/admin/logs")
return
}
_ = models.DeleteIPBan(id)
c.Redirect(http.StatusFound, "/admin/logs")
}
// ChangePasswordGet 渲染修改密码页面。
func ChangePasswordGet(c *gin.Context) {
username, _ := c.Get("username")
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "",
"Message": "",
})
}
// ChangePasswordPost 处理修改密码表单提交。
func ChangePasswordPost(c *gin.Context) {
username, _ := c.Get("username")
adminID, _ := c.Get("adminID")
oldPassword := c.PostForm("old_password")
newPassword := c.PostForm("new_password")
confirmPassword := c.PostForm("confirm_password")
// 验证输入
if oldPassword == "" || newPassword == "" || confirmPassword == "" {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "请填写所有字段",
"Message": "",
})
return
}
if len(newPassword) < 6 {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "新密码长度不能少于6位",
"Message": "",
})
return
}
if newPassword != confirmPassword {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "两次输入的新密码不一致",
"Message": "",
})
return
}
// 验证旧密码
admin, err := models.GetAdminByUsername(username.(string))
if err != nil || admin == nil {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "用户不存在",
"Message": "",
})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(oldPassword)); err != nil {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "旧密码不正确",
"Message": "",
})
return
}
// 生成新密码hash
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "密码加密失败",
"Message": "",
})
return
}
// 更新密码
if err := models.ChangePassword(adminID.(int), string(hashedPassword)); err != nil {
c.HTML(http.StatusOK, "admin/password.html", gin.H{
"Title": "修改密码",
"Username": username,
"Error": "密码修改失败: " + err.Error(),
"Message": "",
})
return
}
// 记录登录日志
ip := c.ClientIP()
userAgent := c.Request.UserAgent()
adminIDInt := adminID.(int)
_ = models.CreateLoginLog(&adminIDInt, username.(string), ip, userAgent, true)
// 清除当前session,强制重新登录
sessionID, _ := c.Cookie("session_id")
if sessionID != "" {
store := c.MustGet("sessionStore").(*session.SessionStore)
store.Delete(sessionID)
}
c.SetCookie("session_id", "", -1, "/", "", false, true)
c.Redirect(http.StatusFound, "/admin/login")
}
// IPWhitelistGet 渲染IP白名单管理页面。
func IPWhitelistGet(c *gin.Context) {
username, _ := c.Get("username")
list, err := models.GetAllWhitelist()
if err != nil {
list = []models.IPWhitelist{}
}
hasWhitelist := len(list) > 0
c.HTML(http.StatusOK, "admin/ip_whitelist.html", gin.H{
"Title": "IP白名单",
"Username": username,
"Whitelist": list,
"HasWhitelist": hasWhitelist,
"Error": "",
"Message": "",
})
}
// IPWhitelistAdd 处理添加IP白名单的请求。
// 当白名单从空变为非空时,自动将当前操作者的IP也加入白名单,防止锁定自己。
func IPWhitelistAdd(c *gin.Context) {
username, _ := c.Get("username")
ip := c.PostForm("ip")
comment := c.PostForm("comment")
if ip == "" {
list, _ := models.GetAllWhitelist()
c.HTML(http.StatusOK, "admin/ip_whitelist.html", gin.H{
"Title": "IP白名单",
"Username": username,
"Whitelist": list,
"HasWhitelist": len(list) > 0,
"Error": "IP地址不能为空",
"Message": "",
})
return
}
// 检查白名单是否之前为空(首次启用白名单时需自动加入当前操作者IP)
wasEmpty, _ := models.HasWhitelist()
if err := models.AddWhitelist(ip, comment); err != nil {
list, _ := models.GetAllWhitelist()
c.HTML(http.StatusOK, "admin/ip_whitelist.html", gin.H{
"Title": "IP白名单",
"Username": username,
"Whitelist": list,
"HasWhitelist": len(list) > 0,
"Error": "添加失败: " + err.Error(),
"Message": "",
})
return
}
// 首次启用白名单:自动把当前操作者IP也加入,防止锁定
if !wasEmpty {
c.Redirect(http.StatusFound, "/admin/ip-whitelist")
return
}
currentIP := c.ClientIP()
// 检查当前IP是否和刚添加的一样
isAlreadyAdded, _ := models.IsIPWhitelisted(currentIP)
if !isAlreadyAdded {
_ = models.AddWhitelist(currentIP, "自动添加(当前操作者)")
}
c.Redirect(http.StatusFound, "/admin/ip-whitelist")
}
// IPWhitelistDelete 处理删除IP白名单的请求。
func IPWhitelistDelete(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.Redirect(http.StatusFound, "/admin/ip-whitelist")
return
}
_ = models.DeleteWhitelist(id)
c.Redirect(http.StatusFound, "/admin/ip-whitelist")
}