diff --git a/README.md b/README.md index 70d540af9b..31e9d159ba 100644 --- a/README.md +++ b/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. diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 0f51a4d2ff..16d83f93fc 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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?", diff --git a/routers/web/explore/blog.go b/routers/web/explore/blog.go index 72427ba798..4913076ae6 100644 --- a/routers/web/explore/blog.go +++ b/routers/web/explore/blog.go @@ -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 } } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 656edb15e5..47742be2d8 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -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 } diff --git a/routers/web/repo/blog.go b/routers/web/repo/blog.go index 97d68a3dfb..8d221d8be9 100644 --- a/routers/web/repo/blog.go +++ b/routers/web/repo/blog.go @@ -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 } } diff --git a/templates/blog/standalone_view.tmpl b/templates/blog/standalone_view.tmpl index 9250ff23c6..bfbcc70ba3 100644 --- a/templates/blog/standalone_view.tmpl +++ b/templates/blog/standalone_view.tmpl @@ -53,7 +53,7 @@ {{if .BlogTags}}
{{end}} @@ -97,7 +97,7 @@ - {{if .BlogPost.AllowComments}} + {{if or .BlogPost.AllowComments .BlogComments}} + {{end}} {{end}} {{else}} -{{ctx.Locale.Tr "repo.blog.comments.empty"}}
+ {{if .BlogPost.AllowComments}} +{{ctx.Locale.Tr "repo.blog.comments.empty"}}
+ {{end}} {{end}} + {{if .BlogPost.AllowComments}}{{ctx.Locale.Tr "repo.blog.comments.empty"}}
+ {{if .BlogPost.AllowComments}} +{{ctx.Locale.Tr "repo.blog.comments.empty"}}
+ {{end}} {{end}} + {{if .BlogPost.AllowComments}}
{{svg "octicon-comment" 20}} @@ -105,6 +105,12 @@ {{if .CommentCount}}{{.CommentCount}}{{end}}
+ {{if not .BlogPost.AllowComments}} + + {{end}} + {{if .BlogComments}}