后台独立登录页面,并加入权限控制。
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// adminTemplatePath 后台单页模板(含 {{define}} 分块,按权限渲染)
|
||||
const adminTemplatePath = "./static/admin/index.html"
|
||||
|
||||
// AdminPageData 后台页面模板数据
|
||||
type AdminPageData struct {
|
||||
Menus []AdminMenuGroup // 当前角色可见菜单(登录接口同源)
|
||||
PageSet map[string]bool // 菜单中的页面标识集合,模板据此渲染对应页面框架
|
||||
DisplayName string // 侧边栏显示的用户名
|
||||
}
|
||||
|
||||
var (
|
||||
adminTmplOnce sync.Once
|
||||
adminTmpl *template.Template
|
||||
adminTmplErr error
|
||||
)
|
||||
|
||||
// getAdminTemplate 懒加载后台模板(仅解析一次)
|
||||
func getAdminTemplate() (*template.Template, error) {
|
||||
adminTmplOnce.Do(func() {
|
||||
adminTmpl, adminTmplErr = template.ParseFiles(adminTemplatePath)
|
||||
if adminTmplErr != nil {
|
||||
slog.Error("解析后台模板失败", "path", adminTemplatePath, "error", adminTmplErr)
|
||||
}
|
||||
})
|
||||
return adminTmpl, adminTmplErr
|
||||
}
|
||||
|
||||
// menuPageSet 从菜单提取页面标识集合
|
||||
func menuPageSet(menus []AdminMenuGroup) map[string]bool {
|
||||
set := make(map[string]bool)
|
||||
for _, g := range menus {
|
||||
for _, item := range g.Items {
|
||||
set[item.Key] = true
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// AdminLoginView 独立登录页
|
||||
func AdminLoginView(c *gin.Context) {
|
||||
c.File("./static/admin/login.html")
|
||||
}
|
||||
|
||||
// AdminView 后台管理页面:校验登录态后按角色用模板渲染菜单与页面框架
|
||||
func AdminView(c *gin.Context) {
|
||||
tokenString, err := c.Cookie("token")
|
||||
if err != nil || tokenString == "" {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
// 校验用户当前状态(禁用账号不允许进入后台)
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, claims.UserID).Error; err != nil || user.Status == 0 || !user.IsActive {
|
||||
c.Redirect(http.StatusFound, "/admin/login")
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, tmplErr := getAdminTemplate()
|
||||
if tmplErr != nil {
|
||||
c.String(http.StatusInternalServerError, "后台模板加载失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 菜单与登录接口保持同一来源,页面框架只渲染菜单内的页面
|
||||
menus := GetMenusByRole(user.Role)
|
||||
displayName := user.Nickname
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
data := AdminPageData{
|
||||
Menus: menus,
|
||||
PageSet: menuPageSet(menus),
|
||||
DisplayName: displayName,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.ExecuteTemplate(c.Writer, "admin_index", data); err != nil {
|
||||
slog.Error("渲染后台页面失败", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,50 @@ func JWTSecret() []byte {
|
||||
return jwtSecret
|
||||
}
|
||||
|
||||
// AdminMenuItem 后台菜单项
|
||||
type AdminMenuItem struct {
|
||||
Key string `json:"key"` // 页面标识,与页面 div 的 data-page 对应
|
||||
Name string `json:"name"` // 菜单显示名称
|
||||
}
|
||||
|
||||
// AdminMenuGroup 后台菜单分组(Group 为空表示顶级菜单)
|
||||
type AdminMenuGroup struct {
|
||||
Group string `json:"group"`
|
||||
Items []AdminMenuItem `json:"items"`
|
||||
}
|
||||
|
||||
// GetMenusByRole 根据角色返回后台菜单
|
||||
func GetMenusByRole(role string) []AdminMenuGroup {
|
||||
if role == "admin" {
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
}},
|
||||
{Group: "内容管理", Items: []AdminMenuItem{
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "categories", Name: "分类管理"},
|
||||
{Key: "tags", Name: "标签管理"},
|
||||
{Key: "pages", Name: "页面管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
{Group: "系统管理", Items: []AdminMenuItem{
|
||||
{Key: "users", Name: "用户管理"},
|
||||
{Key: "files", Name: "文件管理"},
|
||||
{Key: "themes", Name: "主题管理"},
|
||||
{Key: "settings", Name: "基础设置"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
// 普通用户:仪表盘 + 文章管理 + 评论管理
|
||||
return []AdminMenuGroup{
|
||||
{Group: "", Items: []AdminMenuItem{
|
||||
{Key: "dashboard", Name: "仪表盘"},
|
||||
{Key: "posts", Name: "文章管理"},
|
||||
{Key: "comments", Name: "评论管理"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
@@ -88,6 +132,9 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 同时写入 Cookie,供服务端渲染后台页面时识别登录态
|
||||
c.SetCookie("token", tokenString, 7*24*3600, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": tokenString,
|
||||
"user": gin.H{
|
||||
@@ -100,9 +147,23 @@ func Login(c *gin.Context) {
|
||||
"last_login_at": user.LastLoginAt,
|
||||
"login_count": user.LoginCount,
|
||||
},
|
||||
// 菜单由登录接口根据角色返回
|
||||
"menus": GetMenusByRole(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录(清除服务端 Cookie)
|
||||
func Logout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已退出登录"})
|
||||
}
|
||||
|
||||
// WebLogout 前台页面退出登录(清除 Cookie 后回首页)
|
||||
func WebLogout(c *gin.Context) {
|
||||
c.SetCookie("token", "", -1, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
func GetCurrentUser(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
|
||||
@@ -30,6 +30,14 @@ func GetComments(c *gin.Context) {
|
||||
return db.Select("id", "title", "slug")
|
||||
})
|
||||
|
||||
// 非管理员只能查看自己文章上的评论
|
||||
if !isAdmin(c) {
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
db = db.Where("post_id IN (?)",
|
||||
database.DB.Model(&models.Post{}).Select("id").Where("author_id = ?", userID))
|
||||
}
|
||||
}
|
||||
|
||||
if postID != "" {
|
||||
db = db.Where("post_id = ?", postID)
|
||||
}
|
||||
@@ -95,6 +103,21 @@ func CreateComment(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, gin.H{"data": comment})
|
||||
}
|
||||
|
||||
// canManageComment 检查当前用户是否有权管理该评论(管理员始终可以,普通用户只能管理自己文章上的评论)
|
||||
func canManageComment(c *gin.Context, comment *models.Comment) bool {
|
||||
if isAdmin(c) {
|
||||
return true
|
||||
}
|
||||
if userID, exists := c.Get("userID"); exists {
|
||||
var post models.Post
|
||||
if err := database.DB.Select("author_id").First(&post, comment.PostID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
return post.AuthorID == userID.(uint)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 审核评论
|
||||
func ApproveComment(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
@@ -105,6 +128,11 @@ func ApproveComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
comment.Status = "approved"
|
||||
if err := database.DB.Save(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "审核失败"})
|
||||
@@ -124,6 +152,11 @@ func MarkSpamComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
comment.Status = "spam"
|
||||
if err := database.DB.Save(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "操作失败"})
|
||||
@@ -143,6 +176,11 @@ func DeleteComment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canManageComment(c, &comment) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该评论"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&comment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除评论失败"})
|
||||
return
|
||||
|
||||
+39
-13
@@ -82,22 +82,23 @@ func GetPosts(c *gin.Context) {
|
||||
|
||||
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author")
|
||||
|
||||
// 前端只显示已发布的文章
|
||||
if !isAdmin(c) {
|
||||
// 管理员可查看全部;登录用户可看到自己的全部文章(含草稿);未登录只能看已发布
|
||||
if isAdmin(c) {
|
||||
if query.Status != "" {
|
||||
db = db.Where("status = ?", query.Status)
|
||||
}
|
||||
} else if userId, exists := c.Get("userID"); exists {
|
||||
// 登录用户:只看自己的,不限状态(后台管理需要看到草稿)
|
||||
db = db.Where("author_id = ?", userId)
|
||||
} else {
|
||||
// 未登录:只看已发布
|
||||
db = db.Where("status = ?", "published")
|
||||
} else if query.Status != "" {
|
||||
db = db.Where("status = ?", query.Status)
|
||||
}
|
||||
|
||||
if query.CategoryID > 0 {
|
||||
db = db.Where("category_id = ?", query.CategoryID)
|
||||
}
|
||||
|
||||
userId, exists := c.Get("userID")
|
||||
if exists {
|
||||
db = db.Where("author_id = ?", userId)
|
||||
}
|
||||
|
||||
if query.TagID > 0 {
|
||||
db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id").
|
||||
Where("post_tags.tag_id = ?", query.TagID)
|
||||
@@ -141,9 +142,13 @@ func GetPost(c *gin.Context) {
|
||||
query = query.Where("slug = ?", id)
|
||||
}
|
||||
|
||||
// 非管理员只能查看已发布文章
|
||||
// 非管理员只能查看已发布文章,但可以查看自己的草稿(编辑用)
|
||||
if !isAdmin(c) {
|
||||
query = query.Where("status = ?", "published")
|
||||
if userId, exists := c.Get("userID"); exists {
|
||||
query = query.Where("status = ? OR author_id = ?", "published", userId)
|
||||
} else {
|
||||
query = query.Where("status = ?", "published")
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.First(&post).Error; err != nil {
|
||||
@@ -176,7 +181,7 @@ func CreatePost(c *gin.Context) {
|
||||
AuthorID: userID.(uint),
|
||||
CategoryID: req.CategoryID,
|
||||
Status: req.Status,
|
||||
IsTop: req.IsTop,
|
||||
IsTop: req.IsTop && isAdmin(c), // 非管理员不允许置顶
|
||||
}
|
||||
|
||||
if req.Status == "published" {
|
||||
@@ -216,6 +221,15 @@ func UpdatePost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非管理员只能编辑自己的文章
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
if post.AuthorID != userID.(uint) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权编辑他人的文章"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var req UpdatePostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -251,7 +265,10 @@ func UpdatePost(c *gin.Context) {
|
||||
now := time.Now()
|
||||
updates["published_at"] = &now
|
||||
}
|
||||
updates["is_top"] = req.IsTop
|
||||
// 非管理员不允许置顶
|
||||
if isAdmin(c) {
|
||||
updates["is_top"] = req.IsTop
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if len(req.Tags) > 0 {
|
||||
@@ -288,6 +305,15 @@ func DeletePost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 非管理员只能删除自己的文章
|
||||
if !isAdmin(c) {
|
||||
userID, _ := c.Get("userID")
|
||||
if post.AuthorID != userID.(uint) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权删除他人的文章"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&post).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文章失败"})
|
||||
return
|
||||
|
||||
@@ -25,6 +25,16 @@ func loadSidebarData() (categories []models.Category, tags []models.Tag, postCou
|
||||
return
|
||||
}
|
||||
|
||||
// loginInfo 从上下文提取登录态信息(供侧边栏系统菜单区分已登录/未登录)
|
||||
func loginInfo(c *gin.Context) (isLoggedIn bool, loginName string) {
|
||||
if _, ok := c.Get("userID"); !ok {
|
||||
return false, ""
|
||||
}
|
||||
name, _ := c.Get("username")
|
||||
loginName, _ = name.(string)
|
||||
return true, loginName
|
||||
}
|
||||
|
||||
// 首页
|
||||
type IndexData struct {
|
||||
Title string
|
||||
@@ -42,6 +52,9 @@ type IndexData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func IndexView(c *gin.Context) {
|
||||
@@ -129,6 +142,7 @@ func IndexView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
}
|
||||
@@ -156,6 +170,9 @@ type PostDetailData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func PostView(c *gin.Context) {
|
||||
@@ -280,6 +297,7 @@ func PostView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
// 只有当查询成功时才赋值
|
||||
if prevErr == nil {
|
||||
@@ -307,6 +325,9 @@ type PageDetailData struct {
|
||||
PostCount int64
|
||||
CategoryCount int64
|
||||
TagCount int64
|
||||
// 登录态(侧边栏系统菜单用)
|
||||
IsLoggedIn bool
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func PageView(c *gin.Context) {
|
||||
@@ -359,6 +380,7 @@ func PageView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "page", data)
|
||||
}
|
||||
@@ -464,6 +486,7 @@ func SearchView(c *gin.Context) {
|
||||
CategoryCount: categoryCount,
|
||||
TagCount: tagCount,
|
||||
}
|
||||
data.IsLoggedIn, data.LoginName = loginInfo(c)
|
||||
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user