feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user