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
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:
18
README.md
18
README.md
@@ -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.
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user