2
0

fix(blog): improve comments and attachment access control
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m15s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 4m35s
Build and Release / Lint (push) Successful in 5m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m8s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m5s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 6m58s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m42s
Build and Release / Build Binary (linux/arm64) (push) Successful in 8m10s

Shows existing comments as read-only when commenting is disabled on a post. Fixes attachment access control for blog featured images by checking repo-level read permissions when attachment isn't linked to issue/release. Updates README with detailed SMTP configuration examples and provider table. Fixes explore page navigation when blogs are in top nav.
This commit is contained in:
2026-02-02 10:12:39 -05:00
parent e89647bfab
commit e55529992c
8 changed files with 118 additions and 56 deletions

View File

@@ -538,19 +538,33 @@ Configure through the admin dashboard with your identity provider's metadata.
### Email/SMTP Setup
Configure email notifications in `app.ini`:
Configure email notifications in `app.ini`. Email is required for account registration, password resets, notification delivery, and **blog guest comment verification** (anonymous commenters receive a 6-digit code via email to verify their identity).
```ini
[mailer]
ENABLED = true
FROM = noreply@your-instance.com
PROTOCOL = smtp
PROTOCOL = smtp+starttls
SMTP_ADDR = smtp.gmail.com
SMTP_PORT = 587
USER = your-email@gmail.com
PASSWD = your-app-password
```
**Supported protocols:** `smtp+starttls` (recommended), `smtps`, `smtp`, `sendmail`
**Common provider examples:**
| Provider | SMTP_ADDR | SMTP_PORT | PROTOCOL |
|----------|-----------|-----------|----------|
| Gmail | smtp.gmail.com | 587 | smtp+starttls |
| Office 365 | smtp.office365.com | 587 | smtp+starttls |
| Amazon SES | email-smtp.us-east-1.amazonaws.com | 587 | smtp+starttls |
| Mailgun | smtp.mailgun.org | 587 | smtp+starttls |
| SendGrid | smtp.sendgrid.net | 587 | smtp+starttls |
> **Note:** Gmail requires an [App Password](https://support.google.com/accounts/answer/185833) (not your regular password) when 2FA is enabled. Amazon SES requires SMTP credentials generated from the SES console.
### Unsplash Integration
Enable Unsplash image search for repository social card backgrounds (Media Kit). This allows repository admins to search and select high-quality background images directly from Unsplash.

View File

@@ -2026,6 +2026,7 @@
"repo.blog.reactions.admin_hint": "Thumbs down counts are only visible to repo admins.",
"repo.blog.comments": "Comments",
"repo.blog.comments.empty": "No comments yet. Be the first to share your thoughts!",
"repo.blog.comments.disabled": "Comments have been disabled for this post.",
"repo.blog.comment.posted": "Comment posted successfully.",
"repo.blog.comment.deleted": "Comment deleted.",
"repo.blog.comment.delete_confirm": "Are you sure you want to delete this comment?",

View File

@@ -38,8 +38,13 @@ func Blogs(ctx *context.Context) {
ctx.Data["PackagesPageIsEnabled"] = setting.Service.Explore.EnablePackagesPage || setting.Config().Theme.EnableExplorePackages.Value(ctx)
ctx.Data["BlogsPageIsEnabled"] = true
ctx.Data["Title"] = ctx.Tr("explore.blogs")
ctx.Data["PageIsExplore"] = true
ctx.Data["PageIsExploreBlogs"] = true
blogsInTopNav := setting.Config().Theme.BlogsInTopNav.Value(ctx)
ctx.Data["BlogsInTopNav"] = blogsInTopNav
if !blogsInTopNav {
ctx.Data["PageIsExplore"] = true
}
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
page := max(ctx.FormInt("page"), 1)
@@ -199,29 +204,27 @@ func StandaloneBlogView(ctx *context.Context) {
userReaction, _ := blog_model.GetUserBlogReaction(ctx, post.ID, userID, guestIP)
ctx.Data["UserReaction"] = userReaction
// Load comments if allowed
if post.AllowComments {
comments, err := blog_model.GetBlogCommentsByPostID(ctx, post.ID)
if err != nil {
ctx.ServerError("GetBlogCommentsByPostID", err)
return
// Always load comments (even when disabled, to show existing ones as read-only)
comments, err := blog_model.GetBlogCommentsByPostID(ctx, post.ID)
if err != nil {
ctx.ServerError("GetBlogCommentsByPostID", err)
return
}
ctx.Data["BlogComments"] = comments
commentCount, _ := blog_model.CountBlogComments(ctx, post.ID)
ctx.Data["CommentCount"] = commentCount
// Load comment reaction counts and user reactions
commentIDs := collectCommentIDs(comments)
if len(commentIDs) > 0 {
commentReactionCounts, err := blog_model.GetBlogCommentReactionCountsBatch(ctx, commentIDs)
if err == nil {
ctx.Data["CommentReactionCounts"] = commentReactionCounts
}
ctx.Data["BlogComments"] = comments
commentCount, _ := blog_model.CountBlogComments(ctx, post.ID)
ctx.Data["CommentCount"] = commentCount
// Load comment reaction counts and user reactions
commentIDs := collectCommentIDs(comments)
if len(commentIDs) > 0 {
commentReactionCounts, err := blog_model.GetBlogCommentReactionCountsBatch(ctx, commentIDs)
if err == nil {
ctx.Data["CommentReactionCounts"] = commentReactionCounts
}
userCommentReactions, err := blog_model.GetUserBlogCommentReactionsBatch(ctx, commentIDs, userID, guestIP)
if err == nil {
ctx.Data["UserCommentReactions"] = userCommentReactions
}
userCommentReactions, err := blog_model.GetUserBlogCommentReactionsBatch(ctx, commentIDs, userID, guestIP)
if err == nil {
ctx.Data["UserCommentReactions"] = userCommentReactions
}
}

View File

@@ -9,6 +9,7 @@ import (
access_model "code.gitcaddy.com/server/v3/models/perm/access"
repo_model "code.gitcaddy.com/server/v3/models/repo"
"code.gitcaddy.com/server/v3/models/unit"
"code.gitcaddy.com/server/v3/modules/httpcache"
"code.gitcaddy.com/server/v3/modules/log"
"code.gitcaddy.com/server/v3/modules/setting"
@@ -106,8 +107,25 @@ func ServeAttachment(ctx *context.Context, uuid string) {
return
}
if repository == nil { // If not linked
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
if repository == nil { // If not linked via issue/release
if attach.RepoID > 0 {
// Attachment belongs to a repo (e.g. blog featured image) but isn't linked to an issue/release.
// Fall back to checking repo-level read access.
repository, err = repo_model.GetRepositoryByID(ctx, attach.RepoID)
if err != nil {
ctx.HTTPError(http.StatusNotFound)
return
}
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
return
}
if !perm.CanRead(unit.TypeCode) {
ctx.HTTPError(http.StatusNotFound)
return
}
} else if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
ctx.HTTPError(http.StatusNotFound)
return
}

View File

@@ -219,29 +219,27 @@ func BlogView(ctx *context.Context) {
userReaction, _ := blog_model.GetUserBlogReaction(ctx, post.ID, userID, guestIP)
ctx.Data["UserReaction"] = userReaction
// Load comments if allowed
if post.AllowComments {
comments, err := blog_model.GetBlogCommentsByPostID(ctx, post.ID)
if err != nil {
ctx.ServerError("GetBlogCommentsByPostID", err)
return
// Always load comments (even when disabled, to show existing ones as read-only)
comments, err := blog_model.GetBlogCommentsByPostID(ctx, post.ID)
if err != nil {
ctx.ServerError("GetBlogCommentsByPostID", err)
return
}
ctx.Data["BlogComments"] = comments
commentCount, _ := blog_model.CountBlogComments(ctx, post.ID)
ctx.Data["CommentCount"] = commentCount
// Load comment reaction counts and user reactions
commentIDs := collectCommentIDs(comments)
if len(commentIDs) > 0 {
commentReactionCounts, err := blog_model.GetBlogCommentReactionCountsBatch(ctx, commentIDs)
if err == nil {
ctx.Data["CommentReactionCounts"] = commentReactionCounts
}
ctx.Data["BlogComments"] = comments
commentCount, _ := blog_model.CountBlogComments(ctx, post.ID)
ctx.Data["CommentCount"] = commentCount
// Load comment reaction counts and user reactions
commentIDs := collectCommentIDs(comments)
if len(commentIDs) > 0 {
commentReactionCounts, err := blog_model.GetBlogCommentReactionCountsBatch(ctx, commentIDs)
if err == nil {
ctx.Data["CommentReactionCounts"] = commentReactionCounts
}
userCommentReactions, err := blog_model.GetUserBlogCommentReactionsBatch(ctx, commentIDs, userID, guestIP)
if err == nil {
ctx.Data["UserCommentReactions"] = userCommentReactions
}
userCommentReactions, err := blog_model.GetUserBlogCommentReactionsBatch(ctx, commentIDs, userID, guestIP)
if err == nil {
ctx.Data["UserCommentReactions"] = userCommentReactions
}
}

View File

@@ -53,7 +53,7 @@
{{if .BlogTags}}
<div class="blog-view-tags">
{{range .BlogTags}}
<span class="ui small label">{{.}}</span>
<a class="ui small label" href="{{AppSubUrl}}/blogs?tag={{.}}">{{.}}</a>
{{end}}
</div>
{{end}}
@@ -97,7 +97,7 @@
</div>
<!-- Comments Section -->
{{if .BlogPost.AllowComments}}
{{if or .BlogPost.AllowComments .BlogComments}}
<div class="blog-comments" id="blog-comments">
<h3 class="blog-comments-header">
{{svg "octicon-comment" 20}}
@@ -105,6 +105,12 @@
{{if .CommentCount}}<span class="ui small label">{{.CommentCount}}</span>{{end}}
</h3>
{{if not .BlogPost.AllowComments}}
<div class="ui message info">
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.blog.comments.disabled"}}
</div>
{{end}}
{{if .BlogComments}}
<div class="blog-comments-list">
{{range .BlogComments}}
@@ -134,9 +140,11 @@
<span class="comment-dislike-count" data-comment-id="{{.ID}}">{{if index $.CommentReactionCounts .ID}}{{(index $.CommentReactionCounts .ID).Dislikes}}{{else}}0{{end}}</span>
</button>
</div>
{{if $.BlogPost.AllowComments}}
<button class="blog-reply-btn" data-comment-id="{{.ID}}" type="button">
{{svg "octicon-reply" 14}} {{ctx.Locale.Tr "repo.blog.comment.reply"}}
</button>
{{end}}
{{if or $.IsWriter (and $.IsSigned (eq .UserID $.SignedUserID))}}
<form method="post" action="{{$.RepoLink}}/blog/{{$.BlogPost.ID}}/comment/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}}
@@ -192,6 +200,7 @@
</div>
{{end}}
{{if $.BlogPost.AllowComments}}
<!-- Inline reply form (hidden by default) -->
<div class="blog-reply-form tw-hidden" id="reply-form-{{.ID}}">
<form method="post" action="{{$.RepoLink}}/blog/{{$.BlogPost.ID}}/comment">
@@ -205,13 +214,17 @@
</div>
</form>
</div>
{{end}}
</div>
{{end}}
</div>
{{else}}
<p class="blog-comments-empty">{{ctx.Locale.Tr "repo.blog.comments.empty"}}</p>
{{if .BlogPost.AllowComments}}
<p class="blog-comments-empty">{{ctx.Locale.Tr "repo.blog.comments.empty"}}</p>
{{end}}
{{end}}
{{if .BlogPost.AllowComments}}
<!-- New comment form -->
<div class="blog-comment-form-wrapper">
{{if .IsSigned}}
@@ -261,6 +274,7 @@
{{end}}
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>

View File

@@ -1,6 +1,6 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content explore blogs">
{{template "explore/navbar" .}}
{{if not .BlogsInTopNav}}{{template "explore/navbar" .}}{{end}}
<div class="ui container">
{{if .FeaturedPost}}
<div class="blog-featured">

View File

@@ -44,7 +44,7 @@
{{if .BlogTags}}
<div class="blog-view-tags">
{{range .BlogTags}}
<span class="ui small label">{{.}}</span>
<a class="ui small label" href="{{AppSubUrl}}/blogs?tag={{.}}">{{.}}</a>
{{end}}
</div>
{{end}}
@@ -88,7 +88,7 @@
</div>
<!-- Comments Section -->
{{if .BlogPost.AllowComments}}
{{if or .BlogPost.AllowComments .BlogComments}}
<div class="blog-comments" id="blog-comments">
<h3 class="blog-comments-header">
{{svg "octicon-comment" 20}}
@@ -96,6 +96,12 @@
{{if .CommentCount}}<span class="ui small label">{{.CommentCount}}</span>{{end}}
</h3>
{{if not .BlogPost.AllowComments}}
<div class="ui message info">
{{svg "octicon-lock" 16}} {{ctx.Locale.Tr "repo.blog.comments.disabled"}}
</div>
{{end}}
{{if .BlogComments}}
<div class="blog-comments-list">
{{range .BlogComments}}
@@ -125,9 +131,11 @@
<span class="comment-dislike-count" data-comment-id="{{.ID}}">{{if index $.CommentReactionCounts .ID}}{{(index $.CommentReactionCounts .ID).Dislikes}}{{else}}0{{end}}</span>
</button>
</div>
{{if $.BlogPost.AllowComments}}
<button class="blog-reply-btn" data-comment-id="{{.ID}}" type="button">
{{svg "octicon-reply" 14}} {{ctx.Locale.Tr "repo.blog.comment.reply"}}
</button>
{{end}}
{{if or $.IsWriter (and $.IsSigned (eq .UserID $.SignedUserID))}}
<form method="post" action="{{$.RepoLink}}/blog/{{$.BlogPost.ID}}/comment/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}}
@@ -181,6 +189,7 @@
</div>
{{end}}
{{if $.BlogPost.AllowComments}}
<!-- Inline reply form (hidden by default) -->
<div class="blog-reply-form tw-hidden" id="reply-form-{{.ID}}">
<form method="post" action="{{$.RepoLink}}/blog/{{$.BlogPost.ID}}/comment">
@@ -193,13 +202,17 @@
</div>
</form>
</div>
{{end}}
</div>
{{end}}
</div>
{{else}}
<p class="blog-comments-empty">{{ctx.Locale.Tr "repo.blog.comments.empty"}}</p>
{{if .BlogPost.AllowComments}}
<p class="blog-comments-empty">{{ctx.Locale.Tr "repo.blog.comments.empty"}}</p>
{{end}}
{{end}}
{{if .BlogPost.AllowComments}}
<!-- New comment form -->
<div class="blog-comment-form-wrapper">
{{if .IsSigned}}
@@ -247,6 +260,7 @@
{{end}}
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>