diff --git a/src/templates/src/error.html b/src/templates/src/error.html new file mode 100644 index 00000000..27ba47cd --- /dev/null +++ b/src/templates/src/error.html @@ -0,0 +1,16 @@ +{{ template "base.html" . }} + +{{ define "content" }} +
+

+ Hi there, {{ if .User }}{{ .User.Name }}{{ else }}visitor{{ end }}! +

+

+ + We have encountered an error while processing your request. +
+ If this keeps happening, please let us know. +
+

+
+{{ end }} diff --git a/src/templates/types.go b/src/templates/types.go index e86e7d75..248ef5ad 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -6,14 +6,15 @@ import ( ) type BaseData struct { - Title string - CanonicalLink string - OpenGraphItems []OpenGraphItem - BackgroundImage BackgroundImage - Theme string - BodyClasses []string - Breadcrumbs []Breadcrumb - Notices []Notice + Title string + CanonicalLink string + OpenGraphItems []OpenGraphItem + BackgroundImage BackgroundImage + Theme string + BodyClasses []string + Breadcrumbs []Breadcrumb + Notices []Notice + ReportIssueMailto string CurrentUrl string LoginPageUrl string diff --git a/src/website/auth.go b/src/website/auth.go index ce325978..3eab2039 100644 --- a/src/website/auth.go +++ b/src/website/auth.go @@ -53,7 +53,7 @@ func Login(c *RequestContext) ResponseData { form, err := c.GetFormValues() if err != nil { - return ErrorResponse(http.StatusBadRequest, NewSafeError(err, "request must contain form data")) + return c.ErrorResponse(http.StatusBadRequest, NewSafeError(err, "request must contain form data")) } redirect := form.Get("redirect") @@ -84,7 +84,7 @@ func Login(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { return showLoginWithFailure(c, redirect) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username")) } } user := userRow.(*models.User) @@ -92,7 +92,7 @@ func Login(c *RequestContext) ResponseData { success, err := tryLogin(c, user, password) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if !success { @@ -106,7 +106,7 @@ func Login(c *RequestContext) ResponseData { res := c.Redirect(redirect, http.StatusSeeOther) err = loginUser(c, user, &res) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } return res } @@ -179,7 +179,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { userAlreadyExists = false } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user")) } } @@ -200,7 +200,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { emailAlreadyExists = false } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user")) } } c.Perf.EndBlock() @@ -215,7 +215,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", "Create user and one time token") tx, err := c.Conn.Begin(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) } defer tx.Rollback(c.Context()) @@ -231,7 +231,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { username, emailAddress, hashed.String(), now, displayName, c.GetIP(), ).Scan(&newUserId) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to store user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to store user")) } ott := models.GenerateToken() @@ -247,7 +247,7 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { newUserId, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to store one-time token")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to store one-time token")) } c.Perf.EndBlock() @@ -257,13 +257,13 @@ func RegisterNewUserSubmit(c *RequestContext) ResponseData { } err = email.SendRegistrationEmail(emailAddress, mailName, username, ott, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send registration email")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send registration email")) } c.Perf.StartBlock("SQL", "Commit user") err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit user to the db")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit user to the db")) } c.Perf.EndBlock() return c.Redirect(hmnurl.BuildRegistrationSuccess(), http.StatusSeeOther) @@ -349,7 +349,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { success, err := tryLogin(c, validationResult.User, password) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } else if !success { var res ResponseData baseData := getBaseData(c) @@ -366,7 +366,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", "Updating user status and deleting token") tx, err := c.Conn.Begin(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) } defer tx.Rollback(c.Context()) @@ -380,7 +380,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { validationResult.User.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user status")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user status")) } _, err = tx.Exec(c.Context(), @@ -390,12 +390,12 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { validationResult.OneTimeToken.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete one time token")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete one time token")) } err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit transaction")) } c.Perf.EndBlock() @@ -403,7 +403,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData { res.AddFutureNotice("success", "You've completed your registration successfully!") err = loginUser(c, validationResult.User, &res) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } return res } @@ -464,7 +464,7 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData { c.Perf.EndBlock() if err != nil { if !errors.Is(err, db.ErrNoMatchingRows) { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username")) } } if userRow != nil { @@ -487,7 +487,7 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData { c.Perf.EndBlock() if err != nil { if !errors.Is(err, db.ErrNoMatchingRows) { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch onetimetoken for user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch onetimetoken for user")) } } var resetToken *models.OneTimeToken @@ -508,7 +508,7 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete onetimetoken")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete onetimetoken")) } resetToken = nil } @@ -530,13 +530,13 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create onetimetoken")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create onetimetoken")) } resetToken = tokenRow.(*models.OneTimeToken) err = email.SendPasswordReset(user.Email, user.BestName(), user.Username, resetToken.Content, resetToken.Expires, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send email")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to send email")) } } } @@ -624,7 +624,7 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", "Update user's password and delete reset token") tx, err := c.Conn.Begin(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to start db transaction")) } defer tx.Rollback(c.Context()) @@ -638,7 +638,7 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData { validationResult.User.ID, ) if err != nil || tag.RowsAffected() == 0 { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user's password")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user's password")) } if validationResult.User.Status == models.UserStatusInactive { @@ -652,7 +652,7 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData { validationResult.User.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user's status")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user's status")) } } @@ -664,12 +664,12 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData { validationResult.OneTimeToken.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete onetimetoken")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete onetimetoken")) } err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit password reset to the db")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit password reset to the db")) } c.Perf.EndBlock() @@ -677,7 +677,7 @@ func DoPasswordResetSubmit(c *RequestContext) ResponseData { res.AddFutureNotice("success", "Password changed successfully.") err = loginUser(c, validationResult.User, &res) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } return res } diff --git a/src/website/blogs.go b/src/website/blogs.go index 85c5111c..8b264098 100644 --- a/src/website/blogs.go +++ b/src/website/blogs.go @@ -50,7 +50,7 @@ func BlogIndex(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts")) } numPages := NumPages(numPosts, postsPerPage) @@ -88,7 +88,7 @@ func BlogIndex(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch blog posts for index")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch blog posts for index")) } var entries []blogIndexEntry @@ -112,7 +112,7 @@ func BlogIndex(c *RequestContext) ResponseData { isProjectOwner := false owners, err := FetchProjectOwners(c, c.CurrentProject.ID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project owners")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project owners")) } for _, owner := range owners { if owner.ID == c.CurrentUser.ID { @@ -232,7 +232,7 @@ func BlogNewThreadSubmit(c *RequestContext) ResponseData { err = c.Req.ParseForm() if err != nil { - return ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid")) + return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid")) } title := c.Req.Form.Get("title") unparsed := c.Req.Form.Get("body") @@ -266,7 +266,7 @@ func BlogNewThreadSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new blog post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new blog post")) } newThreadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, threadId, title) @@ -339,7 +339,7 @@ func BlogPostEditSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit blog post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit blog post")) } postUrl := hmnurl.BuildBlogThreadWithPostHash(c.CurrentProject.Slug, cd.ThreadID, postData.Thread.Title, cd.PostID) @@ -385,7 +385,7 @@ func BlogPostReplySubmit(c *RequestContext) ResponseData { err = c.Req.ParseForm() if err != nil { - return ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid")) + return c.ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid")) } unparsed := c.Req.Form.Get("body") if unparsed == "" { @@ -396,7 +396,7 @@ func BlogPostReplySubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to blog post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to blog post")) } newPostUrl := hmnurl.BuildBlogPost(c.CurrentProject.Slug, cd.ThreadID, newPostId) @@ -462,7 +462,7 @@ func BlogPostDeleteSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post")) } if threadDeleted { diff --git a/src/website/discord.go b/src/website/discord.go index 25040841..b03cd212 100644 --- a/src/website/discord.go +++ b/src/website/discord.go @@ -53,19 +53,19 @@ func DiscordOAuthCallback(c *RequestContext) ResponseData { code := query.Get("code") res, err := discord.ExchangeOAuthCode(c.Context(), code, hmnurl.BuildDiscordOAuthCallback()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to exchange Discord authorization code")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to exchange Discord authorization code")) } expiry := time.Now().Add(time.Duration(res.ExpiresIn) * time.Second) user, err := discord.GetCurrentUserAsOAuth(c.Context(), res.AccessToken) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch Discord user info")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch Discord user info")) } // Add the role on Discord err = discord.AddGuildMemberRole(c.Context(), user.ID, config.Config.Discord.MemberRoleID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to add member role")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to add member role")) } // Add the user to our database @@ -85,7 +85,7 @@ func DiscordOAuthCallback(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new Discord user info")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new Discord user info")) } return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther) @@ -110,7 +110,7 @@ func DiscordUnlink(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get Discord user for unlink")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get Discord user for unlink")) } } discordUser := iDiscordUser.(*models.DiscordUser) @@ -123,12 +123,12 @@ func DiscordUnlink(c *RequestContext) ResponseData { discordUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete Discord user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete Discord user")) } err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit Discord user delete")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit Discord user delete")) } err = discord.RemoveGuildMemberRole(c.Context(), discordUser.UserID, config.Config.Discord.MemberRoleID) @@ -149,13 +149,13 @@ func DiscordShowcaseBacklog(c *RequestContext) ResponseData { c.Logger.Warn().Msg("could not do showcase backlog because no discord user exists") return c.Redirect(hmnurl.BuildUserProfile(c.CurrentUser.Username), http.StatusSeeOther) } else if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get discord user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get discord user")) } duser := iduser.(*models.DiscordUser) ok, err := discord.AllowedToCreateMessageSnippet(c.Context(), c.Conn, duser.UserID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if !ok { diff --git a/src/website/feed.go b/src/website/feed.go index bae6fe62..aec49462 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -45,7 +45,7 @@ func Feed(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts")) } numPages := int(math.Ceil(float64(numPosts) / postsPerPage)) @@ -87,7 +87,7 @@ func Feed(c *RequestContext) ResponseData { posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) } baseData := getBaseData(c) @@ -165,7 +165,7 @@ func AtomFeed(c *RequestContext) ResponseData { posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) } feedData.Posts = posts @@ -203,7 +203,7 @@ func AtomFeed(c *RequestContext) ResponseData { itemsPerFeed, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects")) } var projectIds []int projectMap := make(map[int]int) // map[project id]index in slice @@ -235,7 +235,7 @@ func AtomFeed(c *RequestContext) ResponseData { projectIds, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects owners")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects owners")) } for _, res := range owners.ToSlice() { owner := res.(*ownerResult) @@ -277,7 +277,7 @@ func AtomFeed(c *RequestContext) ResponseData { itemsPerFeed, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) } snippetQuerySlice := snippetQueryResult.ToSlice() for _, s := range snippetQuerySlice { diff --git a/src/website/forums.go b/src/website/forums.go index 1e2ece10..00e96fe7 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -330,7 +330,7 @@ func ForumMarkRead(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to mark all posts as read")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to mark all posts as read")) } // Delete thread unread info @@ -342,7 +342,7 @@ func ForumMarkRead(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete thread unread info")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete thread unread info")) } // Delete subforum unread info @@ -354,7 +354,7 @@ func ForumMarkRead(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete subforum unread info")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete subforum unread info")) } } else { c.Perf.StartBlock("SQL", "Update SLRIs") @@ -373,7 +373,7 @@ func ForumMarkRead(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum slris")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum slris")) } c.Perf.StartBlock("SQL", "Delete TLRIs") @@ -394,13 +394,13 @@ func ForumMarkRead(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete unnecessary tlris")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete unnecessary tlris")) } } err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit SLRI/TLRI updates")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit SLRI/TLRI updates")) } var redirUrl string @@ -503,7 +503,7 @@ func ForumThread(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum tlri")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum tlri")) } } @@ -546,7 +546,7 @@ func ForumPostRedirect(c *RequestContext) ResponseData { cd.ThreadID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch post ids")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch post ids")) } postQuerySlice := postQueryResult.ToSlice() c.Perf.EndBlock() @@ -574,7 +574,7 @@ func ForumPostRedirect(c *RequestContext) ResponseData { cd.ThreadID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch thread title")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch thread title")) } c.Perf.EndBlock() threadTitle := threadTitleQueryResult.(*threadTitleQuery).ThreadTitle @@ -625,7 +625,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { err = c.Req.ParseForm() if err != nil { - return ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid")) + return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "the form data was invalid")) } title := c.Req.Form.Get("title") unparsed := c.Req.Form.Get("body") @@ -665,7 +665,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new forum thread")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new forum thread")) } newThreadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), threadId, title, 1) @@ -711,7 +711,7 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData { err = c.Req.ParseForm() if err != nil { - return ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid")) + return c.ErrorResponse(http.StatusBadRequest, oops.New(nil, "the form data was invalid")) } unparsed := c.Req.Form.Get("body") if unparsed == "" { @@ -722,7 +722,7 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post")) } newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, newPostId) @@ -784,7 +784,7 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit forum post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit forum post")) } postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID) @@ -846,7 +846,7 @@ func ForumPostDeleteSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post")) } if threadDeleted { diff --git a/src/website/landing.go b/src/website/landing.go index 6baaacf8..09952e73 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -67,7 +67,7 @@ func Index(c *RequestContext) ResponseData { numProjectsToGet*2, // hedge your bets against projects that don't have any content ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get projects for home page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get projects for home page")) } defer iterProjects.Close() @@ -274,7 +274,7 @@ func Index(c *RequestContext) ResponseData { models.ThreadTypeProjectBlogPost, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) } newsPostResult := newsPostRow.(*newsPostQuery) c.Perf.EndBlock() @@ -299,7 +299,7 @@ func Index(c *RequestContext) ResponseData { `, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) } snippetQuerySlice := snippetQueryResult.ToSlice() showcaseItems := make([]templates.TimelineItem, 0, len(snippetQuerySlice)) @@ -343,7 +343,7 @@ func Index(c *RequestContext) ResponseData { WheelJamUrl: hmnurl.BuildJamIndex(), }, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render landing page template")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render landing page template")) } return res diff --git a/src/website/podcast.go b/src/website/podcast.go index 536f3f24..3f820409 100644 --- a/src/website/podcast.go +++ b/src/website/podcast.go @@ -32,7 +32,7 @@ type PodcastIndexData struct { func PodcastIndex(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, "") if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil { @@ -41,7 +41,7 @@ func PodcastIndex(c *RequestContext) ResponseData { canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } baseData := getBaseData(c) @@ -63,7 +63,7 @@ func PodcastIndex(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("podcast_index.html", podcastIndexData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast index page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast index page")) } return res } @@ -76,12 +76,12 @@ type PodcastEditData struct { func PodcastEdit(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "") if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || !canEdit { @@ -99,7 +99,7 @@ func PodcastEdit(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("podcast_edit.html", podcastEditData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast edit page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast edit page")) } return res } @@ -107,12 +107,12 @@ func PodcastEdit(c *RequestContext) ResponseData { func PodcastEditSubmit(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "") if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || !canEdit { @@ -128,7 +128,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData { c.Perf.EndBlock() if err != nil { // NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here. - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form")) } title := c.Req.Form.Get("title") @@ -143,7 +143,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", "Updating podcast") tx, err := c.Conn.Begin(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to start db transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to start db transaction")) } defer tx.Rollback(c.Context()) @@ -153,7 +153,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData { if errors.As(err, &rejectErr) { return RejectRequest(c, rejectErr.Error()) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to save podcast image")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to save podcast image")) } } @@ -173,7 +173,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData { podcastResult.Podcast.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast")) } } else { _, err = tx.Exec(c.Context(), @@ -192,7 +192,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to commit db transaction")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to commit db transaction")) } res := c.Redirect(hmnurl.BuildPodcastEdit(c.CurrentProject.Slug), http.StatusSeeOther) @@ -212,7 +212,7 @@ func PodcastEpisode(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, episodeGUIDStr) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || len(podcastResult.Episodes) == 0 { @@ -246,7 +246,7 @@ func PodcastEpisode(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("podcast_episode.html", podcastEpisodeData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode page")) } return res } @@ -264,12 +264,12 @@ type PodcastEpisodeEditData struct { func PodcastEpisodeNew(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, false, "") if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || !canEdit { @@ -278,7 +278,7 @@ func PodcastEpisodeNew(c *RequestContext) ResponseData { episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) } podcast := templates.PodcastToTemplate(c.CurrentProject.Slug, podcastResult.Podcast, "") @@ -291,7 +291,7 @@ func PodcastEpisodeNew(c *RequestContext) ResponseData { EpisodeFiles: episodeFiles, }, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode new page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode new page")) } return res } @@ -304,12 +304,12 @@ func PodcastEpisodeEdit(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, episodeGUIDStr) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || len(podcastResult.Episodes) == 0 || !canEdit { @@ -318,7 +318,7 @@ func PodcastEpisodeEdit(c *RequestContext) ResponseData { episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) } episode := podcastResult.Episodes[0] @@ -339,7 +339,7 @@ func PodcastEpisodeEdit(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("podcast_episode_edit.html", podcastEpisodeEditData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode edit page")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast episode edit page")) } return res } @@ -350,12 +350,12 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { isEdit := found && episodeGUIDStr != "" podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, isEdit, episodeGUIDStr) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canEdit, err := CanEditProject(c, c.CurrentUser, podcastResult.Podcast.ProjectID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil || (isEdit && len(podcastResult.Episodes) == 0) || !canEdit { @@ -366,7 +366,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { episodeFiles, err := GetEpisodeFiles(c.CurrentProject.Slug) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch podcast episode file list")) } c.Req.ParseForm() @@ -399,7 +399,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { c.Perf.StartBlock("MP3", "Parsing mp3 file for duration") file, err := os.Open(fmt.Sprintf("public/media/podcast/%s/%s", c.CurrentProject.Slug, episodeFile)) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to open podcast file")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to open podcast file")) } mp3Decoder := mp3.NewDecoder(file) @@ -421,7 +421,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { file.Close() c.Perf.EndBlock() if decodingError != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to decode mp3 file")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to decode mp3 file")) } c.Perf.StartBlock("MARKDOWN", "Parsing description") @@ -455,7 +455,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast episode")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to update podcast episode")) } } else { guid := uuid.New() @@ -480,7 +480,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData { ) c.Perf.EndBlock() if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to create podcast episode")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to create podcast episode")) } } @@ -504,7 +504,7 @@ type PodcastRSSData struct { func PodcastRSS(c *RequestContext) ResponseData { podcastResult, err := FetchPodcast(c, c.CurrentProject.ID, true, "") if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } if podcastResult.Podcast == nil { @@ -529,7 +529,7 @@ func PodcastRSS(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("podcast.xml", podcastRSSData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast RSS")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render podcast RSS")) } return res } diff --git a/src/website/projects.go b/src/website/projects.go index eed0196c..60157559 100644 --- a/src/website/projects.go +++ b/src/website/projects.go @@ -71,7 +71,7 @@ func ProjectIndex(c *RequestContext) ResponseData { models.VisibleProjectLifecycles, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects")) } allProjectsSlice := allProjects.ToSlice() c.Perf.EndBlock() @@ -112,7 +112,7 @@ func ProjectIndex(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user projects")) } for _, project := range userProjectsResult.ToSlice() { p := project.(*UserProjectQuery).Project @@ -244,7 +244,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { return FourOhFour(c) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project by slug")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project by slug")) } } project = &projectQueryResult.(*projectQuery).Project @@ -262,7 +262,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { owners, err := FetchProjectOwners(c, project.ID) if err != nil { - return ErrorResponse(http.StatusInternalServerError, err) + return c.ErrorResponse(http.StatusInternalServerError, err) } canView := false @@ -312,7 +312,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { project.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch screenshots for project")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch screenshots for project")) } c.Perf.EndBlock() @@ -332,7 +332,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { project.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project links")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project links")) } c.Perf.EndBlock() @@ -363,7 +363,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { maxRecentActivity, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project posts")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project posts")) } c.Perf.EndBlock() @@ -445,7 +445,7 @@ func ProjectHomepage(c *RequestContext) ResponseData { var res ResponseData err = res.WriteTemplate("project_homepage.html", projectHomepageData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render project homepage template")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render project homepage template")) } return res } diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index 6ffbfe1b..a1cefe6c 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -246,6 +246,15 @@ func (c *RequestContext) Redirect(dest string, code int) ResponseData { return res } +func (c *RequestContext) ErrorResponse(status int, errs ...error) ResponseData { + res := ResponseData{ + StatusCode: status, + Errors: errs, + } + res.MustWriteTemplate("error.html", getBaseData(c), c.Perf) + return res +} + type ResponseData struct { StatusCode int Body *bytes.Buffer @@ -304,13 +313,6 @@ func (rd *ResponseData) MustWriteTemplate(name string, data interface{}, rp *per } } -func ErrorResponse(status int, errs ...error) ResponseData { - return ResponseData{ - StatusCode: status, - Errors: errs, - } -} - func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) { defer func() { /* @@ -320,6 +322,7 @@ func doRequest(rw http.ResponseWriter, c *RequestContext, h Handler) { if recovered := recover(); recovered != nil { rw.WriteHeader(http.StatusInternalServerError) logging.LogPanicValue(c.Logger, recovered, "request panicked and was not handled") + rw.Write([]byte("There was a problem handling your request.\nPlease notify an admin at team@handmade.network")) } }() diff --git a/src/website/routes.go b/src/website/routes.go index 4a7c6b99..10aef4c6 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -38,6 +38,7 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe defer logPerf() defer LogContextErrors(c, &res) + defer MiddlewarePanicCatcher(c, &res) return h(c) } @@ -53,6 +54,7 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe defer logPerf() defer LogContextErrors(c, &res) + defer MiddlewarePanicCatcher(c, &res) defer storeNoticesInCookie(c, &res) @@ -74,6 +76,7 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe defer logPerf() defer LogContextErrors(c, &res) + defer MiddlewarePanicCatcher(c, &res) defer storeNoticesInCookie(c, &res) @@ -284,6 +287,8 @@ func getBaseData(c *RequestContext) templates.BaseData { Session: templateSession, Notices: notices, + ReportIssueMailto: "team@handmade.network", + OpenGraphItems: buildDefaultOpenGraphItems(c.CurrentProject), IsProjectPage: !c.CurrentProject.IsHMN(), @@ -355,7 +360,7 @@ func FetchProjectBySlug(ctx context.Context, conn *pgxpool.Pool, slug string) (* func ProjectCSS(c *RequestContext) ResponseData { color := c.URL().Query().Get("color") if color == "" { - return ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n")) + return c.ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n")) } baseData := getBaseData(c) @@ -386,7 +391,7 @@ func ProjectCSS(c *RequestContext) ResponseData { res.Header().Add("Content-Type", "text/css") err := res.WriteTemplate("project.css", templateData, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to generate project CSS")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to generate project CSS")) } return res @@ -423,7 +428,7 @@ func RejectRequest(c *RequestContext, reason string) ResponseData { RejectReason: reason, }, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to render reject template")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to render reject template")) } return res } @@ -439,7 +444,7 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) { dbProject, err := FetchProjectBySlug(c.Context(), c.Conn, slug) if err != nil { - return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch current project")) + return false, c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch current project")) } if dbProject == nil { return false, c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) @@ -453,7 +458,7 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) { if err == nil { user, session, err := getCurrentUserAndSession(c, sessionCookie.Value) if err != nil { - return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user")) + return false, c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user")) } c.CurrentUser = user @@ -539,6 +544,19 @@ func LogContextErrors(c *RequestContext, res *ResponseData) { } } +func MiddlewarePanicCatcher(c *RequestContext, res *ResponseData) { + if recovered := recover(); recovered != nil { + maybeError, ok := recovered.(*error) + var err error + if ok { + err = *maybeError + } else { + err = oops.New(nil, fmt.Sprintf("Recovered from panic with value: %v", recovered)) + } + *res = c.ErrorResponse(http.StatusInternalServerError, err) + } +} + const NoticesCookieName = "hmn_notices" func getNoticesFromCookie(c *RequestContext) []templates.Notice { diff --git a/src/website/routes_test.go b/src/website/routes_test.go index 0e4d088d..bbd822a9 100644 --- a/src/website/routes_test.go +++ b/src/website/routes_test.go @@ -35,7 +35,7 @@ func TestLogContextErrors(t *testing.T) { } routes.GET(regexp.MustCompile("^/test$"), func(c *RequestContext) ResponseData { - return ErrorResponse(http.StatusInternalServerError, err1, err2) + return c.ErrorResponse(http.StatusInternalServerError, err1, err2) }) srv := httptest.NewServer(router) diff --git a/src/website/showcase.go b/src/website/showcase.go index f7b7f015..3577fa15 100644 --- a/src/website/showcase.go +++ b/src/website/showcase.go @@ -36,7 +36,7 @@ func Showcase(c *RequestContext) ResponseData { `, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets")) } snippetQuerySlice := snippetQueryResult.ToSlice() showcaseItems := make([]templates.TimelineItem, 0, len(snippetQuerySlice)) diff --git a/src/website/snippet.go b/src/website/snippet.go index 0a279331..08d5c86d 100644 --- a/src/website/snippet.go +++ b/src/website/snippet.go @@ -53,7 +53,7 @@ func Snippet(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { return FourOhFour(c) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippet")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippet")) } } c.Perf.EndBlock() @@ -111,7 +111,7 @@ func Snippet(c *RequestContext) ResponseData { Snippet: snippet, }, c.Perf) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render snippet template")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render snippet template")) } return res } diff --git a/src/website/user.go b/src/website/user.go index dc35a399..01d7b550 100644 --- a/src/website/user.go +++ b/src/website/user.go @@ -59,7 +59,7 @@ func UserProfile(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { return FourOhFour(c) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", username)) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", username)) } } profileUser = userResult.(*models.User) @@ -80,7 +80,7 @@ func UserProfile(c *RequestContext) ResponseData { profileUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch links for user: %s", username)) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch links for user: %s", username)) } userLinksSlice := userLinkQueryResult.ToSlice() profileUserLinks := make([]templates.Link, 0, len(userLinksSlice)) @@ -108,7 +108,7 @@ func UserProfile(c *RequestContext) ResponseData { models.VisibleProjectLifecycles, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects for user: %s", username)) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch projects for user: %s", username)) } projectQuerySlice := projectQueryResult.ToSlice() templateProjects := make([]templates.Project, 0, len(projectQuerySlice)) @@ -139,7 +139,7 @@ func UserProfile(c *RequestContext) ResponseData { models.VisibleProjectLifecycles, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch posts for user: %s", username)) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch posts for user: %s", username)) } postQuerySlice := postQueryResult.ToSlice() c.Perf.EndBlock() @@ -163,7 +163,7 @@ func UserProfile(c *RequestContext) ResponseData { profileUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets for user: %s", username)) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets for user: %s", username)) } snippetQuerySlice := snippetQueryResult.ToSlice() c.Perf.EndBlock() @@ -272,7 +272,7 @@ func UserSettings(c *RequestContext) ResponseData { c.CurrentUser.ID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links")) } links := ilinks.ToSlice() @@ -295,7 +295,7 @@ func UserSettings(c *RequestContext) ResponseData { if errors.Is(err, db.ErrNoMatchingRows) { // this is fine, but don't fetch any more messages } else if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user's Discord account")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user's Discord account")) } else { duser := iduser.(*models.DiscordUser) tmp := templates.DiscordUserToTemplate(duser) @@ -316,7 +316,7 @@ func UserSettings(c *RequestContext) ResponseData { config.Config.Discord.ShowcaseChannelID, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check for unsaved user messages")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check for unsaved user messages")) } } @@ -402,7 +402,7 @@ func UserSettingsSave(c *RequestContext) ResponseData { discordDeleteSnippetOnMessageDelete, ) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user")) } // Process links @@ -460,7 +460,7 @@ func UserSettingsSave(c *RequestContext) ResponseData { if errors.As(err, &rejectErr) { return RejectRequest(c, rejectErr.Error()) } else { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new avatar")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new avatar")) } } @@ -468,7 +468,7 @@ func UserSettingsSave(c *RequestContext) ResponseData { err = tx.Commit(c.Context()) if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save user settings")) + return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save user settings")) } return c.Redirect(hmnurl.BuildUserSettings(""), http.StatusSeeOther) @@ -489,7 +489,7 @@ func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *Res ok, err := auth.CheckPassword(old, oldHashedPassword) if err != nil { - res := ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check user's password")) + res := c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check user's password")) return &res } @@ -501,7 +501,7 @@ func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *Res newHashedPassword := auth.HashPassword(new) err = auth.UpdatePassword(c.Context(), tx, c.CurrentUser.Username, newHashedPassword) if err != nil { - res := ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update password")) + res := c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update password")) return &res }