Add route grouping stuff for projects (needs thorough testing)

This commit is contained in:
Ben Visness 2021-11-09 11:14:38 -08:00
parent 7486f9e57d
commit ff901e4fb8
12 changed files with 204 additions and 141 deletions

View File

@ -9,8 +9,8 @@ set -euxo pipefail
# TODO(opensource): We should adapt Asaf's seedfile command and then delete this.
THIS_PATH=$(pwd)
BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta'
# BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta'
#BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta'
BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta'
pushd $BETA_PATH
docker-compose down -v
@ -19,4 +19,5 @@ pushd $BETA_PATH
docker-compose exec postgres bash -c "psql -U postgres -c \"CREATE ROLE hmn CREATEDB LOGIN PASSWORD 'password';\""
popd
go run src/main.go seedfile local/backups/hmn_pg_dump_live_2021-09-06
#go run src/main.go seedfile local/backups/hmn_pg_dump_live_2021-09-06
go run src/main.go seedfile local/backups/hmn_pg_dump_live_2021-10-23

View File

@ -97,8 +97,6 @@ func BuildRegistrationSuccess() string {
return Url("/registered_successfully", nil)
}
// TODO(asaf): Delete the old version a bit after launch
var RegexOldEmailConfirmation = regexp.MustCompile(`^/_register/confirm/(?P<username>[\w\ \.\,\-@\+\_]+)/(?P<hash>[\d\w]+)/(?P<nonce>.+)[\/]?$`)
var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")
func BuildEmailConfirmation(username, token string) string {
@ -295,14 +293,14 @@ func BuildProjectNew() string {
return Url("/projects/new", nil)
}
var RegexPersonalProjectHomepage = regexp.MustCompile("^/p/(?P<id>[0-9]+)(/(?P<slug>[^/]*))?")
var RegexPersonalProject = regexp.MustCompile("^/p/(?P<projectid>[0-9]+)(/(?P<slug>[a-zA-Z0-9-]+))?")
func BuildPersonalProjectHomepage(id int, slug string) string {
func BuildPersonalProject(id int, slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/p/%d/%s", id, slug), nil)
}
var RegexProjectEdit = regexp.MustCompile("^/p/(?P<slug>.+)/edit$")
var RegexProjectEdit = regexp.MustCompile("^/edit$")
func BuildProjectEdit(slug string, section string) string {
defer CatchPanic()
@ -730,7 +728,7 @@ func BuildForumMarkRead(projectSlug string, subforumId int) string {
return ProjectUrl(builder.String(), nil, projectSlug)
}
var RegexCatchAll = regexp.MustCompile("")
var RegexCatchAll = regexp.MustCompile("^")
/*
* Helper functions

View File

@ -62,7 +62,7 @@ var LifecycleBadgeStrings = map[models.ProjectLifecycle]string{
func ProjectUrl(p *models.Project) string {
var url string
if p.Personal {
url = hmnurl.BuildPersonalProjectHomepage(p.ID, models.GeneratePersonalProjectSlug(p.Name))
url = hmnurl.BuildPersonalProject(p.ID, models.GeneratePersonalProjectSlug(p.Name))
} else {
url = hmnurl.BuildOfficialProjectHomepage(p.Slug)
}

View File

@ -26,7 +26,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
notices := getNoticesFromCookie(c)
if len(breadcrumbs) > 0 {
projectUrl := hmnurl.BuildProjectHomepage(c.CurrentProject.Slug)
projectUrl := UrlForProject(c.CurrentProject)
if breadcrumbs[0].Url != projectUrl {
rootBreadcrumb := templates.Breadcrumb{
Name: c.CurrentProject.Name,
@ -42,7 +42,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
Breadcrumbs: breadcrumbs,
CurrentUrl: c.FullUrl(),
CurrentProjectUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
CurrentProjectUrl: UrlForProject(c.CurrentProject),
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
ProjectCSSUrl: hmnurl.BuildProjectCSS(c.CurrentProject.Color1),

View File

@ -517,7 +517,7 @@ func BlogPostDeleteSubmit(c *RequestContext) ResponseData {
}
if threadDeleted {
projectUrl := hmnurl.BuildProjectHomepage(c.CurrentProject.Slug)
projectUrl := UrlForProject(c.CurrentProject)
return c.Redirect(projectUrl, http.StatusSeeOther)
} else {
thread, err := FetchThread(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, ThreadsQuery{

View File

@ -9,7 +9,7 @@ import (
func ProjectBreadcrumb(project *models.Project) templates.Breadcrumb {
return templates.Breadcrumb{
Name: project.Name,
Url: hmnurl.BuildProjectHomepage(project.Slug),
Url: UrlForProject(project),
}
}

View File

@ -53,7 +53,7 @@ func EpisodeList(c *RequestContext) ResponseData {
defaultTopic, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide {
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther)
return c.Redirect(UrlForProject(c.CurrentProject), http.StatusSeeOther)
}
if topic == "" {
@ -114,7 +114,7 @@ func Episode(c *RequestContext) ResponseData {
_, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide {
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther)
return c.Redirect(UrlForProject(c.CurrentProject), http.StatusSeeOther)
}
_, foundTopic := topicsForProject(slug, topic)

View File

@ -15,7 +15,7 @@ func UrlForGenericThread(thread *models.Thread, lineageBuilder *models.SubforumL
return hmnurl.BuildForumThread(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), thread.ID, thread.Title, 1)
}
return hmnurl.BuildProjectHomepage(projectSlug)
return hmnurl.BuildOfficialProjectHomepage(projectSlug) // TODO: both official and personal projects
}
func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder *models.SubforumLineageBuilder, projectSlug string) string {
@ -26,7 +26,7 @@ func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder
return hmnurl.BuildForumPost(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), post.ThreadID, post.ID)
}
return hmnurl.BuildProjectHomepage(projectSlug)
return hmnurl.BuildOfficialProjectHomepage(projectSlug) // TODO: both official and personal projects
}
var PostTypeMap = map[models.ThreadType][]templates.PostType{
@ -55,7 +55,7 @@ func GenericThreadBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, pro
result = []templates.Breadcrumb{
{
Name: project.Name,
Url: hmnurl.BuildProjectHomepage(project.Slug),
Url: UrlForProject(project),
},
{
Name: ThreadTypeDisplayNames[thread.Type],
@ -73,7 +73,7 @@ func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) str
case models.ThreadTypeForumPost:
return hmnurl.BuildForum(projectSlug, nil, 1)
}
return hmnurl.BuildProjectHomepage(projectSlug)
return hmnurl.BuildOfficialProjectHomepage(projectSlug) // TODO: both official and personal projects
}
func MakePostListItem(

View File

@ -18,8 +18,9 @@ const (
type ProjectsQuery struct {
// Available on all project queries
Lifecycles []models.ProjectLifecycle // if empty, defaults to models.VisibleProjectLifecycles
Types ProjectTypeQuery // bitfield
Lifecycles []models.ProjectLifecycle // if empty, defaults to models.VisibleProjectLifecycles
Types ProjectTypeQuery // bitfield
IncludeHidden bool
// Ignored when using FetchProject
ProjectIDs []int // if empty, all projects
@ -62,8 +63,11 @@ func FetchProjects(
FROM
handmade_project AS project
WHERE
NOT hidden
TRUE
`)
if !q.IncludeHidden {
qb.Add(`AND NOT hidden`)
}
if len(q.ProjectIDs) > 0 {
qb.Add(`AND project.id = ANY ($?)`, q.ProjectIDs)
}
@ -386,7 +390,7 @@ func FetchProjectOwners(
func UrlForProject(p *models.Project) string {
if p.Personal {
return hmnurl.BuildPersonalProjectHomepage(p.ID, models.GeneratePersonalProjectSlug(p.Name))
return hmnurl.BuildPersonalProject(p.ID, models.GeneratePersonalProjectSlug(p.Name))
} else {
return hmnurl.BuildOfficialProjectHomepage(p.Slug)
}

View File

@ -1,12 +1,10 @@
package website
import (
"errors"
"fmt"
"math"
"math/rand"
"net/http"
"strconv"
"time"
"git.handmade.network/hmn/hmn/src/db"
@ -155,49 +153,15 @@ type ProjectHomepageData struct {
func ProjectHomepage(c *RequestContext) ResponseData {
maxRecentActivity := 15
var project *models.Project
if c.CurrentProject.IsHMN() {
// Viewing a personal project
idStr := c.PathParams["id"]
slug := c.PathParams["slug"]
id, err := strconv.Atoi(idStr)
if err != nil {
panic(oops.New(err, "id was not numeric (bad regex in routing)"))
}
if id == models.HMNProjectID {
return c.Redirect(hmnurl.BuildHomepage(), http.StatusPermanentRedirect)
}
p, err := FetchProject(c.Context(), c.Conn, c.CurrentUser, id, 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 project by slug"))
}
}
correctSlug := models.GeneratePersonalProjectSlug(p.Project.Name)
if slug != correctSlug {
return c.Redirect(hmnurl.BuildPersonalProjectHomepage(id, correctSlug), http.StatusPermanentRedirect)
}
project = &p.Project
} else {
project = c.CurrentProject
}
if project == nil {
if c.CurrentProject == nil {
return FourOhFour(c)
}
// There are no further permission checks to do, because permissions are
// checked whatever way we fetch the project.
owners, err := FetchProjectOwners(c.Context(), c.Conn, project.ID)
owners, err := FetchProjectOwners(c.Context(), c.Conn, c.CurrentProject.ID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
@ -215,7 +179,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
WHERE
handmade_project_screenshots.project_id = $1
`,
project.ID,
c.CurrentProject.ID,
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch screenshots for project"))
@ -235,7 +199,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
link.project_id = $1
ORDER BY link.ordering ASC
`,
project.ID,
c.CurrentProject.ID,
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project links"))
@ -265,7 +229,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
ORDER BY post.postdate DESC
LIMIT $2
`,
project.ID,
c.CurrentProject.ID,
maxRecentActivity,
)
if err != nil {
@ -275,36 +239,36 @@ func ProjectHomepage(c *RequestContext) ResponseData {
var projectHomepageData ProjectHomepageData
projectHomepageData.BaseData = getBaseData(c, project.Name, nil)
if canEdit {
// TODO: Move to project-specific navigation
// projectHomepageData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
}
projectHomepageData.BaseData = getBaseData(c, c.CurrentProject.Name, nil)
//if canEdit {
// // TODO: Move to project-specific navigation
// // projectHomepageData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
//}
projectHomepageData.BaseData.OpenGraphItems = append(projectHomepageData.BaseData.OpenGraphItems, templates.OpenGraphItem{
Property: "og:description",
Value: project.Blurb,
Value: c.CurrentProject.Blurb,
})
projectHomepageData.Project = templates.ProjectToTemplate(project, c.Theme)
projectHomepageData.Project = templates.ProjectToTemplate(c.CurrentProject, c.Theme)
for _, owner := range owners {
projectHomepageData.Owners = append(projectHomepageData.Owners, templates.UserToTemplate(owner, c.Theme))
}
if project.Hidden {
if c.CurrentProject.Hidden {
projectHomepageData.BaseData.AddImmediateNotice(
"hidden",
"NOTICE: This project is hidden. It is currently visible only to owners and site admins.",
)
}
if project.Lifecycle != models.ProjectLifecycleActive {
switch project.Lifecycle {
if c.CurrentProject.Lifecycle != models.ProjectLifecycleActive {
switch c.CurrentProject.Lifecycle {
case models.ProjectLifecycleUnapproved:
projectHomepageData.BaseData.AddImmediateNotice(
"unapproved",
fmt.Sprintf(
"NOTICE: This project has not yet been submitted for approval. It is only visible to owners. Please <a href=\"%s\">submit it for approval</a> when the project content is ready for review.",
hmnurl.BuildProjectEdit(project.Slug, "submit"),
hmnurl.BuildProjectEdit(c.CurrentProject.Slug, "submit"),
),
)
case models.ProjectLifecycleApprovalRequired:
@ -348,7 +312,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
lineageBuilder,
&post.(*postQuery).Post,
&post.(*postQuery).Thread,
project,
c.CurrentProject,
&post.(*postQuery).Author,
c.Theme,
))

View File

@ -30,12 +30,13 @@ type Router struct {
type Route struct {
Method string
Regex *regexp.Regexp
Regexes []*regexp.Regexp
Handler Handler
}
type RouteBuilder struct {
Router *Router
Prefixes []*regexp.Regexp
Middleware Middleware
}
@ -44,11 +45,17 @@ type Handler func(c *RequestContext) ResponseData
type Middleware func(h Handler) Handler
func (rb *RouteBuilder) Handle(methods []string, regex *regexp.Regexp, h Handler) {
// Ensure that this regex matches the start of the string
regexStr := regex.String()
if len(regexStr) == 0 || regexStr[0] != '^' {
panic("All routing regexes must begin with '^'")
}
h = rb.Middleware(h)
for _, method := range methods {
rb.Router.Routes = append(rb.Router.Routes, Route{
Method: method,
Regex: regex,
Regexes: append(rb.Prefixes, regex),
Handler: h,
})
}
@ -66,33 +73,36 @@ func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) {
rb.Handle([]string{http.MethodPost}, regex, h)
}
func (rb *RouteBuilder) Group(regex *regexp.Regexp, addRoutes func(rb *RouteBuilder)) {
newRb := *rb
newRb.Prefixes = append(newRb.Prefixes, regex)
addRoutes(&newRb)
}
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
path := req.URL.Path
nextroute:
for _, route := range r.Routes {
if route.Method != "" && req.Method != route.Method {
continue
}
path = strings.TrimSuffix(path, "/")
if path == "" {
path = "/"
currentPath := strings.TrimSuffix(req.URL.Path, "/")
if currentPath == "" {
currentPath = "/"
}
match := route.Regex.FindStringSubmatch(path)
if match == nil {
continue
}
var params map[string]string
for _, regex := range route.Regexes {
c := &RequestContext{
Route: route.Regex.String(),
Logger: logging.GlobalLogger(),
Req: req,
Res: rw,
}
match := regex.FindStringSubmatch(currentPath)
if len(match) == 0 {
continue nextroute
}
if len(match) > 0 {
params := map[string]string{}
subexpNames := route.Regex.SubexpNames()
if params == nil {
params = map[string]string{}
}
subexpNames := regex.SubexpNames()
for i, paramValue := range match {
paramName := subexpNames[i]
if paramName == "" {
@ -100,15 +110,35 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
params[paramName] = paramValue
}
c.PathParams = params
// Make sure that we never consume trailing slashes even if the route regex matches them
toConsume := strings.TrimSuffix(match[0], "/")
currentPath = currentPath[len(toConsume):]
if currentPath == "" {
currentPath = "/"
}
}
var routeStrings []string
for _, regex := range route.Regexes {
routeStrings = append(routeStrings, regex.String())
}
c := &RequestContext{
Route: fmt.Sprintf("%v", routeStrings),
Logger: logging.GlobalLogger(),
Req: req,
Res: rw,
PathParams: params,
}
c.PathParams = params
doRequest(rw, c, route.Handler)
return
}
panic(fmt.Sprintf("Path '%s' did not match any routes! Make sure to register a wildcard route to act as a 404.", path))
panic(fmt.Sprintf("Path '%s' did not match any routes! Make sure to register a wildcard route to act as a 404.", req.URL))
}
type RequestContext struct {

View File

@ -8,6 +8,7 @@ import (
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -154,14 +155,6 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
return res
})
anyProject.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
if c.CurrentProject.IsHMN() {
return Index(c)
} else {
return ProjectHomepage(c)
}
})
// NOTE(asaf): HMN-only routes:
hmnOnly.GET(hmnurl.RegexManifesto, Manifesto)
hmnOnly.GET(hmnurl.RegexAbout, About)
@ -175,14 +168,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
hmnOnly.GET(hmnurl.RegexOldHome, Index)
hmnOnly.POST(hmnurl.RegexLoginAction, securityTimerMiddleware(time.Millisecond*100, Login)) // TODO(asaf): Adjust this after launch
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.RegexOldEmailConfirmation, EmailConfirmation) // TODO(asaf): Delete this a bit after launch
hmnOnly.GET(hmnurl.RegexEmailConfirmation, EmailConfirmation)
hmnOnly.POST(hmnurl.RegexEmailConfirmation, EmailConfirmationSubmit)
@ -202,7 +194,6 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
hmnOnly.GET(hmnurl.RegexShowcase, Showcase)
hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
hmnOnly.GET(hmnurl.RegexPersonalProjectHomepage, ProjectHomepage)
hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
hmnOnly.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink)))
@ -225,36 +216,97 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
// NOTE(asaf): Any-project routes:
anyProject.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
anyProject.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
anyProject.GET(hmnurl.RegexForumThread, ForumThread)
anyProject.GET(hmnurl.RegexForum, Forum)
anyProject.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead)))
anyProject.GET(hmnurl.RegexForumPost, ForumPostRedirect)
anyProject.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
anyProject.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
anyProject.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
anyProject.POST(hmnurl.RegexForumPostEdit, authMiddleware(csrfMiddleware(ForumPostEditSubmit)))
anyProject.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
anyProject.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
anyProject.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect)
attachProjectRoutes := func(rb *RouteBuilder) {
rb.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
if c.CurrentProject.IsHMN() {
return Index(c)
} else {
return ProjectHomepage(c)
}
})
anyProject.GET(hmnurl.RegexBlog, BlogIndex)
anyProject.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread))
anyProject.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit)))
anyProject.GET(hmnurl.RegexBlogThread, BlogThread)
anyProject.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
anyProject.GET(hmnurl.RegexBlogPostReply, authMiddleware(BlogPostReply))
anyProject.POST(hmnurl.RegexBlogPostReply, authMiddleware(csrfMiddleware(BlogPostReplySubmit)))
anyProject.GET(hmnurl.RegexBlogPostEdit, authMiddleware(BlogPostEdit))
anyProject.POST(hmnurl.RegexBlogPostEdit, authMiddleware(csrfMiddleware(BlogPostEditSubmit)))
anyProject.GET(hmnurl.RegexBlogPostDelete, authMiddleware(BlogPostDelete))
anyProject.POST(hmnurl.RegexBlogPostDelete, authMiddleware(csrfMiddleware(BlogPostDeleteSubmit)))
anyProject.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData {
return c.Redirect(hmnurl.ProjectUrl(
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,
c.CurrentProject.Slug,
), http.StatusMovedPermanently)
rb.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
rb.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
rb.GET(hmnurl.RegexForumThread, ForumThread)
rb.GET(hmnurl.RegexForum, Forum)
rb.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead)))
rb.GET(hmnurl.RegexForumPost, ForumPostRedirect)
rb.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
rb.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
rb.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
rb.POST(hmnurl.RegexForumPostEdit, authMiddleware(csrfMiddleware(ForumPostEditSubmit)))
rb.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
rb.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
rb.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect)
rb.GET(hmnurl.RegexBlog, BlogIndex)
rb.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread))
rb.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit)))
rb.GET(hmnurl.RegexBlogThread, BlogThread)
rb.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
rb.GET(hmnurl.RegexBlogPostReply, authMiddleware(BlogPostReply))
rb.POST(hmnurl.RegexBlogPostReply, authMiddleware(csrfMiddleware(BlogPostReplySubmit)))
rb.GET(hmnurl.RegexBlogPostEdit, authMiddleware(BlogPostEdit))
rb.POST(hmnurl.RegexBlogPostEdit, authMiddleware(csrfMiddleware(BlogPostEditSubmit)))
rb.GET(hmnurl.RegexBlogPostDelete, authMiddleware(BlogPostDelete))
rb.POST(hmnurl.RegexBlogPostDelete, authMiddleware(csrfMiddleware(BlogPostDeleteSubmit)))
rb.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData {
return c.Redirect(hmnurl.ProjectUrl(
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,
c.CurrentProject.Slug,
), 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 := FetchProject(c.Context(), c.Conn, c.CurrentUser, id, 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"))
}
}
if !p.Project.Personal {
// TODO: Redirect to the same page on the other prefix
return c.Redirect(hmnurl.BuildOfficialProjectHomepage(p.Project.Slug), http.StatusSeeOther)
}
c.CurrentProject = &p.Project
return h(c)
})
}
attachProjectRoutes(rb)
})
anyProject.Group(hmnurl.RegexHomepage, 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 {
// TODO: Redirect to the same page on the other prefix
return c.Redirect(hmnurl.BuildPersonalProject(c.CurrentProject.ID, c.CurrentProject.Slug), http.StatusSeeOther)
}
return h(c)
})
}
attachProjectRoutes(rb)
})
anyProject.POST(hmnurl.RegexAssetUpload, AssetUpload)
@ -378,15 +430,29 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
slug := strings.TrimRight(hostPrefix, ".")
dbProject, err := FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, ProjectsQuery{})
if err != nil {
if err == nil {
c.CurrentProject = &dbProject.Project
} else {
if errors.Is(err, db.NotFound) {
return false, c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther)
// do nothing, this is fine
} else {
return false, c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
}
}
c.CurrentProject = &dbProject.Project
if c.CurrentProject == nil {
dbProject, err := FetchProject(c.Context(), c.Conn, c.CurrentUser, models.HMNProjectID, 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")
}
}
theme := "light"