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 61683966a2
commit cb967b92fd
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. # TODO(opensource): We should adapt Asaf's seedfile command and then delete this.
THIS_PATH=$(pwd) THIS_PATH=$(pwd)
BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta' #BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta'
# BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta' BETA_PATH='/Users/benvisness/Developer/handmade/handmade-beta'
pushd $BETA_PATH pushd $BETA_PATH
docker-compose down -v 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';\"" docker-compose exec postgres bash -c "psql -U postgres -c \"CREATE ROLE hmn CREATEDB LOGIN PASSWORD 'password';\""
popd 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) 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>[^/]+)$") var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")
func BuildEmailConfirmation(username, token string) string { func BuildEmailConfirmation(username, token string) string {
@ -295,14 +293,14 @@ func BuildProjectNew() string {
return Url("/projects/new", nil) 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() defer CatchPanic()
return Url(fmt.Sprintf("/p/%d/%s", id, slug), nil) 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 { func BuildProjectEdit(slug string, section string) string {
defer CatchPanic() defer CatchPanic()
@ -730,7 +728,7 @@ func BuildForumMarkRead(projectSlug string, subforumId int) string {
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }
var RegexCatchAll = regexp.MustCompile("") var RegexCatchAll = regexp.MustCompile("^")
/* /*
* Helper functions * Helper functions

View File

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

View File

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

View File

@ -517,7 +517,7 @@ func BlogPostDeleteSubmit(c *RequestContext) ResponseData {
} }
if threadDeleted { if threadDeleted {
projectUrl := hmnurl.BuildProjectHomepage(c.CurrentProject.Slug) projectUrl := UrlForProject(c.CurrentProject)
return c.Redirect(projectUrl, http.StatusSeeOther) return c.Redirect(projectUrl, http.StatusSeeOther)
} else { } else {
thread, err := FetchThread(c.Context(), c.Conn, c.CurrentUser, cd.ThreadID, ThreadsQuery{ 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 { func ProjectBreadcrumb(project *models.Project) templates.Breadcrumb {
return templates.Breadcrumb{ return templates.Breadcrumb{
Name: project.Name, 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] defaultTopic, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide { if !hasEpisodeGuide {
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther) return c.Redirect(UrlForProject(c.CurrentProject), http.StatusSeeOther)
} }
if topic == "" { if topic == "" {
@ -114,7 +114,7 @@ func Episode(c *RequestContext) ResponseData {
_, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug] _, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug]
if !hasEpisodeGuide { if !hasEpisodeGuide {
return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther) return c.Redirect(UrlForProject(c.CurrentProject), http.StatusSeeOther)
} }
_, foundTopic := topicsForProject(slug, topic) _, 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.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 { 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.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{ var PostTypeMap = map[models.ThreadType][]templates.PostType{
@ -55,7 +55,7 @@ func GenericThreadBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, pro
result = []templates.Breadcrumb{ result = []templates.Breadcrumb{
{ {
Name: project.Name, Name: project.Name,
Url: hmnurl.BuildProjectHomepage(project.Slug), Url: UrlForProject(project),
}, },
{ {
Name: ThreadTypeDisplayNames[thread.Type], Name: ThreadTypeDisplayNames[thread.Type],
@ -73,7 +73,7 @@ func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) str
case models.ThreadTypeForumPost: case models.ThreadTypeForumPost:
return hmnurl.BuildForum(projectSlug, nil, 1) return hmnurl.BuildForum(projectSlug, nil, 1)
} }
return hmnurl.BuildProjectHomepage(projectSlug) return hmnurl.BuildOfficialProjectHomepage(projectSlug) // TODO: both official and personal projects
} }
func MakePostListItem( func MakePostListItem(

View File

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

View File

@ -1,12 +1,10 @@
package website package website
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"math/rand" "math/rand"
"net/http" "net/http"
"strconv"
"time" "time"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
@ -155,49 +153,15 @@ type ProjectHomepageData struct {
func ProjectHomepage(c *RequestContext) ResponseData { func ProjectHomepage(c *RequestContext) ResponseData {
maxRecentActivity := 15 maxRecentActivity := 15
var project *models.Project
if c.CurrentProject.IsHMN() { if c.CurrentProject == nil {
// 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 {
return FourOhFour(c) return FourOhFour(c)
} }
// There are no further permission checks to do, because permissions are // There are no further permission checks to do, because permissions are
// checked whatever way we fetch the project. // 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 { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err) return c.ErrorResponse(http.StatusInternalServerError, err)
} }
@ -215,7 +179,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
WHERE WHERE
handmade_project_screenshots.project_id = $1 handmade_project_screenshots.project_id = $1
`, `,
project.ID, c.CurrentProject.ID,
) )
if err != nil { if err != nil {
return c.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"))
@ -235,7 +199,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
link.project_id = $1 link.project_id = $1
ORDER BY link.ordering ASC ORDER BY link.ordering ASC
`, `,
project.ID, c.CurrentProject.ID,
) )
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project links")) 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 ORDER BY post.postdate DESC
LIMIT $2 LIMIT $2
`, `,
project.ID, c.CurrentProject.ID,
maxRecentActivity, maxRecentActivity,
) )
if err != nil { if err != nil {
@ -275,36 +239,36 @@ func ProjectHomepage(c *RequestContext) ResponseData {
var projectHomepageData ProjectHomepageData var projectHomepageData ProjectHomepageData
projectHomepageData.BaseData = getBaseData(c, project.Name, nil) projectHomepageData.BaseData = getBaseData(c, c.CurrentProject.Name, nil)
if canEdit { //if canEdit {
// TODO: Move to project-specific navigation // // TODO: Move to project-specific navigation
// projectHomepageData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "") // // projectHomepageData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
} //}
projectHomepageData.BaseData.OpenGraphItems = append(projectHomepageData.BaseData.OpenGraphItems, templates.OpenGraphItem{ projectHomepageData.BaseData.OpenGraphItems = append(projectHomepageData.BaseData.OpenGraphItems, templates.OpenGraphItem{
Property: "og:description", 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 { for _, owner := range owners {
projectHomepageData.Owners = append(projectHomepageData.Owners, templates.UserToTemplate(owner, c.Theme)) projectHomepageData.Owners = append(projectHomepageData.Owners, templates.UserToTemplate(owner, c.Theme))
} }
if project.Hidden { if c.CurrentProject.Hidden {
projectHomepageData.BaseData.AddImmediateNotice( projectHomepageData.BaseData.AddImmediateNotice(
"hidden", "hidden",
"NOTICE: This project is hidden. It is currently visible only to owners and site admins.", "NOTICE: This project is hidden. It is currently visible only to owners and site admins.",
) )
} }
if project.Lifecycle != models.ProjectLifecycleActive { if c.CurrentProject.Lifecycle != models.ProjectLifecycleActive {
switch project.Lifecycle { switch c.CurrentProject.Lifecycle {
case models.ProjectLifecycleUnapproved: case models.ProjectLifecycleUnapproved:
projectHomepageData.BaseData.AddImmediateNotice( projectHomepageData.BaseData.AddImmediateNotice(
"unapproved", "unapproved",
fmt.Sprintf( 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.", "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: case models.ProjectLifecycleApprovalRequired:
@ -348,7 +312,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
lineageBuilder, lineageBuilder,
&post.(*postQuery).Post, &post.(*postQuery).Post,
&post.(*postQuery).Thread, &post.(*postQuery).Thread,
project, c.CurrentProject,
&post.(*postQuery).Author, &post.(*postQuery).Author,
c.Theme, c.Theme,
)) ))

View File

@ -30,12 +30,13 @@ type Router struct {
type Route struct { type Route struct {
Method string Method string
Regex *regexp.Regexp Regexes []*regexp.Regexp
Handler Handler Handler Handler
} }
type RouteBuilder struct { type RouteBuilder struct {
Router *Router Router *Router
Prefixes []*regexp.Regexp
Middleware Middleware Middleware Middleware
} }
@ -44,11 +45,17 @@ type Handler func(c *RequestContext) ResponseData
type Middleware func(h Handler) Handler type Middleware func(h Handler) Handler
func (rb *RouteBuilder) Handle(methods []string, regex *regexp.Regexp, h 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) h = rb.Middleware(h)
for _, method := range methods { for _, method := range methods {
rb.Router.Routes = append(rb.Router.Routes, Route{ rb.Router.Routes = append(rb.Router.Routes, Route{
Method: method, Method: method,
Regex: regex, Regexes: append(rb.Prefixes, regex),
Handler: h, Handler: h,
}) })
} }
@ -66,33 +73,36 @@ func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) {
rb.Handle([]string{http.MethodPost}, regex, h) 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) { func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
path := req.URL.Path nextroute:
for _, route := range r.Routes { for _, route := range r.Routes {
if route.Method != "" && req.Method != route.Method { if route.Method != "" && req.Method != route.Method {
continue continue
} }
path = strings.TrimSuffix(path, "/") currentPath := strings.TrimSuffix(req.URL.Path, "/")
if path == "" { if currentPath == "" {
path = "/" currentPath = "/"
} }
match := route.Regex.FindStringSubmatch(path) var params map[string]string
if match == nil { for _, regex := range route.Regexes {
continue
}
c := &RequestContext{ match := regex.FindStringSubmatch(currentPath)
Route: route.Regex.String(), if len(match) == 0 {
Logger: logging.GlobalLogger(), continue nextroute
Req: req, }
Res: rw,
}
if len(match) > 0 { if params == nil {
params := map[string]string{} params = map[string]string{}
subexpNames := route.Regex.SubexpNames() }
subexpNames := regex.SubexpNames()
for i, paramValue := range match { for i, paramValue := range match {
paramName := subexpNames[i] paramName := subexpNames[i]
if paramName == "" { if paramName == "" {
@ -100,15 +110,35 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
params[paramName] = paramValue 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) doRequest(rw, c, route.Handler)
return 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 { type RequestContext struct {

View File

@ -8,6 +8,7 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@ -154,14 +155,6 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
return res 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: // NOTE(asaf): HMN-only routes:
hmnOnly.GET(hmnurl.RegexManifesto, Manifesto) hmnOnly.GET(hmnurl.RegexManifesto, Manifesto)
hmnOnly.GET(hmnurl.RegexAbout, About) hmnOnly.GET(hmnurl.RegexAbout, About)
@ -175,14 +168,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
hmnOnly.GET(hmnurl.RegexOldHome, Index) 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.RegexLogoutAction, Logout)
hmnOnly.GET(hmnurl.RegexLoginPage, LoginPage) hmnOnly.GET(hmnurl.RegexLoginPage, LoginPage)
hmnOnly.GET(hmnurl.RegexRegister, RegisterNewUser) hmnOnly.GET(hmnurl.RegexRegister, RegisterNewUser)
hmnOnly.POST(hmnurl.RegexRegister, securityTimerMiddleware(email.ExpectedEmailSendDuration, RegisterNewUserSubmit)) hmnOnly.POST(hmnurl.RegexRegister, securityTimerMiddleware(email.ExpectedEmailSendDuration, RegisterNewUserSubmit))
hmnOnly.GET(hmnurl.RegexRegistrationSuccess, RegisterNewUserSuccess) hmnOnly.GET(hmnurl.RegexRegistrationSuccess, RegisterNewUserSuccess)
hmnOnly.GET(hmnurl.RegexOldEmailConfirmation, EmailConfirmation) // TODO(asaf): Delete this a bit after launch
hmnOnly.GET(hmnurl.RegexEmailConfirmation, EmailConfirmation) hmnOnly.GET(hmnurl.RegexEmailConfirmation, EmailConfirmation)
hmnOnly.POST(hmnurl.RegexEmailConfirmation, EmailConfirmationSubmit) 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.RegexShowcase, Showcase)
hmnOnly.GET(hmnurl.RegexSnippet, Snippet) hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex) hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
hmnOnly.GET(hmnurl.RegexPersonalProjectHomepage, ProjectHomepage)
hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback)) hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
hmnOnly.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink))) 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) hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
// NOTE(asaf): Any-project routes: // NOTE(asaf): Any-project routes:
anyProject.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread)) attachProjectRoutes := func(rb *RouteBuilder) {
anyProject.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit))) rb.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
anyProject.GET(hmnurl.RegexForumThread, ForumThread) if c.CurrentProject.IsHMN() {
anyProject.GET(hmnurl.RegexForum, Forum) return Index(c)
anyProject.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead))) } else {
anyProject.GET(hmnurl.RegexForumPost, ForumPostRedirect) return ProjectHomepage(c)
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)
anyProject.GET(hmnurl.RegexBlog, BlogIndex) rb.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
anyProject.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread)) rb.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
anyProject.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit))) rb.GET(hmnurl.RegexForumThread, ForumThread)
anyProject.GET(hmnurl.RegexBlogThread, BlogThread) rb.GET(hmnurl.RegexForum, Forum)
anyProject.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread) rb.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead)))
anyProject.GET(hmnurl.RegexBlogPostReply, authMiddleware(BlogPostReply)) rb.GET(hmnurl.RegexForumPost, ForumPostRedirect)
anyProject.POST(hmnurl.RegexBlogPostReply, authMiddleware(csrfMiddleware(BlogPostReplySubmit))) rb.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
anyProject.GET(hmnurl.RegexBlogPostEdit, authMiddleware(BlogPostEdit)) rb.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
anyProject.POST(hmnurl.RegexBlogPostEdit, authMiddleware(csrfMiddleware(BlogPostEditSubmit))) rb.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
anyProject.GET(hmnurl.RegexBlogPostDelete, authMiddleware(BlogPostDelete)) rb.POST(hmnurl.RegexForumPostEdit, authMiddleware(csrfMiddleware(ForumPostEditSubmit)))
anyProject.POST(hmnurl.RegexBlogPostDelete, authMiddleware(csrfMiddleware(BlogPostDeleteSubmit))) rb.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
anyProject.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData { rb.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
return c.Redirect(hmnurl.ProjectUrl( rb.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect)
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,
c.CurrentProject.Slug, rb.GET(hmnurl.RegexBlog, BlogIndex)
), http.StatusMovedPermanently) 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) anyProject.POST(hmnurl.RegexAssetUpload, AssetUpload)
@ -378,15 +430,29 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
slug := strings.TrimRight(hostPrefix, ".") slug := strings.TrimRight(hostPrefix, ".")
dbProject, err := FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, ProjectsQuery{}) 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) { if errors.Is(err, db.NotFound) {
return false, c.Redirect(hmnurl.BuildHomepage(), http.StatusSeeOther) // do nothing, this is fine
} else { } else {
return false, c.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"))
} }
} }
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" theme := "light"