2021-03-14 20:49:58 +00:00
package website
import (
"context"
2021-03-21 20:38:37 +00:00
"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"
2021-03-21 20:38:37 +00:00
"strings"
2021-04-26 06:56:49 +00:00
"time"
2021-03-14 20:49:58 +00:00
2021-03-22 03:07:18 +00:00
"git.handmade.network/hmn/hmn/src/auth"
2021-07-08 07:40:30 +00:00
"git.handmade.network/hmn/hmn/src/config"
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-05-04 14:40:40 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
2021-03-22 03:07:18 +00:00
"git.handmade.network/hmn/hmn/src/logging"
2021-03-21 20:38:37 +00:00
"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"
2021-05-03 22:45:17 +00:00
"github.com/teacat/noire"
2021-03-14 20:49:58 +00:00
)
2021-08-17 06:08:33 +00:00
func NewWebsiteRoutes ( longRequestContext context . Context , conn * pgxpool . Pool , perfCollector * perf . PerfCollector ) http . Handler {
2021-04-29 03:07:14 +00:00
router := & Router { }
2021-04-06 05:06:19 +00:00
routes := RouteBuilder {
Router : router ,
2021-04-29 03:07:14 +00:00
Middleware : func ( h Handler ) Handler {
return func ( c * RequestContext ) ( res ResponseData ) {
2021-04-06 05:06:19 +00:00
c . Conn = conn
2021-04-29 03:07:14 +00:00
logPerf := TrackRequestPerf ( c , perfCollector )
defer logPerf ( )
2021-09-06 00:00:25 +00:00
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
}
2021-04-06 05:06:19 +00:00
mainRoutes := routes
2021-04-29 03:07:14 +00:00
mainRoutes . Middleware = func ( h Handler ) Handler {
return func ( c * RequestContext ) ( res ResponseData ) {
c . Conn = conn
logPerf := TrackRequestPerf ( c , perfCollector )
defer logPerf ( )
2021-09-06 00:00:25 +00:00
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 )
}
}
2021-05-04 14:40:40 +00:00
staticPages := routes
staticPages . Middleware = func ( h Handler ) Handler {
return func ( c * RequestContext ) ( res ResponseData ) {
c . Conn = conn
logPerf := TrackRequestPerf ( c , perfCollector )
defer logPerf ( )
2021-09-06 00:00:25 +00:00
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 ( ) {
2021-05-30 18:35:01 +00:00
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 )
}
}
2021-06-12 03:51:07 +00:00
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 {
2021-08-27 17:58:52 +00:00
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-04-06 05:06:19 +00:00
2021-05-05 20:34:32 +00:00
mainRoutes . GET ( hmnurl . RegexHomepage , func ( c * RequestContext ) ResponseData {
2021-04-22 23:02:50 +00:00
if c . CurrentProject . IsHMN ( ) {
2021-04-06 05:06:19 +00:00
return Index ( c )
2021-03-31 03:55:19 +00:00
} else {
2021-07-08 07:40:30 +00:00
return ProjectHomepage ( c )
2021-03-31 03:55:19 +00:00
}
} )
2021-05-05 20:34:32 +00:00
staticPages . GET ( hmnurl . RegexManifesto , Manifesto )
staticPages . GET ( hmnurl . RegexAbout , About )
staticPages . GET ( hmnurl . RegexCodeOfConduct , CodeOfConduct )
staticPages . GET ( hmnurl . RegexCommunicationGuidelines , CommunicationGuidelines )
staticPages . GET ( hmnurl . RegexContactPage , ContactPage )
staticPages . GET ( hmnurl . RegexMonthlyUpdatePolicy , MonthlyUpdatePolicy )
staticPages . GET ( hmnurl . RegexProjectSubmissionGuidelines , ProjectSubmissionGuidelines )
2021-08-17 19:48:44 +00:00
staticPages . GET ( hmnurl . RegexWhenIsIt , WhenIsIt )
2021-08-28 11:26:17 +00:00
staticPages . GET ( hmnurl . RegexJamIndex , JamIndex )
2021-05-04 14:40:40 +00:00
2021-06-22 09:50:40 +00:00
// TODO(asaf): Have separate middleware for HMN-only routes and any-project routes
// NOTE(asaf): HMN-only routes:
2021-08-08 20:05:52 +00:00
mainRoutes . GET ( hmnurl . RegexOldHome , Index )
2021-08-28 13:31:19 +00:00
mainRoutes . POST ( hmnurl . RegexLoginAction , securityTimerMiddleware ( time . Millisecond * 100 , Login ) ) // TODO(asaf): Adjust this after launch
2021-08-08 20:05:52 +00:00
mainRoutes . GET ( hmnurl . RegexLogoutAction , Logout )
mainRoutes . GET ( hmnurl . RegexLoginPage , LoginPage )
mainRoutes . GET ( hmnurl . RegexRegister , RegisterNewUser )
2021-08-17 18:48:54 +00:00
mainRoutes . POST ( hmnurl . RegexRegister , securityTimerMiddleware ( email . ExpectedEmailSendDuration , RegisterNewUserSubmit ) )
2021-08-08 20:05:52 +00:00
mainRoutes . GET ( hmnurl . RegexRegistrationSuccess , RegisterNewUserSuccess )
mainRoutes . GET ( hmnurl . RegexOldEmailConfirmation , EmailConfirmation ) // TODO(asaf): Delete this a bit after launch
mainRoutes . GET ( hmnurl . RegexEmailConfirmation , EmailConfirmation )
mainRoutes . POST ( hmnurl . RegexEmailConfirmation , EmailConfirmationSubmit )
2021-08-17 05:18:04 +00:00
mainRoutes . GET ( hmnurl . RegexRequestPasswordReset , RequestPasswordReset )
2021-08-17 18:48:54 +00:00
mainRoutes . POST ( hmnurl . RegexRequestPasswordReset , securityTimerMiddleware ( email . ExpectedEmailSendDuration , RequestPasswordResetSubmit ) )
2021-08-17 05:18:04 +00:00
mainRoutes . GET ( hmnurl . RegexPasswordResetSent , PasswordResetSent )
mainRoutes . GET ( hmnurl . RegexOldDoPasswordReset , DoPasswordReset )
mainRoutes . GET ( hmnurl . RegexDoPasswordReset , DoPasswordReset )
mainRoutes . POST ( hmnurl . RegexDoPasswordReset , DoPasswordResetSubmit )
2021-09-24 00:12:46 +00:00
mainRoutes . GET ( hmnurl . RegexAdminAtomFeed , AdminAtomFeed )
mainRoutes . GET ( hmnurl . RegexAdminApprovalQueue , adminMiddleware ( AdminApprovalQueue ) )
mainRoutes . POST ( hmnurl . RegexAdminApprovalQueue , adminMiddleware ( csrfMiddleware ( AdminApprovalQueueSubmit ) ) )
2021-05-05 20:34:32 +00:00
mainRoutes . GET ( hmnurl . RegexFeed , Feed )
2021-05-30 18:35:01 +00:00
mainRoutes . GET ( hmnurl . RegexAtomFeed , AtomFeed )
2021-06-22 17:08:05 +00:00
mainRoutes . GET ( hmnurl . RegexShowcase , Showcase )
2021-06-23 20:13:22 +00:00
mainRoutes . GET ( hmnurl . RegexSnippet , Snippet )
2021-06-06 23:48:43 +00:00
mainRoutes . GET ( hmnurl . RegexProjectIndex , ProjectIndex )
2021-07-08 07:40:30 +00:00
mainRoutes . GET ( hmnurl . RegexProjectNotApproved , ProjectHomepage )
2021-06-06 23:48:43 +00:00
2021-08-28 10:40:13 +00:00
mainRoutes . GET ( hmnurl . RegexDiscordOAuthCallback , authMiddleware ( DiscordOAuthCallback ) )
mainRoutes . POST ( hmnurl . RegexDiscordUnlink , authMiddleware ( csrfMiddleware ( DiscordUnlink ) ) )
mainRoutes . POST ( hmnurl . RegexDiscordShowcaseBacklog , authMiddleware ( csrfMiddleware ( DiscordShowcaseBacklog ) ) )
2021-08-28 11:26:17 +00:00
mainRoutes . GET ( hmnurl . RegexUserProfile , UserProfile )
2021-08-28 10:40:13 +00:00
mainRoutes . GET ( hmnurl . RegexUserSettings , authMiddleware ( UserSettings ) )
mainRoutes . POST ( hmnurl . RegexUserSettings , authMiddleware ( csrfMiddleware ( UserSettingsSave ) ) )
2021-06-22 09:50:40 +00:00
// NOTE(asaf): Any-project routes:
2021-07-20 02:35:22 +00:00
mainRoutes . GET ( hmnurl . RegexForumNewThread , authMiddleware ( ForumNewThread ) )
2021-07-02 05:11:58 +00:00
mainRoutes . POST ( hmnurl . RegexForumNewThreadSubmit , authMiddleware ( csrfMiddleware ( ForumNewThreadSubmit ) ) )
2021-05-05 20:34:32 +00:00
mainRoutes . GET ( hmnurl . RegexForumThread , ForumThread )
2021-07-30 03:40:47 +00:00
mainRoutes . GET ( hmnurl . RegexForum , Forum )
mainRoutes . POST ( hmnurl . RegexForumMarkRead , authMiddleware ( csrfMiddleware ( ForumMarkRead ) ) )
2021-06-25 13:52:43 +00:00
mainRoutes . GET ( hmnurl . RegexForumPost , ForumPostRedirect )
2021-07-20 02:35:22 +00:00
mainRoutes . GET ( hmnurl . RegexForumPostReply , authMiddleware ( ForumPostReply ) )
2021-07-23 19:00:37 +00:00
mainRoutes . POST ( hmnurl . RegexForumPostReply , authMiddleware ( csrfMiddleware ( ForumPostReplySubmit ) ) )
2021-07-22 01:41:23 +00:00
mainRoutes . GET ( hmnurl . RegexForumPostEdit , authMiddleware ( ForumPostEdit ) )
2021-07-23 19:00:37 +00:00
mainRoutes . POST ( hmnurl . RegexForumPostEdit , authMiddleware ( csrfMiddleware ( ForumPostEditSubmit ) ) )
2021-07-22 04:42:34 +00:00
mainRoutes . GET ( hmnurl . RegexForumPostDelete , authMiddleware ( ForumPostDelete ) )
2021-07-23 19:00:37 +00:00
mainRoutes . POST ( hmnurl . RegexForumPostDelete , authMiddleware ( csrfMiddleware ( ForumPostDeleteSubmit ) ) )
2021-09-09 00:43:24 +00:00
mainRoutes . GET ( hmnurl . RegexWikiArticle , WikiArticleRedirect )
2021-04-29 03:34:22 +00:00
2021-08-03 01:52:46 +00:00
mainRoutes . GET ( hmnurl . RegexBlog , BlogIndex )
2021-07-30 23:08:42 +00:00
mainRoutes . GET ( hmnurl . RegexBlogNewThread , authMiddleware ( BlogNewThread ) )
mainRoutes . POST ( hmnurl . RegexBlogNewThread , authMiddleware ( csrfMiddleware ( BlogNewThreadSubmit ) ) )
2021-07-30 19:59:48 +00:00
mainRoutes . GET ( hmnurl . RegexBlogThread , BlogThread )
mainRoutes . GET ( hmnurl . RegexBlogPost , BlogPostRedirectToThread )
2021-07-30 23:08:42 +00:00
mainRoutes . GET ( hmnurl . RegexBlogPostReply , authMiddleware ( BlogPostReply ) )
mainRoutes . POST ( hmnurl . RegexBlogPostReply , authMiddleware ( csrfMiddleware ( BlogPostReplySubmit ) ) )
mainRoutes . GET ( hmnurl . RegexBlogPostEdit , authMiddleware ( BlogPostEdit ) )
mainRoutes . POST ( hmnurl . RegexBlogPostEdit , authMiddleware ( csrfMiddleware ( BlogPostEditSubmit ) ) )
mainRoutes . GET ( hmnurl . RegexBlogPostDelete , authMiddleware ( BlogPostDelete ) )
mainRoutes . POST ( hmnurl . RegexBlogPostDelete , authMiddleware ( csrfMiddleware ( BlogPostDeleteSubmit ) ) )
2021-09-20 15:17:53 +00:00
mainRoutes . 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 )
} )
2021-07-30 19:59:48 +00:00
2021-09-21 23:13:11 +00:00
mainRoutes . POST ( hmnurl . RegexAssetUpload , AssetUpload )
2021-07-23 03:09:46 +00:00
mainRoutes . GET ( hmnurl . RegexPodcast , PodcastIndex )
mainRoutes . GET ( hmnurl . RegexPodcastEdit , PodcastEdit )
mainRoutes . POST ( hmnurl . RegexPodcastEdit , PodcastEditSubmit )
mainRoutes . GET ( hmnurl . RegexPodcastEpisodeNew , PodcastEpisodeNew )
mainRoutes . POST ( hmnurl . RegexPodcastEpisodeNew , PodcastEpisodeSubmit )
mainRoutes . GET ( hmnurl . RegexPodcastEpisodeEdit , PodcastEpisodeEdit )
mainRoutes . POST ( hmnurl . RegexPodcastEpisodeEdit , PodcastEpisodeSubmit )
mainRoutes . GET ( hmnurl . RegexPodcastEpisode , PodcastEpisode )
mainRoutes . GET ( hmnurl . RegexPodcastRSS , PodcastRSS )
2021-08-28 10:40:13 +00:00
mainRoutes . GET ( hmnurl . RegexEpisodeList , EpisodeList )
mainRoutes . GET ( hmnurl . RegexEpisode , Episode )
mainRoutes . GET ( hmnurl . RegexCineraIndex , CineraIndex )
2021-08-16 04:40:56 +00:00
2021-05-05 20:34:32 +00:00
mainRoutes . GET ( hmnurl . RegexProjectCSS , ProjectCSS )
2021-07-30 22:32:19 +00:00
mainRoutes . GET ( hmnurl . RegexEditorPreviewsJS , func ( c * RequestContext ) ResponseData {
var res ResponseData
res . MustWriteTemplate ( "editorpreviews.js" , nil , c . Perf )
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-05-05 20:34:32 +00:00
mainRoutes . 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
}
2021-09-01 18:25:09 +00:00
func getBaseDataAutocrumb ( c * RequestContext , title string ) templates . BaseData {
return getBaseData ( c , title , [ ] templates . Breadcrumb { { Name : title , Url : "" } } )
}
// NOTE(asaf): If you set breadcrumbs, the breadcrumb for the current project will automatically be prepended when necessary.
// If you pass nil, no breadcrumbs will be created.
func getBaseData ( c * RequestContext , title string , breadcrumbs [ ] templates . Breadcrumb ) templates . BaseData {
2021-03-27 21:10:11 +00:00
var templateUser * templates . User
2021-06-12 03:51:07 +00:00
var templateSession * templates . Session
2021-04-06 03:30:11 +00:00
if c . CurrentUser != nil {
2021-06-12 03:51:07 +00:00
u := templates . UserToTemplate ( c . CurrentUser , c . Theme )
s := templates . SessionToTemplate ( c . CurrentSession )
templateUser = & u
templateSession = & s
2021-03-27 21:10:11 +00:00
}
2021-08-17 05:18:04 +00:00
notices := getNoticesFromCookie ( c )
2021-09-01 18:25:09 +00:00
if len ( breadcrumbs ) > 0 {
projectUrl := hmnurl . BuildProjectHomepage ( c . CurrentProject . Slug )
if breadcrumbs [ 0 ] . Url != projectUrl {
rootBreadcrumb := templates . Breadcrumb {
Name : c . CurrentProject . Name ,
Url : projectUrl ,
}
breadcrumbs = append ( [ ] templates . Breadcrumb { rootBreadcrumb } , breadcrumbs ... )
}
}
2021-08-28 10:40:13 +00:00
episodeGuideUrl := ""
defaultTopic , hasAnnotations := config . Config . EpisodeGuide . Projects [ c . CurrentProject . Slug ]
if hasAnnotations {
episodeGuideUrl = hmnurl . BuildEpisodeList ( c . CurrentProject . Slug , defaultTopic )
}
2021-08-28 17:07:45 +00:00
baseData := templates . BaseData {
2021-09-01 18:25:09 +00:00
Theme : c . Theme ,
Title : title ,
Breadcrumbs : breadcrumbs ,
2021-06-12 00:48:03 +00:00
CurrentUrl : c . FullUrl ( ) ,
2021-05-11 22:53:23 +00:00
LoginPageUrl : hmnurl . BuildLoginPage ( c . FullUrl ( ) ) ,
ProjectCSSUrl : hmnurl . BuildProjectCSS ( c . CurrentProject . Color1 ) ,
2021-06-12 00:48:03 +00:00
Project : templates . ProjectToTemplate ( c . CurrentProject , c . Theme ) ,
User : templateUser ,
2021-06-12 03:51:07 +00:00
Session : templateSession ,
2021-08-17 05:18:04 +00:00
Notices : notices ,
2021-06-12 00:48:03 +00:00
2021-08-28 12:21:03 +00:00
ReportIssueMailto : "team@handmade.network" ,
2021-09-09 02:51:43 +00:00
OpenGraphItems : buildDefaultOpenGraphItems ( c . CurrentProject , title ) ,
2021-08-17 19:48:44 +00:00
2021-06-06 23:48:43 +00:00
IsProjectPage : ! c . CurrentProject . IsHMN ( ) ,
2021-05-11 22:53:23 +00:00
Header : templates . Header {
2021-09-24 00:12:46 +00:00
AdminUrl : hmnurl . BuildAdminApprovalQueue ( ) , // TODO(asaf): Replace with general-purpose admin page
2021-08-17 05:18:04 +00:00
UserSettingsUrl : hmnurl . BuildUserSettings ( "" ) ,
2021-05-11 22:53:23 +00:00
LoginActionUrl : hmnurl . BuildLoginAction ( c . FullUrl ( ) ) ,
2021-06-12 00:48:03 +00:00
LogoutActionUrl : hmnurl . BuildLogoutAction ( c . FullUrl ( ) ) ,
2021-08-28 13:31:19 +00:00
ForgotPasswordUrl : hmnurl . BuildRequestPasswordReset ( ) ,
2021-08-08 20:05:52 +00:00
RegisterUrl : hmnurl . BuildRegister ( ) ,
2021-06-22 10:12:17 +00:00
HMNHomepageUrl : hmnurl . BuildHomepage ( ) ,
2021-05-11 22:53:23 +00:00
ProjectHomepageUrl : hmnurl . BuildProjectHomepage ( c . CurrentProject . Slug ) ,
2021-06-06 23:48:43 +00:00
ProjectIndexUrl : hmnurl . BuildProjectIndex ( 1 ) ,
2021-05-11 22:53:23 +00:00
BlogUrl : hmnurl . BuildBlog ( c . CurrentProject . Slug , 1 ) ,
2021-07-30 03:40:47 +00:00
ForumsUrl : hmnurl . BuildForum ( c . CurrentProject . Slug , nil , 1 ) ,
2021-05-11 22:53:23 +00:00
LibraryUrl : hmnurl . BuildLibrary ( c . CurrentProject . Slug ) ,
ManifestoUrl : hmnurl . BuildManifesto ( ) ,
2021-08-28 10:40:13 +00:00
EpisodeGuideUrl : episodeGuideUrl ,
2021-07-08 07:40:30 +00:00
EditUrl : "" ,
2021-08-17 20:09:24 +00:00
SearchActionUrl : "https://duckduckgo.com" ,
2021-05-11 22:53:23 +00:00
} ,
Footer : templates . Footer {
HomepageUrl : hmnurl . BuildHomepage ( ) ,
AboutUrl : hmnurl . BuildAbout ( ) ,
ManifestoUrl : hmnurl . BuildManifesto ( ) ,
CodeOfConductUrl : hmnurl . BuildCodeOfConduct ( ) ,
CommunicationGuidelinesUrl : hmnurl . BuildCommunicationGuidelines ( ) ,
2021-06-06 23:48:43 +00:00
ProjectIndexUrl : hmnurl . BuildProjectIndex ( 1 ) ,
2021-07-30 03:40:47 +00:00
ForumsUrl : hmnurl . BuildForum ( models . HMNProjectSlug , nil , 1 ) ,
2021-05-11 22:53:23 +00:00
ContactUrl : hmnurl . BuildContactPage ( ) ,
} ,
2021-03-21 20:38:37 +00:00
}
2021-08-28 17:07:45 +00:00
if c . CurrentUser != nil {
baseData . Header . UserProfileUrl = hmnurl . BuildUserProfile ( c . CurrentUser . Username )
}
return baseData
2021-03-21 20:38:37 +00:00
}
2021-09-09 02:51:43 +00:00
func buildDefaultOpenGraphItems ( project * models . Project , title string ) [ ] templates . OpenGraphItem {
if title == "" {
title = "Handmade Network"
}
image := hmnurl . BuildPublic ( "logo.png" , false )
if ! project . IsHMN ( ) {
image = hmnurl . BuildUserFile ( project . LogoLight )
}
2021-08-17 19:48:44 +00:00
return [ ] templates . OpenGraphItem {
2021-09-09 02:51:43 +00:00
{ Property : "og:title" , Value : title } ,
{ Property : "og:site_name" , Value : "Handmade Network" } ,
2021-08-17 19:48:44 +00:00
{ Property : "og:type" , Value : "website" } ,
2021-09-09 02:51:43 +00:00
{ Property : "og:image" , Value : image } ,
2021-08-17 19:48:44 +00:00
}
}
2021-03-21 20:38:37 +00:00
func FetchProjectBySlug ( ctx context . Context , conn * pgxpool . Pool , slug string ) ( * models . Project , error ) {
2021-05-25 13:26:12 +00:00
if len ( slug ) > 0 && slug != models . HMNProjectSlug {
subdomainProjectRow , err := db . QueryOne ( ctx , conn , models . Project { } , "SELECT $columns FROM handmade_project WHERE slug = $1" , slug )
if err == nil {
subdomainProject := subdomainProjectRow . ( * models . Project )
return subdomainProject , nil
2021-09-14 04:13:58 +00:00
} else if ! errors . Is ( err , db . NotFound ) {
2021-05-25 13:26:12 +00:00
return nil , oops . New ( err , "failed to get projects by slug" )
2021-03-21 20:38:37 +00:00
} else {
2021-05-25 13:26:12 +00:00
return nil , nil
}
} else {
defaultProjectRow , err := db . QueryOne ( ctx , conn , models . Project { } , "SELECT $columns FROM handmade_project WHERE id = $1" , models . HMNProjectID )
if err != nil {
2021-09-14 04:13:58 +00:00
if errors . Is ( err , db . NotFound ) {
2021-05-25 13:26:12 +00:00
return nil , oops . New ( nil , "default project didn't exist in the database" )
} else {
return nil , oops . New ( err , "failed to get default project" )
}
2021-03-21 20:38:37 +00:00
}
2021-05-25 13:26:12 +00:00
defaultProject := defaultProjectRow . ( * models . Project )
return defaultProject , nil
2021-03-21 20:38:37 +00:00
}
2021-03-14 20:49:58 +00:00
}
2021-04-06 05:06:19 +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 )
2021-05-03 22:45:17 +00:00
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 {
2021-05-03 22:45:17 +00:00
templates . BaseData
2021-05-04 01:59:45 +00:00
Color string
PostBgColor string
2021-03-14 20:49:58 +00:00
} {
2021-05-04 01:59:45 +00:00
BaseData : baseData ,
Color : color ,
PostBgColor : bgColor . HTML ( ) ,
2021-03-14 20:49:58 +00:00
}
2021-04-06 03:30:11 +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
}
2021-04-06 03:30:11 +00:00
return res
2021-03-14 20:49:58 +00:00
}
2021-03-21 20:38:37 +00:00
2021-04-06 05:06:19 +00:00
func FourOhFour ( c * RequestContext ) ResponseData {
2021-05-04 13:23:02 +00:00
var res ResponseData
res . StatusCode = http . StatusNotFound
2021-05-04 13:35:30 +00:00
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 ) ,
2021-05-04 13:35:30 +00:00
Wanted : c . FullUrl ( ) ,
}
2021-07-17 15:19:17 +00:00
res . MustWriteTemplate ( "404.html" , templateData , c . Perf )
2021-05-04 13:35:30 +00:00
} 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-04-06 05:06:19 +00:00
// get project
{
2021-07-31 03:31:43 +00:00
hostPrefix := strings . TrimSuffix ( c . Req . Host , hmnurl . GetBaseHost ( ) )
slug := strings . TrimRight ( hostPrefix , "." )
2021-03-27 21:10:11 +00:00
2021-04-06 05:06:19 +00:00
dbProject , err := FetchProjectBySlug ( c . Context ( ) , c . Conn , slug )
if err != nil {
2021-08-28 12:21:03 +00:00
return false , c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch current project" ) )
2021-03-21 20:38:37 +00:00
}
2021-05-25 13:26:12 +00:00
if dbProject == nil {
return false , c . Redirect ( hmnurl . BuildHomepage ( ) , http . StatusSeeOther )
}
2021-03-21 20:38:37 +00:00
2021-04-06 05:06:19 +00:00
c . CurrentProject = dbProject
}
2021-03-21 20:38:37 +00:00
2021-04-26 06:56:49 +00:00
{
sessionCookie , err := c . Req . Cookie ( auth . SessionCookieName )
if err == nil {
2021-06-12 03:51:07 +00:00
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-03-21 20:38:37 +00:00
2021-04-26 06:56:49 +00:00
c . CurrentUser = user
2021-06-12 03:51:07 +00:00
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-03-21 20:38:37 +00:00
}
2021-04-06 05:06:19 +00:00
2021-05-25 13:12:20 +00:00
theme := "light"
if c . CurrentUser != nil && c . CurrentUser . DarkTheme {
theme = "dark"
}
c . Theme = theme
2021-04-06 05:06:19 +00:00
return true , ResponseData { }
2021-03-21 20:38:37 +00:00
}
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-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.
2021-06-12 03:51:07 +00:00
func getCurrentUserAndSession ( c * RequestContext , sessionId string ) ( * models . User , * models . Session , error ) {
2021-04-06 05:06:19 +00:00
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 ) {
2021-06-12 03:51:07 +00:00
return nil , nil , nil
2021-03-27 21:10:11 +00:00
} else {
2021-06-12 03:51:07 +00:00
return nil , nil , oops . New ( err , "failed to get current session" )
2021-03-27 21:10:11 +00:00
}
}
2021-04-06 05:06:19 +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 {
2021-09-14 04:13:58 +00:00
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" )
2021-06-12 03:51:07 +00:00
return nil , nil , nil // user was deleted or something
2021-03-27 21:10:11 +00:00
} else {
2021-06-12 03:51:07 +00:00
return nil , nil , oops . New ( err , "failed to get user for session" )
2021-03-27 21:10:11 +00:00
}
}
2021-03-31 03:55:19 +00:00
user := userRow . ( * models . User )
2021-03-27 21:10:11 +00:00
2021-06-12 03:51:07 +00:00
return user , session , nil
2021-03-27 21:10:11 +00:00
}
2021-04-29 03:07:14 +00:00
2021-09-14 04:13:58 +00:00
const PerfContextKey = "HMNPerf"
2021-04-29 03:07:14 +00:00
func TrackRequestPerf ( c * RequestContext , perfCollector * perf . PerfCollector ) ( after func ( ) ) {
2021-07-23 03:09:46 +00:00
c . Perf = perf . MakeNewRequestPerf ( c . Route , c . Req . Method , c . Req . URL . Path )
2021-09-14 04:13:58 +00:00
c . ctx = context . WithValue ( c . Context ( ) , 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
}
}
2021-09-14 04:13:58 +00:00
func ExtractPerf ( ctx context . Context ) * perf . RequestPerf {
iperf := ctx . Value ( PerfContextKey )
if iperf == nil {
return nil
}
return iperf . ( * perf . RequestPerf )
}
2021-09-06 00:00:25 +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
2021-09-06 00:00:25 +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
}