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-05-05 18:44:19 +00:00
defer LogContextErrors ( 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-05-05 18:44:19 +00:00
defer LogContextErrors ( 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-05-05 18:44:19 +00:00
defer LogContextErrors ( 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-07-02 05:11:58 +00:00
csrfMiddleware := func ( h Handler ) Handler {
// CSRF mitigation actions per the OWASP cheat sheet:
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
return func ( c * RequestContext ) ResponseData {
c . Req . ParseForm ( )
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?" )
err := auth . DeleteSession ( c . Context ( ) , c . Conn , c . CurrentSession . ID )
if err != nil {
c . Logger . Error ( ) . Err ( err ) . Msg ( "failed to delete session on CSRF failure" )
}
res := c . Redirect ( "/" , http . StatusSeeOther )
res . SetCookie ( auth . DeleteSessionCookie )
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-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 )
mainRoutes . POST ( hmnurl . RegexLoginAction , Login )
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-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-06-22 09:50:40 +00:00
mainRoutes . GET ( hmnurl . RegexUserProfile , UserProfile )
2021-07-08 07:40:30 +00:00
mainRoutes . GET ( hmnurl . RegexProjectNotApproved , ProjectHomepage )
2021-06-06 23:48:43 +00:00
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-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-07-30 19:59:48 +00:00
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-16 04:40:56 +00:00
mainRoutes . GET ( hmnurl . RegexDiscordTest , authMiddleware ( DiscordTest ) ) // TODO: Delete this route
mainRoutes . GET ( hmnurl . RegexDiscordOAuthCallback , authMiddleware ( DiscordOAuthCallback ) )
2021-08-16 05:07:17 +00:00
mainRoutes . POST ( hmnurl . RegexDiscordUnlink , authMiddleware ( DiscordUnlink ) )
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-04-06 05:06:19 +00:00
func getBaseData ( c * RequestContext ) 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-03-21 20:38:37 +00:00
return templates . BaseData {
2021-06-12 00:48:03 +00:00
Theme : c . Theme ,
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-17 19:48:44 +00:00
OpenGraphItems : buildDefaultOpenGraphItems ( c . CurrentProject ) ,
2021-06-06 23:48:43 +00:00
IsProjectPage : ! c . CurrentProject . IsHMN ( ) ,
2021-05-11 22:53:23 +00:00
Header : templates . Header {
AdminUrl : hmnurl . BuildHomepage ( ) , // TODO(asaf)
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-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 ( ) ,
EpisodeGuideUrl : hmnurl . BuildHomepage ( ) , // TODO(asaf)
2021-07-08 07:40:30 +00:00
EditUrl : "" ,
2021-05-11 22:53:23 +00:00
SearchActionUrl : hmnurl . BuildHomepage ( ) , // TODO(asaf)
} ,
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 ( ) ,
SitemapUrl : hmnurl . BuildSiteMap ( ) ,
} ,
2021-03-21 20:38:37 +00:00
}
}
2021-08-17 19:48:44 +00:00
func buildDefaultOpenGraphItems ( project * models . Project ) [ ] templates . OpenGraphItem {
return [ ] templates . OpenGraphItem {
{ Property : "og:site_name" , Value : "Handmade.Network" } ,
{ Property : "og:type" , Value : "website" } ,
{ Property : "og:image" , Value : hmnurl . BuildUserFile ( project . LogoLight ) } ,
{ Property : "og:image:secure_url" , Value : hmnurl . BuildUserFile ( project . LogoLight ) } ,
}
}
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
} else if ! errors . Is ( err , db . ErrNoMatchingRows ) {
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 {
if errors . Is ( err , db . ErrNoMatchingRows ) {
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-04-06 03:30:11 +00:00
return ErrorResponse ( http . StatusBadRequest , NewSafeError ( nil , "You must provide a 'color' parameter.\n" ) )
2021-03-14 20:49:58 +00:00
}
2021-05-03 22:45:17 +00:00
baseData := getBaseData ( c )
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-04-06 03:30:11 +00:00
return 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
} {
BaseData : getBaseData ( c ) ,
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 {
BaseData : getBaseData ( c ) ,
RejectReason : reason ,
} , c . Perf )
if err != nil {
return ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "Failed to render reject template" ) )
}
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 {
return false , 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 {
return false , ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to get current user" ) )
}
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 {
if errors . Is ( err , db . ErrNoMatchingRows ) {
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
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-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-04-29 03:07:14 +00:00
perfCollector . SubmitRun ( c . Perf )
}
}
2021-05-05 18:44:19 +00:00
func LogContextErrors ( c * RequestContext , res * ResponseData ) {
2021-04-29 03:07:14 +00:00
for _ , err := range res . Errors {
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
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
}