2021-03-14 20:49:58 +00:00
package website
import (
2021-03-21 20:38:37 +00:00
"errors"
2021-04-26 06:56:49 +00:00
"fmt"
2021-03-14 20:49:58 +00:00
"net/http"
2021-11-09 19:14:38 +00:00
"strconv"
2021-04-26 06:56:49 +00:00
"time"
2021-03-14 20:49:58 +00:00
2021-03-21 20:38:37 +00:00
"git.handmade.network/hmn/hmn/src/db"
2021-08-17 18:48:54 +00:00
"git.handmade.network/hmn/hmn/src/email"
2021-12-09 02:04:15 +00:00
"git.handmade.network/hmn/hmn/src/hmndata"
2021-05-04 14:40:40 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
2021-03-21 20:38:37 +00:00
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
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"
)
2022-06-24 21:38:11 +00:00
func NewWebsiteRoutes ( conn * pgxpool . Pool ) http . Handler {
2021-04-29 03:07:14 +00:00
router := & Router { }
2021-04-06 05:06:19 +00:00
routes := RouteBuilder {
Router : router ,
2022-06-24 21:38:11 +00:00
Middlewares : [ ] Middleware {
setDBConn ( conn ) ,
trackRequestPerf ,
logContextErrorsMiddleware ,
panicCatcherMiddleware ,
2021-04-26 06:56:49 +00:00
} ,
2021-03-14 20:49:58 +00:00
}
2022-06-24 21:38:11 +00:00
anyProject := routes . WithMiddleware (
storeNoticesInCookieMiddleware ,
loadCommonData ,
)
hmnOnly := anyProject . WithMiddleware (
redirectToHMN ,
)
2021-08-17 06:08:33 +00:00
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 )
2022-06-24 21:38:11 +00:00
addCORSHeaders ( c , & res )
2021-07-08 07:40:30 +00:00
return res
} )
2022-06-14 01:06:44 +00:00
routes . GET ( hmnurl . RegexFishbowlFiles , FishbowlFiles )
2021-04-06 05:06:19 +00:00
2021-06-22 09:50:40 +00:00
// NOTE(asaf): HMN-only routes:
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexManifesto , Manifesto )
hmnOnly . GET ( hmnurl . RegexAbout , About )
hmnOnly . GET ( hmnurl . RegexCommunicationGuidelines , CommunicationGuidelines )
hmnOnly . GET ( hmnurl . RegexContactPage , ContactPage )
hmnOnly . GET ( hmnurl . RegexMonthlyUpdatePolicy , MonthlyUpdatePolicy )
hmnOnly . GET ( hmnurl . RegexProjectSubmissionGuidelines , ProjectSubmissionGuidelines )
2022-07-26 15:07:57 +00:00
hmnOnly . GET ( hmnurl . RegexConferences , Conferences )
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexWhenIsIt , WhenIsIt )
2022-06-17 22:30:18 +00:00
hmnOnly . GET ( hmnurl . RegexJamIndex , JamIndex2022 )
hmnOnly . GET ( hmnurl . RegexJamIndex2021 , JamIndex2021 )
2022-06-19 22:26:33 +00:00
hmnOnly . GET ( hmnurl . RegexJamIndex2022 , JamIndex2022 )
hmnOnly . GET ( hmnurl . RegexJamFeed2022 , JamFeed2022 )
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexOldHome , Index )
2021-11-09 19:14:38 +00:00
hmnOnly . POST ( hmnurl . RegexLoginAction , securityTimerMiddleware ( time . Millisecond * 100 , Login ) )
2021-10-27 00:45:11 +00:00
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 )
2022-06-24 21:38:11 +00:00
hmnOnly . GET ( hmnurl . RegexAdminApprovalQueue , adminsOnly ( AdminApprovalQueue ) )
hmnOnly . POST ( hmnurl . RegexAdminApprovalQueue , adminsOnly ( csrfMiddleware ( AdminApprovalQueueSubmit ) ) )
2022-09-10 21:52:02 +00:00
hmnOnly . POST ( hmnurl . RegexAdminSetUserOptions , adminsOnly ( csrfMiddleware ( UserProfileAdminSetOptions ) ) )
2022-06-24 21:38:11 +00:00
hmnOnly . POST ( hmnurl . RegexAdminNukeUser , adminsOnly ( csrfMiddleware ( UserProfileAdminNuke ) ) )
2021-10-27 00:45:11 +00:00
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 )
2022-06-24 21:38:11 +00:00
hmnOnly . GET ( hmnurl . RegexProjectNew , needsAuth ( ProjectNew ) )
hmnOnly . POST ( hmnurl . RegexProjectNew , needsAuth ( csrfMiddleware ( ProjectNewSubmit ) ) )
2021-11-25 03:59:51 +00:00
2022-06-24 21:38:11 +00:00
hmnOnly . GET ( hmnurl . RegexDiscordOAuthCallback , needsAuth ( DiscordOAuthCallback ) )
hmnOnly . POST ( hmnurl . RegexDiscordUnlink , needsAuth ( csrfMiddleware ( DiscordUnlink ) ) )
hmnOnly . POST ( hmnurl . RegexDiscordShowcaseBacklog , needsAuth ( csrfMiddleware ( DiscordShowcaseBacklog ) ) )
2021-10-27 00:45:11 +00:00
2022-03-22 18:07:43 +00:00
hmnOnly . POST ( hmnurl . RegexTwitchEventSubCallback , TwitchEventSubCallback )
hmnOnly . GET ( hmnurl . RegexTwitchDebugPage , TwitchDebugPage )
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexUserProfile , UserProfile )
2022-06-24 21:38:11 +00:00
hmnOnly . GET ( hmnurl . RegexUserSettings , needsAuth ( UserSettings ) )
hmnOnly . POST ( hmnurl . RegexUserSettings , needsAuth ( csrfMiddleware ( UserSettingsSave ) ) )
2021-10-27 00:45:11 +00:00
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 )
2022-06-12 12:45:56 +00:00
hmnOnly . GET ( hmnurl . RegexFishbowlIndex , FishbowlIndex )
hmnOnly . GET ( hmnurl . RegexFishbowl , Fishbowl )
2022-09-10 17:54:26 +00:00
educationPrerelease := hmnOnly . WithMiddleware ( educationBetaTestersOnly )
{
educationPrerelease . GET ( hmnurl . RegexEducationIndex , EducationIndex )
educationPrerelease . GET ( hmnurl . RegexEducationGlossary , EducationGlossary )
educationPrerelease . GET ( hmnurl . RegexEducationArticleNew , educationAuthorsOnly ( EducationArticleNew ) )
educationPrerelease . POST ( hmnurl . RegexEducationArticleNew , educationAuthorsOnly ( EducationArticleNewSubmit ) )
2022-09-20 01:26:43 +00:00
educationPrerelease . GET ( hmnurl . RegexEducationRerender , educationAuthorsOnly ( EducationRerender ) )
2022-09-10 17:54:26 +00:00
educationPrerelease . GET ( hmnurl . RegexEducationArticle , EducationArticle ) // Article stuff must be last so `/glossary` and others do not match as an article slug
educationPrerelease . GET ( hmnurl . RegexEducationArticleEdit , educationAuthorsOnly ( EducationArticleEdit ) )
educationPrerelease . POST ( hmnurl . RegexEducationArticleEdit , educationAuthorsOnly ( EducationArticleEditSubmit ) )
educationPrerelease . GET ( hmnurl . RegexEducationArticleDelete , educationAuthorsOnly ( EducationArticleDelete ) )
educationPrerelease . POST ( hmnurl . RegexEducationArticleDelete , educationAuthorsOnly ( csrfMiddleware ( EducationArticleDeleteSubmit ) ) )
}
2022-09-10 16:29:57 +00:00
2021-11-25 03:59:51 +00:00
hmnOnly . POST ( hmnurl . RegexAPICheckUsername , csrfMiddleware ( APICheckUsername ) )
2022-09-10 17:54:26 +00:00
hmnOnly . GET ( hmnurl . RegexLibraryAny , LibraryNotPortedYet )
// hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
// return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)
// })
2021-08-28 10:40:13 +00:00
2022-06-24 21:38:11 +00:00
// Project routes can appear either at the root (e.g. hero.handmade.network/edit)
// or on a personal project path (e.g. handmade.network/p/123/hero/edit). So, we
// have pulled all those routes into this function.
2021-11-09 19:14:38 +00:00
attachProjectRoutes := func ( rb * RouteBuilder ) {
rb . GET ( hmnurl . RegexHomepage , func ( c * RequestContext ) ResponseData {
if c . CurrentProject . IsHMN ( ) {
return Index ( c )
} else {
return ProjectHomepage ( c )
}
} )
2022-06-24 21:38:11 +00:00
rb . GET ( hmnurl . RegexProjectEdit , needsAuth ( ProjectEdit ) )
rb . POST ( hmnurl . RegexProjectEdit , needsAuth ( csrfMiddleware ( ProjectEditSubmit ) ) )
2021-11-25 03:59:51 +00:00
2021-11-10 17:13:56 +00:00
// 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
2022-06-24 21:38:11 +00:00
return needsAuth ( h ) ( c )
2021-11-10 17:13:56 +00:00
}
}
rb . POST ( hmnurl . RegexForumNewThreadSubmit , needsForums ( csrfMiddleware ( ForumNewThreadSubmit ) ) )
rb . GET ( hmnurl . RegexForumNewThread , needsForums ( ForumNewThread ) )
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexForumThread , ForumThread )
rb . GET ( hmnurl . RegexForum , Forum )
2022-06-24 21:38:11 +00:00
rb . POST ( hmnurl . RegexForumMarkRead , needsAuth ( csrfMiddleware ( ForumMarkRead ) ) ) // needs auth but doesn't need forums enabled
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexForumPost , ForumPostRedirect )
2021-11-10 17:13:56 +00:00
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 ) ) )
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexWikiArticle , WikiArticleRedirect )
2021-11-10 17:13:56 +00:00
// 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
2022-06-24 21:38:11 +00:00
return needsAuth ( h ) ( c )
2021-11-10 17:13:56 +00:00
}
}
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexBlog , BlogIndex )
2021-11-10 17:13:56 +00:00
rb . GET ( hmnurl . RegexBlogNewThread , needsBlogs ( BlogNewThread ) )
rb . POST ( hmnurl . RegexBlogNewThread , needsBlogs ( csrfMiddleware ( BlogNewThreadSubmit ) ) )
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexBlogThread , BlogThread )
rb . GET ( hmnurl . RegexBlogPost , BlogPostRedirectToThread )
2021-11-10 17:13:56 +00:00
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 ) ) )
2021-11-09 19:14:38 +00:00
rb . GET ( hmnurl . RegexBlogsRedirect , func ( c * RequestContext ) ResponseData {
2021-11-10 04:11:39 +00:00
return c . Redirect ( c . UrlContext . Url (
2021-11-09 19:14:38 +00:00
fmt . Sprintf ( "blog%s" , c . PathParams [ "remainder" ] ) , nil ,
) , http . StatusMovedPermanently )
} )
2021-07-30 19:59:48 +00:00
2022-08-02 02:59:42 +00:00
rb . POST ( hmnurl . RegexAssetUpload , AssetUpload )
2021-11-09 19:14:38 +00:00
}
2022-06-24 21:38:11 +00:00
officialProjectRoutes := anyProject . WithMiddleware ( officialProjectMiddleware )
personalProjectRoutes := hmnOnly . Group ( hmnurl . RegexPersonalProject , personalProjectMiddleware )
attachProjectRoutes ( & officialProjectRoutes )
attachProjectRoutes ( & personalProjectRoutes )
2021-07-23 03:09:46 +00:00
2022-08-05 04:03:45 +00:00
anyProject . POST ( hmnurl . RegexSnippetSubmit , needsAuth ( csrfMiddleware ( SnippetEditSubmit ) ) )
2021-10-27 00:45:11 +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
2021-10-27 00:45:11 +00:00
anyProject . GET ( hmnurl . RegexProjectCSS , ProjectCSS )
2021-12-07 05:20:12 +00:00
anyProject . GET ( hmnurl . RegexMarkdownWorkerJS , func ( c * RequestContext ) ResponseData {
2021-07-30 22:32:19 +00:00
var res ResponseData
2021-12-07 05:20:12 +00:00
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
2021-10-27 00:45:11 +00:00
anyProject . AnyMethod ( hmnurl . RegexCatchAll , FourOhFour )
2021-03-31 04:20:50 +00:00
2021-04-06 05:06:19 +00:00
return router
2021-03-14 20:49:58 +00:00
}
2022-06-24 21:38:11 +00:00
func setDBConn ( conn * pgxpool . Pool ) Middleware {
return func ( h Handler ) Handler {
return func ( c * RequestContext ) ResponseData {
c . Conn = conn
return h ( c )
2021-11-09 19:14:38 +00:00
}
2021-11-08 19:16:54 +00:00
}
2021-03-21 20:38:37 +00:00
}
2021-03-27 21:10:11 +00:00
2022-06-24 21:38:11 +00:00
func redirectToHMN ( h Handler ) Handler {
return func ( c * RequestContext ) ResponseData {
if ! c . CurrentProject . IsHMN ( ) {
return c . Redirect ( hmnurl . Url ( c . URL ( ) . Path , hmnurl . QFromURL ( c . URL ( ) ) ) , http . StatusMovedPermanently )
2021-03-27 21:10:11 +00:00
}
2022-06-24 21:38:11 +00:00
return h ( c )
2021-03-27 21:10:11 +00:00
}
}
2021-04-29 03:07:14 +00:00
2022-06-24 21:38:11 +00:00
func officialProjectMiddleware ( h Handler ) Handler {
return func ( c * RequestContext ) ResponseData {
// 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 )
2021-04-29 03:07:14 +00:00
}
2022-06-24 21:38:11 +00:00
return h ( c )
2021-04-29 03:07:14 +00:00
}
}
2021-08-17 05:18:04 +00:00
2022-06-24 21:38:11 +00:00
func personalProjectMiddleware ( h Handler ) Handler {
return func ( c * RequestContext ) ResponseData {
hmnProject := c . CurrentProject
2021-09-06 00:00:25 +00:00
2022-06-24 21:38:11 +00:00
id := utils . Must1 ( strconv . Atoi ( c . PathParams [ "projectid" ] ) )
p , err := hmndata . FetchProject ( c , c . Conn , c . CurrentUser , id , hmndata . ProjectsQuery {
Lifecycles : models . AllProjectLifecycles ,
IncludeHidden : true ,
} )
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" ) )
}
2021-08-28 12:21:03 +00:00
}
2022-06-24 21:38:11 +00:00
c . CurrentProject = & p . Project
c . CurrentProject . Color1 = hmnProject . Color1
c . CurrentProject . Color2 = hmnProject . Color2
2021-08-17 05:18:04 +00:00
2022-06-24 21:38:11 +00:00
c . UrlContext = hmndata . UrlContextForProject ( c . CurrentProject )
c . CurrentUserCanEditCurrentProject = CanEditProject ( c . CurrentUser , p . Owners )
2021-08-17 05:18:04 +00:00
2022-06-24 21:38:11 +00:00
if ! c . CurrentProject . Personal {
return c . Redirect ( c . UrlContext . RewriteProjectUrl ( c . URL ( ) ) , http . StatusSeeOther )
2021-08-17 05:18:04 +00:00
}
2022-06-24 21:38:11 +00:00
if c . PathParams [ "projectslug" ] != models . GeneratePersonalProjectSlug ( c . CurrentProject . Name ) {
return c . Redirect ( c . UrlContext . RewriteProjectUrl ( c . URL ( ) ) , http . StatusSeeOther )
2021-08-17 05:18:04 +00:00
}
2022-06-24 21:38:11 +00:00
return h ( c )
2021-08-17 05:18:04 +00:00
}
}