hmn/src/website/routes.go

687 lines
22 KiB
Go
Raw Normal View History

2021-03-14 20:49:58 +00:00
package website
import (
"context"
"errors"
2021-04-26 06:56:49 +00:00
"fmt"
2021-08-17 05:18:04 +00:00
"html/template"
2021-08-17 06:08:33 +00:00
"math/rand"
2021-03-14 20:49:58 +00:00
"net/http"
2021-07-08 07:40:30 +00:00
"net/url"
"regexp"
"strconv"
"strings"
2021-04-26 06:56:49 +00:00
"time"
2021-03-14 20:49:58 +00:00
"git.handmade.network/hmn/hmn/src/auth"
2021-07-08 07:40:30 +00:00
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
2021-08-17 18:48:54 +00:00
"git.handmade.network/hmn/hmn/src/email"
"git.handmade.network/hmn/hmn/src/hmndata"
2021-05-04 14:40:40 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
2021-04-26 06:56:49 +00:00
"git.handmade.network/hmn/hmn/src/perf"
2021-03-14 20:49:58 +00:00
"git.handmade.network/hmn/hmn/src/templates"
2021-08-17 06:08:33 +00:00
"git.handmade.network/hmn/hmn/src/utils"
2021-03-14 20:49:58 +00:00
"github.com/jackc/pgx/v4/pgxpool"
"github.com/teacat/noire"
2021-03-14 20:49:58 +00:00
)
func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) http.Handler {
2021-04-29 03:07:14 +00:00
router := &Router{}
routes := RouteBuilder{
Router: router,
2021-04-29 03:07:14 +00:00
Middleware: func(h Handler) Handler {
return func(c *RequestContext) (res ResponseData) {
c.Conn = conn
2021-04-29 03:07:14 +00:00
logPerf := TrackRequestPerf(c)
2021-04-29 03:07:14 +00:00
defer logPerf()
defer LogContextErrorsFromResponse(c, &res)
2021-08-28 12:21:03 +00:00
defer MiddlewarePanicCatcher(c, &res)
2021-04-29 03:07:14 +00:00
return h(c)
}
2021-04-26 06:56:49 +00:00
},
2021-03-14 20:49:58 +00:00
}
anyProject := routes
anyProject.Middleware = func(h Handler) Handler {
2021-04-29 03:07:14 +00:00
return func(c *RequestContext) (res ResponseData) {
c.Conn = conn
logPerf := TrackRequestPerf(c)
2021-04-29 03:07:14 +00:00
defer logPerf()
defer LogContextErrorsFromResponse(c, &res)
2021-08-28 12:21:03 +00:00
defer MiddlewarePanicCatcher(c, &res)
2021-04-29 03:07:14 +00:00
2021-08-17 05:18:04 +00:00
defer storeNoticesInCookie(c, &res)
2021-04-29 03:07:14 +00:00
ok, errRes := LoadCommonWebsiteData(c)
if !ok {
return errRes
}
return h(c)
}
}
hmnOnly := routes
hmnOnly.Middleware = func(h Handler) Handler {
2021-05-04 14:40:40 +00:00
return func(c *RequestContext) (res ResponseData) {
c.Conn = conn
logPerf := TrackRequestPerf(c)
2021-05-04 14:40:40 +00:00
defer logPerf()
defer LogContextErrorsFromResponse(c, &res)
2021-08-28 12:21:03 +00:00
defer MiddlewarePanicCatcher(c, &res)
2021-05-04 14:40:40 +00:00
2021-08-17 05:18:04 +00:00
defer storeNoticesInCookie(c, &res)
2021-05-04 14:40:40 +00:00
ok, errRes := LoadCommonWebsiteData(c)
if !ok {
return errRes
}
if !c.CurrentProject.IsHMN() {
return c.Redirect(hmnurl.Url(c.URL().Path, hmnurl.QFromURL(c.URL())), http.StatusMovedPermanently)
2021-05-04 14:40:40 +00:00
}
return h(c)
}
}
authMiddleware := func(h Handler) Handler {
return func(c *RequestContext) (res ResponseData) {
if c.CurrentUser == nil {
return c.Redirect(hmnurl.BuildLoginPage(c.FullUrl()), http.StatusSeeOther)
}
return h(c)
}
}
2021-09-24 00:12:46 +00:00
adminMiddleware := func(h Handler) Handler {
return func(c *RequestContext) (res ResponseData) {
if c.CurrentUser == nil || !c.CurrentUser.IsStaff {
return FourOhFour(c)
}
return h(c)
}
}
2021-07-02 05:11:58 +00:00
csrfMiddleware := func(h Handler) Handler {
// CSRF mitigation actions per the OWASP cheat sheet:
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
return func(c *RequestContext) ResponseData {
c.Req.ParseMultipartForm(100 * 1024 * 1024)
2021-07-02 05:11:58 +00:00
csrfToken := c.Req.Form.Get(auth.CSRFFieldName)
if csrfToken != c.CurrentSession.CSRFToken {
c.Logger.Warn().Str("userId", c.CurrentUser.Username).Msg("user failed CSRF validation - potential attack?")
res := c.Redirect("/", http.StatusSeeOther)
2021-08-28 17:07:45 +00:00
logoutUser(c, &res)
2021-07-02 05:11:58 +00:00
return res
}
return h(c)
}
}
2021-08-17 18:48:54 +00:00
securityTimerMiddleware := func(duration time.Duration, h Handler) Handler {
2021-08-17 06:08:33 +00:00
// NOTE(asaf): Will make sure that the request takes at least `delayMs` to finish. Adds a 10% random duration.
return func(c *RequestContext) ResponseData {
2021-08-17 18:48:54 +00:00
additionalDuration := time.Duration(rand.Int63n(utils.Int64Max(1, int64(duration)/10)))
timer := time.NewTimer(duration + additionalDuration)
2021-08-17 06:08:33 +00:00
res := h(c)
2021-08-17 18:48:54 +00:00
select {
case <-longRequestContext.Done():
case <-c.Context().Done():
case <-timer.C:
}
2021-08-17 06:08:33 +00:00
return res
}
}
2021-07-08 07:40:30 +00:00
routes.GET(hmnurl.RegexPublic, func(c *RequestContext) ResponseData {
var res ResponseData
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))).ServeHTTP(&res, c.Req)
AddCORSHeaders(c, &res)
return res
})
2021-06-22 09:50:40 +00:00
// NOTE(asaf): HMN-only routes:
hmnOnly.GET(hmnurl.RegexManifesto, Manifesto)
hmnOnly.GET(hmnurl.RegexAbout, About)
hmnOnly.GET(hmnurl.RegexCodeOfConduct, CodeOfConduct)
hmnOnly.GET(hmnurl.RegexCommunicationGuidelines, CommunicationGuidelines)
hmnOnly.GET(hmnurl.RegexContactPage, ContactPage)
hmnOnly.GET(hmnurl.RegexMonthlyUpdatePolicy, MonthlyUpdatePolicy)
hmnOnly.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt)
hmnOnly.GET(hmnurl.RegexJamIndex, JamIndex)
hmnOnly.GET(hmnurl.RegexOldHome, Index)
hmnOnly.POST(hmnurl.RegexLoginAction, securityTimerMiddleware(time.Millisecond*100, Login))
hmnOnly.GET(hmnurl.RegexLogoutAction, Logout)
hmnOnly.GET(hmnurl.RegexLoginPage, LoginPage)
hmnOnly.GET(hmnurl.RegexRegister, RegisterNewUser)
hmnOnly.POST(hmnurl.RegexRegister, securityTimerMiddleware(email.ExpectedEmailSendDuration, RegisterNewUserSubmit))
hmnOnly.GET(hmnurl.RegexRegistrationSuccess, RegisterNewUserSuccess)
hmnOnly.GET(hmnurl.RegexEmailConfirmation, EmailConfirmation)
hmnOnly.POST(hmnurl.RegexEmailConfirmation, EmailConfirmationSubmit)
hmnOnly.GET(hmnurl.RegexRequestPasswordReset, RequestPasswordReset)
hmnOnly.POST(hmnurl.RegexRequestPasswordReset, securityTimerMiddleware(email.ExpectedEmailSendDuration, RequestPasswordResetSubmit))
hmnOnly.GET(hmnurl.RegexPasswordResetSent, PasswordResetSent)
hmnOnly.GET(hmnurl.RegexOldDoPasswordReset, DoPasswordReset)
hmnOnly.GET(hmnurl.RegexDoPasswordReset, DoPasswordReset)
hmnOnly.POST(hmnurl.RegexDoPasswordReset, DoPasswordResetSubmit)
hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed)
hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
hmnOnly.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit)))
hmnOnly.GET(hmnurl.RegexFeed, Feed)
hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed)
hmnOnly.GET(hmnurl.RegexShowcase, Showcase)
hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
2021-11-25 03:59:51 +00:00
hmnOnly.GET(hmnurl.RegexProjectNew, authMiddleware(ProjectNew))
hmnOnly.POST(hmnurl.RegexProjectNew, authMiddleware(csrfMiddleware(ProjectNewSubmit)))
hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
hmnOnly.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink)))
hmnOnly.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog)))
hmnOnly.GET(hmnurl.RegexUserProfile, UserProfile)
hmnOnly.GET(hmnurl.RegexUserSettings, authMiddleware(UserSettings))
hmnOnly.POST(hmnurl.RegexUserSettings, authMiddleware(csrfMiddleware(UserSettingsSave)))
hmnOnly.GET(hmnurl.RegexPodcast, PodcastIndex)
hmnOnly.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
hmnOnly.POST(hmnurl.RegexPodcastEdit, PodcastEditSubmit)
hmnOnly.GET(hmnurl.RegexPodcastEpisodeNew, PodcastEpisodeNew)
hmnOnly.POST(hmnurl.RegexPodcastEpisodeNew, PodcastEpisodeSubmit)
hmnOnly.GET(hmnurl.RegexPodcastEpisodeEdit, PodcastEpisodeEdit)
hmnOnly.POST(hmnurl.RegexPodcastEpisodeEdit, PodcastEpisodeSubmit)
hmnOnly.GET(hmnurl.RegexPodcastEpisode, PodcastEpisode)
hmnOnly.GET(hmnurl.RegexPodcastRSS, PodcastRSS)
2021-11-25 03:59:51 +00:00
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
attachProjectRoutes := func(rb *RouteBuilder) {
rb.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
if c.CurrentProject.IsHMN() {
return Index(c)
} else {
return ProjectHomepage(c)
}
})
2021-11-25 03:59:51 +00:00
rb.GET(hmnurl.RegexProjectEdit, authMiddleware(ProjectEdit))
rb.POST(hmnurl.RegexProjectEdit, authMiddleware(csrfMiddleware(ProjectEditSubmit)))
// Middleware used for forum action routes - anything related to actually creating or editing forum content
needsForums := func(h Handler) Handler {
return func(c *RequestContext) ResponseData {
// 404 if the project has forums disabled
if !c.CurrentProject.HasForums() {
return FourOhFour(c)
}
// Require auth if forums are enabled
return authMiddleware(h)(c)
}
}
rb.POST(hmnurl.RegexForumNewThreadSubmit, needsForums(csrfMiddleware(ForumNewThreadSubmit)))
rb.GET(hmnurl.RegexForumNewThread, needsForums(ForumNewThread))
rb.GET(hmnurl.RegexForumThread, ForumThread)
rb.GET(hmnurl.RegexForum, Forum)
rb.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead))) // needs auth but doesn't need forums enabled
rb.GET(hmnurl.RegexForumPost, ForumPostRedirect)
rb.GET(hmnurl.RegexForumPostReply, needsForums(ForumPostReply))
rb.POST(hmnurl.RegexForumPostReply, needsForums(csrfMiddleware(ForumPostReplySubmit)))
rb.GET(hmnurl.RegexForumPostEdit, needsForums(ForumPostEdit))
rb.POST(hmnurl.RegexForumPostEdit, needsForums(csrfMiddleware(ForumPostEditSubmit)))
rb.GET(hmnurl.RegexForumPostDelete, needsForums(ForumPostDelete))
rb.POST(hmnurl.RegexForumPostDelete, needsForums(csrfMiddleware(ForumPostDeleteSubmit)))
rb.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect)
// Middleware used for blog action routes - anything related to actually creating or editing blog content
needsBlogs := func(h Handler) Handler {
return func(c *RequestContext) ResponseData {
// 404 if the project has blogs disabled
if !c.CurrentProject.HasBlog() {
return FourOhFour(c)
}
// Require auth if blogs are enabled
return authMiddleware(h)(c)
}
}
rb.GET(hmnurl.RegexBlog, BlogIndex)
rb.GET(hmnurl.RegexBlogNewThread, needsBlogs(BlogNewThread))
rb.POST(hmnurl.RegexBlogNewThread, needsBlogs(csrfMiddleware(BlogNewThreadSubmit)))
rb.GET(hmnurl.RegexBlogThread, BlogThread)
rb.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
rb.GET(hmnurl.RegexBlogPostReply, needsBlogs(BlogPostReply))
rb.POST(hmnurl.RegexBlogPostReply, needsBlogs(csrfMiddleware(BlogPostReplySubmit)))
rb.GET(hmnurl.RegexBlogPostEdit, needsBlogs(BlogPostEdit))
rb.POST(hmnurl.RegexBlogPostEdit, needsBlogs(csrfMiddleware(BlogPostEditSubmit)))
rb.GET(hmnurl.RegexBlogPostDelete, needsBlogs(BlogPostDelete))
rb.POST(hmnurl.RegexBlogPostDelete, needsBlogs(csrfMiddleware(BlogPostDeleteSubmit)))
rb.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData {
return c.Redirect(c.UrlContext.Url(
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,
), http.StatusMovedPermanently)
})
}
hmnOnly.Group(hmnurl.RegexPersonalProject, func(rb *RouteBuilder) {
// TODO(ben): Perhaps someday we can make this middleware modification feel better? It seems
// pretty common to run the outermost middleware first before doing other stuff, but having
// to nest functions this way feels real bad.
rb.Middleware = func(h Handler) Handler {
return hmnOnly.Middleware(func(c *RequestContext) ResponseData {
// At this point we are definitely on the plain old HMN subdomain.
// Fetch personal project and do whatever
id, err := strconv.Atoi(c.PathParams["projectid"])
if err != nil {
panic(oops.New(err, "project id was not numeric (bad regex in routing)"))
}
p, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, id, hmndata.ProjectsQuery{})
if err != nil {
if errors.Is(err, db.NotFound) {
return FourOhFour(c)
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch personal project"))
}
}
c.CurrentProject = &p.Project
c.UrlContext = hmndata.UrlContextForProject(c.CurrentProject)
2021-12-09 03:50:35 +00:00
c.CurrentUserCanEditCurrentProject = CanEditProject(c.CurrentUser, p.Owners)
if !p.Project.Personal {
return c.Redirect(c.UrlContext.RewriteProjectUrl(c.URL()), http.StatusSeeOther)
}
2021-11-10 17:34:48 +00:00
if c.PathParams["projectslug"] != models.GeneratePersonalProjectSlug(p.Project.Name) {
return c.Redirect(c.UrlContext.RewriteProjectUrl(c.URL()), http.StatusSeeOther)
}
return h(c)
})
}
attachProjectRoutes(rb)
})
anyProject.Group(regexp.MustCompile("^"), func(rb *RouteBuilder) {
rb.Middleware = func(h Handler) Handler {
return anyProject.Middleware(func(c *RequestContext) ResponseData {
// We could be on any project's subdomain.
// Check if the current project (matched by subdomain) is actually no longer official
// and therefore needs to be redirected to the personal project version of the route.
if c.CurrentProject.Personal {
return c.Redirect(c.UrlContext.RewriteProjectUrl(c.URL()), http.StatusSeeOther)
}
return h(c)
})
}
attachProjectRoutes(rb)
2021-09-20 15:17:53 +00:00
})
2021-07-30 19:59:48 +00:00
anyProject.POST(hmnurl.RegexAssetUpload, AssetUpload)
2021-07-23 03:09:46 +00:00
anyProject.GET(hmnurl.RegexEpisodeList, EpisodeList)
anyProject.GET(hmnurl.RegexEpisode, Episode)
anyProject.GET(hmnurl.RegexCineraIndex, CineraIndex)
2021-08-16 04:40:56 +00:00
anyProject.GET(hmnurl.RegexProjectCSS, ProjectCSS)
anyProject.GET(hmnurl.RegexMarkdownWorkerJS, func(c *RequestContext) ResponseData {
2021-07-30 22:32:19 +00:00
var res ResponseData
res.MustWriteTemplate("markdown_worker.js", nil, c.Perf)
2021-07-30 22:32:19 +00:00
res.Header().Add("Content-Type", "application/javascript")
return res
})
2021-03-14 20:49:58 +00:00
2021-06-22 09:50:40 +00:00
// Other
anyProject.AnyMethod(hmnurl.RegexCatchAll, FourOhFour)
2021-03-31 04:20:50 +00:00
return router
2021-03-14 20:49:58 +00:00
}
func ProjectCSS(c *RequestContext) ResponseData {
2021-03-18 02:14:06 +00:00
color := c.URL().Query().Get("color")
2021-03-14 20:49:58 +00:00
if color == "" {
2021-08-28 12:21:03 +00:00
return c.ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n"))
2021-03-14 20:49:58 +00:00
}
2021-09-01 18:25:09 +00:00
baseData := getBaseData(c, "", nil)
bgColor := noire.NewHex(color)
h, s, l := bgColor.HSL()
if baseData.Theme == "dark" {
l = 15
} else {
l = 95
}
if s > 20 {
s = 20
}
bgColor = noire.NewHSL(h, s, l)
2021-03-14 20:49:58 +00:00
templateData := struct {
templates.BaseData
Color string
PostBgColor string
2021-03-14 20:49:58 +00:00
}{
BaseData: baseData,
Color: color,
PostBgColor: bgColor.HTML(),
2021-03-14 20:49:58 +00:00
}
var res ResponseData
2021-04-29 03:07:14 +00:00
res.Header().Add("Content-Type", "text/css")
2021-04-26 06:56:49 +00:00
err := res.WriteTemplate("project.css", templateData, c.Perf)
2021-03-14 20:49:58 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to generate project CSS"))
2021-03-14 20:49:58 +00:00
}
return res
2021-03-14 20:49:58 +00:00
}
func FourOhFour(c *RequestContext) ResponseData {
2021-05-04 13:23:02 +00:00
var res ResponseData
res.StatusCode = http.StatusNotFound
if c.Req.Header["Accept"] != nil && strings.Contains(c.Req.Header["Accept"][0], "text/html") {
templateData := struct {
templates.BaseData
Wanted string
}{
2021-09-01 18:25:09 +00:00
BaseData: getBaseData(c, "Page not found", nil),
Wanted: c.FullUrl(),
}
res.MustWriteTemplate("404.html", templateData, c.Perf)
} else {
res.Write([]byte("Not Found"))
}
2021-05-04 13:23:02 +00:00
return res
2021-03-31 04:20:50 +00:00
}
2021-07-23 03:22:31 +00:00
type RejectData struct {
templates.BaseData
RejectReason string
}
func RejectRequest(c *RequestContext, reason string) ResponseData {
var res ResponseData
err := res.WriteTemplate("reject.html", RejectData{
2021-09-01 18:25:09 +00:00
BaseData: getBaseData(c, "Rejected", nil),
2021-07-23 03:22:31 +00:00
RejectReason: reason,
}, c.Perf)
if err != nil {
2021-08-28 12:21:03 +00:00
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to render reject template"))
2021-07-23 03:22:31 +00:00
}
return res
}
2021-04-29 03:07:14 +00:00
func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
2021-04-26 06:56:49 +00:00
c.Perf.StartBlock("MIDDLEWARE", "Load common website data")
defer c.Perf.EndBlock()
2021-06-22 09:50:40 +00:00
2021-11-08 19:16:54 +00:00
// get user
2021-04-26 06:56:49 +00:00
{
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
if err == nil {
user, session, err := getCurrentUserAndSession(c, sessionCookie.Value)
2021-04-26 06:56:49 +00:00
if err != nil {
2021-08-28 12:21:03 +00:00
return false, c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user"))
2021-04-26 06:56:49 +00:00
}
2021-04-26 06:56:49 +00:00
c.CurrentUser = user
c.CurrentSession = session
2021-04-26 06:56:49 +00:00
}
// http.ErrNoCookie is the only error Cookie ever returns, so no further handling to do here.
}
2021-11-08 19:16:54 +00:00
// get official project
{
hostPrefix := strings.TrimSuffix(c.Req.Host, hmnurl.GetBaseHost())
slug := strings.TrimRight(hostPrefix, ".")
2021-12-04 14:55:45 +00:00
var owners []*models.User
2021-11-08 19:16:54 +00:00
2021-12-02 10:53:36 +00:00
if len(slug) > 0 {
dbProject, err := hmndata.FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, hmndata.ProjectsQuery{})
2021-12-02 10:53:36 +00:00
if err == nil {
c.CurrentProject = &dbProject.Project
2021-12-04 14:55:45 +00:00
owners = dbProject.Owners
2021-11-08 19:16:54 +00:00
} else {
2021-12-02 10:53:36 +00:00
if errors.Is(err, db.NotFound) {
// do nothing, this is fine
} else {
return false, c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
}
2021-11-08 19:16:54 +00:00
}
}
if c.CurrentProject == nil {
dbProject, err := hmndata.FetchProject(c.Context(), c.Conn, c.CurrentUser, models.HMNProjectID, hmndata.ProjectsQuery{
IncludeHidden: true,
})
if err != nil {
panic(oops.New(err, "failed to fetch HMN project"))
}
c.CurrentProject = &dbProject.Project
}
if c.CurrentProject == nil {
panic("failed to load project data")
}
2021-12-04 14:55:45 +00:00
canEditProject := false
if c.CurrentUser != nil {
2021-12-09 03:50:35 +00:00
if c.CurrentUser.IsStaff {
canEditProject = true
} else {
2021-12-04 14:55:45 +00:00
for _, o := range owners {
if o.ID == c.CurrentUser.ID {
2021-12-09 03:50:35 +00:00
canEditProject = true
2021-12-04 14:55:45 +00:00
break
}
}
}
}
c.CurrentUserCanEditCurrentProject = canEditProject
c.UrlContext = hmndata.UrlContextForProject(c.CurrentProject)
2021-11-08 19:16:54 +00:00
}
c.Theme = "light"
if c.CurrentUser != nil && c.CurrentUser.DarkTheme {
c.Theme = "dark"
}
return true, ResponseData{}
}
2021-03-27 21:10:11 +00:00
2021-07-08 07:40:30 +00:00
func AddCORSHeaders(c *RequestContext, res *ResponseData) {
parsed, err := url.Parse(config.Config.BaseUrl)
if err != nil {
c.Logger.Error().Str("Config.BaseUrl", config.Config.BaseUrl).Msg("Config.BaseUrl cannot be parsed. Skipping CORS headers")
return
}
origin := ""
origins, found := c.Req.Header["Origin"]
if found {
origin = origins[0]
}
if strings.HasSuffix(origin, parsed.Host) {
res.Header().Add("Access-Control-Allow-Origin", origin)
2021-12-04 14:55:45 +00:00
res.Header().Add("Access-Control-Allow-Credentials", "true")
2021-07-23 03:09:46 +00:00
res.Header().Add("Vary", "Origin")
2021-07-08 07:40:30 +00:00
}
}
2021-04-17 00:01:13 +00:00
// Given a session id, fetches user data from the database. Will return nil if
// the user cannot be found, and will only return an error if it's serious.
func getCurrentUserAndSession(c *RequestContext, sessionId string) (*models.User, *models.Session, error) {
session, err := auth.GetSession(c.Context(), c.Conn, sessionId)
2021-03-27 21:10:11 +00:00
if err != nil {
if errors.Is(err, auth.ErrNoSession) {
return nil, nil, nil
2021-03-27 21:10:11 +00:00
} else {
return nil, nil, oops.New(err, "failed to get current session")
2021-03-27 21:10:11 +00:00
}
}
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE username = $1", session.Username)
2021-03-27 21:10:11 +00:00
if err != nil {
if errors.Is(err, db.NotFound) {
2021-03-27 21:10:11 +00:00
logging.Debug().Str("username", session.Username).Msg("returning no current user for this request because the user for the session couldn't be found")
return nil, nil, nil // user was deleted or something
2021-03-27 21:10:11 +00:00
} else {
return nil, nil, oops.New(err, "failed to get user for session")
2021-03-27 21:10:11 +00:00
}
}
user := userRow.(*models.User)
2021-03-27 21:10:11 +00:00
return user, session, nil
2021-03-27 21:10:11 +00:00
}
2021-04-29 03:07:14 +00:00
func TrackRequestPerf(c *RequestContext) (after func()) {
2021-07-23 03:09:46 +00:00
c.Perf = perf.MakeNewRequestPerf(c.Route, c.Req.Method, c.Req.URL.Path)
c.ctx = context.WithValue(c.Context(), perf.PerfContextKey, c.Perf)
2021-04-29 03:07:14 +00:00
return func() {
c.Perf.EndRequest()
log := logging.Info()
blockStack := make([]time.Time, 0)
2021-05-11 22:53:23 +00:00
for i, block := range c.Perf.Blocks {
2021-04-29 03:07:14 +00:00
for len(blockStack) > 0 && block.End.After(blockStack[len(blockStack)-1]) {
blockStack = blockStack[:len(blockStack)-1]
}
2021-05-11 22:53:23 +00:00
log.Str(fmt.Sprintf("[%4.d] At %9.2fms", i, c.Perf.MsFromStart(&block)), fmt.Sprintf("%*.s[%s] %s (%.4fms)", len(blockStack)*2, "", block.Category, block.Description, block.DurationMs()))
2021-04-29 03:07:14 +00:00
blockStack = append(blockStack, block.End)
}
2021-07-23 03:09:46 +00:00
log.Msg(fmt.Sprintf("Served [%s] %s in %.4fms", c.Perf.Method, c.Perf.Path, float64(c.Perf.End.Sub(c.Perf.Start).Nanoseconds())/1000/1000))
2021-09-23 20:03:28 +00:00
// perfCollector.SubmitRun(c.Perf) // TODO(asaf): Implement a use for this
2021-04-29 03:07:14 +00:00
}
}
func LogContextErrors(c *RequestContext, errs ...error) {
for _, err := range errs {
2021-05-11 22:53:23 +00:00
c.Logger.Error().Timestamp().Stack().Str("Requested", c.FullUrl()).Err(err).Msg("error occurred during request")
2021-04-29 03:07:14 +00:00
}
}
2021-08-17 05:18:04 +00:00
func LogContextErrorsFromResponse(c *RequestContext, res *ResponseData) {
LogContextErrors(c, res.Errors...)
}
2021-08-28 12:21:03 +00:00
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)
}
}
2021-08-17 05:18:04 +00:00
const NoticesCookieName = "hmn_notices"
func getNoticesFromCookie(c *RequestContext) []templates.Notice {
cookie, err := c.Req.Cookie(NoticesCookieName)
if err != nil {
if !errors.Is(err, http.ErrNoCookie) {
c.Logger.Warn().Err(err).Msg("failed to get notices cookie")
}
return nil
}
return deserializeNoticesFromCookie(cookie.Value)
}
func storeNoticesInCookie(c *RequestContext, res *ResponseData) {
serialized := serializeNoticesForCookie(c, res.FutureNotices)
if serialized != "" {
noticesCookie := http.Cookie{
Name: NoticesCookieName,
Value: serialized,
Path: "/",
Domain: config.Config.Auth.CookieDomain,
Expires: time.Now().Add(time.Minute * 5),
Secure: config.Config.Auth.CookieSecure,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
res.SetCookie(&noticesCookie)
} else if !(res.StatusCode >= 300 && res.StatusCode < 400) {
// NOTE(asaf): Don't clear on redirect
noticesCookie := http.Cookie{
Name: NoticesCookieName,
Path: "/",
Domain: config.Config.Auth.CookieDomain,
MaxAge: -1,
}
res.SetCookie(&noticesCookie)
}
}
func serializeNoticesForCookie(c *RequestContext, notices []templates.Notice) string {
var builder strings.Builder
maxSize := 1024 // NOTE(asaf): Make sure we don't use too much space for notices.
size := 0
for i, notice := range notices {
sizeIncrease := len(notice.Class) + len(string(notice.Content)) + 1
if i != 0 {
sizeIncrease += 1
}
if size+sizeIncrease > maxSize {
c.Logger.Warn().Interface("Notices", notices).Msg("Notices too big for cookie")
break
}
if i != 0 {
builder.WriteString("\t")
}
builder.WriteString(notice.Class)
builder.WriteString("|")
builder.WriteString(string(notice.Content))
size += sizeIncrease
}
return builder.String()
}
func deserializeNoticesFromCookie(cookieVal string) []templates.Notice {
var result []templates.Notice
notices := strings.Split(cookieVal, "\t")
for _, notice := range notices {
parts := strings.SplitN(notice, "|", 2)
if len(parts) == 2 {
result = append(result, templates.Notice{
Class: parts[0],
Content: template.HTML(parts[1]),
})
}
}
return result
}