diff --git a/handlers/my_articles.go b/handlers/my_articles.go new file mode 100644 index 0000000..d17e543 --- /dev/null +++ b/handlers/my_articles.go @@ -0,0 +1,169 @@ +package handlers + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "go_blog/models" +) + +// MyArticlesPage renders the logged-in user's article management page. +func MyArticlesPage(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + tr := getTr(c) + session := sessions.Default(c) + userID := session.Get("user_id") + + var articles []models.Article + db.Where("author_id = ?", userID).Order("created_at DESC").Find(&articles) + + data := DefaultData(c) + data["Title"] = tr["my_articles_title"] + data["Articles"] = articles + c.HTML(http.StatusOK, "my_articles", data) + } +} + +// MyArticleCreatePage renders the article creation form for regular users. +func MyArticleCreatePage(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + tr := getTr(c) + token := randomToken() + session := sessions.Default(c) + session.Set("pending_token", token) + session.Save() + + renderMyArticleForm(c, db, articleForm{ + Action: "/my/articles/new", + TitleText: tr["article_create_title"], + SessionToken: token, + }, "") + } +} + +// MyArticleCreate handles the POST request to create a new article for regular users. +func MyArticleCreate(db *gorm.DB) gin.HandlerFunc { + return ArticleCreate(db) // Reuse the same logic +} + +// MyArticleEditPage renders the article edit form for the logged-in user's own articles. +func MyArticleEditPage(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + tr := getTr(c) + id := c.Param("id") + session := sessions.Default(c) + userID := session.Get("user_id") + + var article models.Article + if err := db.First(&article, "id = ? AND author_id = ?", id, userID).Error; err != nil { + c.Redirect(http.StatusFound, "/my/articles") + return + } + + renderMyArticleForm(c, db, articleForm{ + Title: article.Title, + Slug: article.Slug, + Summary: article.Summary, + Content: article.Content, + Cover: article.Cover, + StatusStr: strconv.Itoa(article.Status), + IsTop: article.IsTop, + Action: "/my/articles/" + id + "/edit", + TitleText: tr["article_edit_title"], + ArticleID: article.ID, + }, "") + } +} + +// MyArticleUpdate handles the POST request to update the user's own article. +func MyArticleUpdate(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + tr := getTr(c) + id := c.Param("id") + session := sessions.Default(c) + userID := session.Get("user_id") + + var article models.Article + if err := db.First(&article, "id = ? AND author_id = ?", id, userID).Error; err != nil { + c.Redirect(http.StatusFound, "/my/articles") + return + } + + f := parseArticleForm(c) + f.Action = "/my/articles/" + id + "/edit" + f.TitleText = tr["article_edit_title"] + f.ArticleID = article.ID + + if f.Title == "" { + renderMyArticleForm(c, db, f, tr["article_title_required"]) + return + } + if f.Content == "" { + renderMyArticleForm(c, db, f, tr["article_content_required"]) + return + } + + if f.Slug == "" { + f.Slug = generateSlug(f.Title) + if f.Slug == "" { + f.Slug = fallbackSlug(article.ID) + } + } + + newStatus := statusFromForm(f.StatusStr) + + // Stamp the publish time the first time an article is published. + wasPublished := article.Status == models.ArticlePublished + var publishedAt *time.Time = article.PublishedAt + if newStatus == models.ArticlePublished && !wasPublished && publishedAt == nil { + now := time.Now() + publishedAt = &now + } + + updates := map[string]interface{}{ + "title": f.Title, + "slug": f.Slug, + "summary": f.Summary, + "content": f.Content, + "cover": f.Cover, + "status": newStatus, + "is_top": f.IsTop, + "published_at": publishedAt, + } + + if err := db.Model(&article).Updates(updates).Error; err != nil { + renderMyArticleForm(c, db, f, tr["article_error"]) + return + } + + c.Redirect(http.StatusFound, "/my/articles") + } +} + +// MyArticleDelete soft-deletes the user's own article. +func MyArticleDelete(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + session := sessions.Default(c) + userID := session.Get("user_id") + + db.Where("id = ? AND author_id = ?", id, userID).Delete(&models.Article{}) + c.Redirect(http.StatusFound, "/my/articles") + } +} + +// renderMyArticleForm renders the article form for regular users. +func renderMyArticleForm(c *gin.Context, db *gorm.DB, f articleForm, errMsg string) { + data := DefaultData(c) + data["Title"] = f.TitleText + if errMsg != "" { + data["Error"] = errMsg + } + applyFormToData(data, f) + c.HTML(http.StatusOK, "my_article_form", data) +} diff --git a/i18n/i18n.go b/i18n/i18n.go index e9d810e..d93ff5a 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -72,6 +72,7 @@ var translations = map[Lang]map[string]string{ // Profile dropdown & page "profile": "Profile", "admin_panel": "Admin Panel", + "my_articles": "My Articles", "profile_title": "Edit Profile", "profile_avatar": "Avatar", "profile_change_avatar": "Change Avatar", @@ -102,17 +103,28 @@ var translations = map[Lang]map[string]string{ // Article creation "article_create_title": "Create Article", + "article_field_title": "Title", "article_title": "Title", + "article_field_slug": "Slug", "article_slug": "Slug", "article_slug_hint": "Auto-generated from title if empty", + "article_slug_help": "Auto-generated from title if empty", + "article_field_summary": "Summary", "article_summary": "Summary", + "article_field_content": "Content", "article_content": "Content", + "article_field_cover": "Cover Image URL", "article_cover": "Cover Image URL", + "article_cover_help": "Optional cover image URL", + "article_field_status": "Status", "article_status": "Status", "article_draft": "Draft", "article_published": "Published", + "article_field_is_top": "Pin to top", "article_is_top": "Pin to top", "article_save_draft": "Save Draft", + "article_save": "Save", + "article_cancel": "Cancel", "article_publish": "Publish", "article_created": "Article created successfully.", "article_error": "Failed to create article.", @@ -135,6 +147,7 @@ var translations = map[Lang]map[string]string{ // Article management "article_list_title": "Articles", + "my_articles_title": "My Articles", "article_edit_title": "Edit Article", "article_manage": "Manage Articles", "article_edit": "Edit", @@ -241,6 +254,7 @@ var translations = map[Lang]map[string]string{ // Admin: comments "admin_comments": "Comments", + "comment_manage": "Manage Comments", "admin_comments_title": "Comments", "comment_status_pending": "Pending", "comment_status_approved": "Approved", @@ -368,6 +382,7 @@ var translations = map[Lang]map[string]string{ // 个人中心下拉菜单 & 页面 "profile": "个人信息", "admin_panel": "后台管理", + "my_articles": "我的文章", "profile_title": "编辑个人信息", "profile_avatar": "头像", "profile_change_avatar": "更换头像", @@ -398,17 +413,28 @@ var translations = map[Lang]map[string]string{ // 文章创建 "article_create_title": "新建文章", + "article_field_title": "标题", "article_title": "标题", + "article_field_slug": "Slug", "article_slug": "Slug", "article_slug_hint": "留空则从标题自动生成", + "article_slug_help": "留空则从标题自动生成", + "article_field_summary": "摘要", "article_summary": "摘要", + "article_field_content": "正文", "article_content": "正文", + "article_field_cover": "封面图 URL", "article_cover": "封面图 URL", + "article_cover_help": "可选的封面图片链接", + "article_field_status": "状态", "article_status": "状态", "article_draft": "草稿", "article_published": "已发布", + "article_field_is_top": "置顶", "article_is_top": "置顶", "article_save_draft": "保存草稿", + "article_save": "保存", + "article_cancel": "取消", "article_publish": "发布", "article_created": "文章创建成功。", "article_error": "创建文章失败。", @@ -431,6 +457,7 @@ var translations = map[Lang]map[string]string{ // 文章管理 "article_list_title": "文章管理", + "my_articles_title": "我的文章", "article_edit_title": "编辑文章", "article_manage": "文章管理", "article_edit": "编辑", @@ -537,6 +564,7 @@ var translations = map[Lang]map[string]string{ // 后台:评论 "admin_comments": "评论管理", + "comment_manage": "评论管理", "admin_comments_title": "评论管理", "comment_status_pending": "待审核", "comment_status_approved": "已通过", diff --git a/main.go b/main.go index 73565b9..45d7130 100644 --- a/main.go +++ b/main.go @@ -56,9 +56,9 @@ func main() { router.GET("/article/:slug", handlers.ArticleDetail(db)) router.POST("/article/:slug/comments", handlers.PostComment(db)) - // Protected admin routes. + // Protected admin routes (admin role only). admin := router.Group("/admin") - admin.Use(middleware.AuthRequired()) + admin.Use(middleware.AuthRequired(), middleware.AdminRequired(db)) { admin.GET("", handlers.AdminDashboard(db)) admin.GET("/articles", handlers.ArticleListPage(db)) @@ -68,10 +68,16 @@ func main() { admin.POST("/articles/:id/edit", handlers.ArticleUpdate(db)) admin.POST("/articles/:id/delete", handlers.ArticleDelete(db)) - admin.GET("/comments", handlers.CommentListPage(db)) - admin.POST("/comments/:id/approve", handlers.CommentApprove(db)) - admin.POST("/comments/:id/reject", handlers.CommentReject(db)) - admin.POST("/comments/:id/delete", handlers.CommentDelete(db)) + } + + // Protected admin comment management routes (admin role only). + comments := router.Group("/admin/comments") + comments.Use(middleware.AuthRequired(), middleware.AdminRequired(db)) + { + comments.GET("", handlers.CommentListPage(db)) + comments.POST("/:id/approve", handlers.CommentApprove(db)) + comments.POST("/:id/reject", handlers.CommentReject(db)) + comments.POST("/:id/delete", handlers.CommentDelete(db)) } // Protected admin user-management routes (admin role only). @@ -86,9 +92,9 @@ func main() { users.POST("/:id/delete", handlers.UserDelete(db)) } - // Protected article attachment routes (AJAX uploads / management). + // Protected article attachment routes (admin role only). attachments := router.Group("/admin/articles") - attachments.Use(middleware.AuthRequired()) + attachments.Use(middleware.AuthRequired(), middleware.AdminRequired(db)) { attachments.POST("/attachments", handlers.UploadAttachment(db, cfg.Path)) attachments.POST("/attachments/:id/delete", handlers.DeleteAttachment(db, cfg.Path)) @@ -97,7 +103,7 @@ func main() { // Protected admin settings routes (platform configuration). settings := router.Group("/admin/settings") - settings.Use(middleware.AuthRequired()) + settings.Use(middleware.AuthRequired(), middleware.AdminRequired(db)) { settings.GET("/site", handlers.SiteSettingsPage(db)) settings.POST("/site", handlers.SiteSettingsSave(db, cfg.Path)) @@ -118,6 +124,27 @@ func main() { profile.POST("/avatar", handlers.UploadAvatar(db, cfg.Path)) } + // Protected user article management routes (for non-admin users). + myArticles := router.Group("/my") + myArticles.Use(middleware.AuthRequired()) + { + myArticles.GET("/articles", handlers.MyArticlesPage(db)) + myArticles.GET("/articles/new", handlers.MyArticleCreatePage(db)) + myArticles.POST("/articles/new", handlers.MyArticleCreate(db)) + myArticles.GET("/articles/:id/edit", handlers.MyArticleEditPage(db)) + myArticles.POST("/articles/:id/edit", handlers.MyArticleUpdate(db)) + myArticles.POST("/articles/:id/delete", handlers.MyArticleDelete(db)) + } + + // Protected article attachment routes for user articles. + myAttachments := router.Group("/my/articles") + myAttachments.Use(middleware.AuthRequired()) + { + myAttachments.POST("/attachments", handlers.UploadAttachment(db, cfg.Path)) + myAttachments.POST("/attachments/:id/delete", handlers.DeleteAttachment(db, cfg.Path)) + myAttachments.GET("/:id/attachments", handlers.ListAttachments(db)) + } + // 9. Start the server. addr := fmt.Sprintf(":%s", cfg.Port) log.Printf("Go Blog starting on http://localhost%s", addr) diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index 8e7620b..751cdb8 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -55,11 +55,15 @@ class="inline-flex items-center gap-2 bg-gray-200 text-gray-700 px-6 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors"> {{index .Tr "dash_user_mgmt"}} - {{end}} + + {{index .Tr "comment_manage"}} + {{index .Tr "settings_nav"}} + {{end}} diff --git a/templates/layouts/base.html b/templates/layouts/base.html index 0f9fe44..adaa894 100644 --- a/templates/layouts/base.html +++ b/templates/layouts/base.html @@ -43,19 +43,14 @@ {{index .Tr "profile"}} + + {{index .Tr "my_articles"}} + {{if eq .Role "admin"}} +
{{index .Tr "admin_panel"}} - - {{index .Tr "article_manage"}} - - - {{index .Tr "admin_comments"}} - - - {{index .Tr "admin_users"}} - {{end}}