feat: 文章详情页编辑按钮按角色放开——作者可编辑自己的文章
- renderArticleDetail 计算 CanEdit/EditURL:管理员(/admin/articles/:id/edit) 或登录用户且为文章作者(/my/articles/:id/edit)可见 - templates/pages/article.html 改用 .CanEdit/.EditURL 渲染 - my 编辑页/接口本身有 author_id 所有权约束,暴露链接无越权风险 - 新增 TestArticleDetailEditButton:作者/管理员可见、他人/匿名不可见
This commit is contained in:
4 files changed
+88
-2
No files matched your search
@@ -0,0 +1,68 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go_blog/models"
|
||||
)
|
||||
|
||||
// TestArticleDetailEditButton 验证文章页编辑按钮的可见性:
|
||||
// - 文章作者(普通用户)可见,链接指向 /my/articles/:id/edit
|
||||
// - 管理员对所有文章可见,链接指向 /admin/articles/:id/edit
|
||||
// - 其他登录用户与未登录访客不可见
|
||||
func TestArticleDetailEditButton(t *testing.T) {
|
||||
e := newSecurityTestEnv(t)
|
||||
|
||||
var aliceArt models.Article
|
||||
if err := e.db.Where("slug = ?", "alice-post").First(&aliceArt).Error; err != nil {
|
||||
t.Fatalf("alice article not found: %v", err)
|
||||
}
|
||||
id := strconv.FormatUint(uint64(aliceArt.ID), 10)
|
||||
myEdit := "/my/articles/" + id + "/edit"
|
||||
adminEdit := "/admin/articles/" + id + "/edit"
|
||||
|
||||
// 未登录访客:两种链接都不得出现。
|
||||
w := e.do(http.MethodGet, "/article/alice-post", "", nil, "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("anonymous GET article: status = %d", w.Code)
|
||||
}
|
||||
if strings.Contains(w.Body.String(), myEdit) || strings.Contains(w.Body.String(), adminEdit) {
|
||||
t.Fatal("anonymous viewer must not see any edit button")
|
||||
}
|
||||
|
||||
// 文章作者:看到 /my/articles/:id/edit。
|
||||
alice := e.login(t, "alice")
|
||||
w = e.do(http.MethodGet, "/article/alice-post", alice, nil, "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("author GET article: status = %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), myEdit) {
|
||||
t.Fatal("author should see their own edit button")
|
||||
}
|
||||
if strings.Contains(w.Body.String(), adminEdit) {
|
||||
t.Fatal("author must not see the admin edit button")
|
||||
}
|
||||
|
||||
// 其他作者:不可见。
|
||||
bob := e.login(t, "bob")
|
||||
w = e.do(http.MethodGet, "/article/alice-post", bob, nil, "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("other author GET article: status = %d", w.Code)
|
||||
}
|
||||
if strings.Contains(w.Body.String(), myEdit) || strings.Contains(w.Body.String(), adminEdit) {
|
||||
t.Fatal("other author must not see the edit button")
|
||||
}
|
||||
|
||||
// 管理员:看到 /admin/articles/:id/edit。
|
||||
admin := e.login(t, "admin")
|
||||
w = e.do(http.MethodGet, "/article/alice-post", admin, nil, "")
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("admin GET article: status = %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), adminEdit) {
|
||||
t.Fatal("admin should see the admin edit button")
|
||||
}
|
||||
}
|
||||
@@ -248,6 +248,23 @@ func renderArticleDetail(c *gin.Context, db *gorm.DB, article *models.Article, f
|
||||
data["CommentError"] = formErr
|
||||
data["CommentNotice"] = notice
|
||||
data["MaxCommentLength"] = MaxCommentLength
|
||||
|
||||
// 文章页编辑按钮:管理员可编辑全部文章;登录用户仅可编辑自己的文章
|
||||
// (普通作者跳转 /my/articles/:id/edit,编辑页/接口均有 author_id 所有权约束)。
|
||||
canEdit := false
|
||||
editURL := ""
|
||||
uid := userIDFromSession(c)
|
||||
role, _ := c.Get("role")
|
||||
if r, _ := role.(string); r == models.RoleAdmin {
|
||||
canEdit = true
|
||||
editURL = fmt.Sprintf("/admin/articles/%d/edit", article.ID)
|
||||
} else if uid != 0 && uid == article.AuthorID {
|
||||
canEdit = true
|
||||
editURL = fmt.Sprintf("/my/articles/%d/edit", article.ID)
|
||||
}
|
||||
data["CanEdit"] = canEdit
|
||||
data["EditURL"] = editURL
|
||||
|
||||
c.HTML(http.StatusOK, "article", data)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ func newSecurityTestEnv(t *testing.T) *securityTestEnv {
|
||||
r.GET("/login", LoginPage())
|
||||
r.GET("/register", RegisterPage(db))
|
||||
r.GET("/rss", RSSFeed(db))
|
||||
r.GET("/article/:slug", ArticleDetail(db))
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<a href="/" class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-blue-600 transition-colors">
|
||||
← {{index .Tr "article_back_home"}}
|
||||
</a>
|
||||
{{if eq .Role "admin"}}
|
||||
<a href="/admin/articles/{{.Article.ID}}/edit"
|
||||
{{if .CanEdit}}
|
||||
<a href="{{.EditURL}}"
|
||||
class="inline-flex items-center gap-1 text-sm px-3 py-1.5 rounded-md border border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100 hover:border-blue-300 transition-colors"
|
||||
title="{{index .Tr "article_edit"}}">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
|
||||
Reference in New Issue
Block a user