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-11-10 04:51:28 +00:00
"regexp"
2021-11-09 19:14:38 +00:00
"strconv"
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-11-11 23:59:05 +00:00
func NewWebsiteRoutes ( longRequestContext context . Context , 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 ,
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
2021-11-11 23:59:05 +00:00
logPerf := TrackRequestPerf ( c )
2021-04-29 03:07:14 +00:00
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-10-27 00:45:11 +00:00
anyProject := routes
anyProject . Middleware = func ( h Handler ) Handler {
2021-04-29 03:07:14 +00:00
return func ( c * RequestContext ) ( res ResponseData ) {
c . Conn = conn
2021-11-11 23:59:05 +00:00
logPerf := TrackRequestPerf ( c )
2021-04-29 03:07:14 +00:00
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-10-27 00:45:11 +00:00
hmnOnly := routes
hmnOnly . Middleware = func ( h Handler ) Handler {
2021-05-04 14:40:40 +00:00
return func ( c * RequestContext ) ( res ResponseData ) {
c . Conn = conn
2021-11-11 23:59:05 +00:00
logPerf := TrackRequestPerf ( c )
2021-05-04 14:40:40 +00:00
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-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 . RegexCodeOfConduct , CodeOfConduct )
hmnOnly . GET ( hmnurl . RegexCommunicationGuidelines , CommunicationGuidelines )
hmnOnly . GET ( hmnurl . RegexContactPage , ContactPage )
hmnOnly . GET ( hmnurl . RegexMonthlyUpdatePolicy , MonthlyUpdatePolicy )
hmnOnly . GET ( hmnurl . RegexProjectSubmissionGuidelines , ProjectSubmissionGuidelines )
hmnOnly . GET ( hmnurl . RegexWhenIsIt , WhenIsIt )
hmnOnly . GET ( hmnurl . RegexJamIndex , JamIndex )
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 )
hmnOnly . GET ( hmnurl . RegexAdminApprovalQueue , adminMiddleware ( AdminApprovalQueue ) )
hmnOnly . POST ( hmnurl . RegexAdminApprovalQueue , adminMiddleware ( csrfMiddleware ( AdminApprovalQueueSubmit ) ) )
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 )
2021-11-25 03:59:51 +00:00
hmnOnly . GET ( hmnurl . RegexProjectNew , authMiddleware ( ProjectNew ) )
hmnOnly . POST ( hmnurl . RegexProjectNew , authMiddleware ( csrfMiddleware ( ProjectNewSubmit ) ) )
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexDiscordOAuthCallback , authMiddleware ( DiscordOAuthCallback ) )
hmnOnly . POST ( hmnurl . RegexDiscordUnlink , authMiddleware ( csrfMiddleware ( DiscordUnlink ) ) )
hmnOnly . POST ( hmnurl . RegexDiscordShowcaseBacklog , authMiddleware ( csrfMiddleware ( DiscordShowcaseBacklog ) ) )
hmnOnly . GET ( hmnurl . RegexUserProfile , UserProfile )
hmnOnly . GET ( hmnurl . RegexUserSettings , authMiddleware ( UserSettings ) )
hmnOnly . POST ( hmnurl . RegexUserSettings , authMiddleware ( csrfMiddleware ( UserSettingsSave ) ) )
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 )
2021-11-25 03:59:51 +00:00
hmnOnly . POST ( hmnurl . RegexAPICheckUsername , csrfMiddleware ( APICheckUsername ) )
2021-10-27 00:45:11 +00:00
hmnOnly . GET ( hmnurl . RegexLibraryAny , LibraryNotPortedYet )
2021-08-28 10:40:13 +00:00
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 )
}
} )
2021-11-25 03:59:51 +00:00
rb . GET ( hmnurl . RegexProjectEdit , authMiddleware ( ProjectEdit ) )
rb . POST ( hmnurl . RegexProjectEdit , authMiddleware ( csrfMiddleware ( ProjectEditSubmit ) ) )
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
return authMiddleware ( h ) ( c )
}
}
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 )
2021-11-10 17:13:56 +00:00
rb . POST ( hmnurl . RegexForumMarkRead , authMiddleware ( 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
return authMiddleware ( h ) ( c )
}
}
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 )
} )
}
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)" ) )
}
2021-12-02 10:53:36 +00:00
p , err := FetchProject ( c . Context ( ) , c . Conn , c . CurrentUser , id , ProjectsQuery {
AlwaysVisibleToOwnerAndStaff : true ,
} )
2021-11-09 19:14:38 +00:00
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-11-10 04:51:28 +00:00
c . CurrentProject = & p . Project
2021-12-02 10:53:36 +00:00
c . UrlContext = UrlContextForProject ( & c . CurrentProject . Project )
2021-11-10 04:51:28 +00:00
2021-11-09 19:14:38 +00:00
if ! p . Project . Personal {
2021-11-10 04:51:28 +00:00
return c . Redirect ( c . UrlContext . RewriteProjectUrl ( c . URL ( ) ) , http . StatusSeeOther )
2021-11-09 19:14:38 +00:00
}
2021-11-10 17:34:48 +00:00
if c . PathParams [ "projectslug" ] != models . GeneratePersonalProjectSlug ( p . Project . Name ) {
2021-11-10 04:51:28 +00:00
return c . Redirect ( c . UrlContext . RewriteProjectUrl ( c . URL ( ) ) , http . StatusSeeOther )
2021-11-09 19:23:36 +00:00
}
2021-11-09 19:14:38 +00:00
return h ( c )
} )
}
attachProjectRoutes ( rb )
} )
2021-11-10 04:51:28 +00:00
anyProject . Group ( regexp . MustCompile ( "^" ) , func ( rb * RouteBuilder ) {
2021-11-09 19:14:38 +00:00
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 {
2021-11-10 04:51:28 +00:00
return c . Redirect ( c . UrlContext . RewriteProjectUrl ( c . URL ( ) ) , http . StatusSeeOther )
2021-11-09 19:14:38 +00:00
}
return h ( c )
} )
}
attachProjectRoutes ( rb )
2021-09-20 15:17:53 +00:00
} )
2021-07-30 19:59:48 +00:00
2021-10-27 00:45:11 +00:00
anyProject . POST ( hmnurl . RegexAssetUpload , AssetUpload )
2021-07-23 03:09:46 +00:00
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 )
anyProject . GET ( hmnurl . RegexEditorPreviewsJS , func ( c * RequestContext ) ResponseData {
2021-07-30 22:32:19 +00:00
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-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
}
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-11-08 19:16:54 +00:00
// get user
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-11-08 19:16:54 +00:00
// get official project
{
hostPrefix := strings . TrimSuffix ( c . Req . Host , hmnurl . GetBaseHost ( ) )
slug := strings . TrimRight ( hostPrefix , "." )
2021-12-04 14:55:45 +00:00
var owners [ ] * models . User
2021-11-08 19:16:54 +00:00
2021-12-02 10:53:36 +00:00
if len ( slug ) > 0 {
dbProject , err := FetchProjectBySlug ( c . Context ( ) , c . Conn , c . CurrentUser , slug , ProjectsQuery { AlwaysVisibleToOwnerAndStaff : true } )
if err == nil {
c . CurrentProject = & dbProject . Project
2021-12-04 14:55:45 +00:00
owners = dbProject . Owners
2021-11-08 19:16:54 +00:00
} else {
2021-12-02 10:53:36 +00:00
if errors . Is ( err , db . NotFound ) {
// do nothing, this is fine
} else {
return false , c . ErrorResponse ( http . StatusInternalServerError , oops . New ( err , "failed to fetch current project" ) )
}
2021-11-08 19:16:54 +00:00
}
}
2021-11-09 19:14:38 +00:00
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
2021-12-04 14:55:45 +00:00
owners = dbProject . Owners
2021-11-09 19:14:38 +00:00
}
if c . CurrentProject == nil {
panic ( "failed to load project data" )
}
2021-11-10 04:11:39 +00:00
2021-12-04 14:55:45 +00:00
canEditProject := false
if c . CurrentUser != nil {
canEditProject = c . CurrentUser . IsStaff
if ! canEditProject {
for _ , o := range owners {
if o . ID == c . CurrentUser . ID {
c . CurrentUserCanEditCurrentProject = true
break
}
}
}
}
c . CurrentUserCanEditCurrentProject = canEditProject
2021-12-02 10:53:36 +00:00
c . UrlContext = UrlContextForProject ( & c . CurrentProject . Project )
2021-11-08 19:16:54 +00:00
}
2021-11-10 04:11:39 +00:00
c . Theme = "light"
2021-05-25 13:12:20 +00:00
if c . CurrentUser != nil && c . CurrentUser . DarkTheme {
2021-11-10 04:11:39 +00:00
c . Theme = "dark"
2021-05-25 13:12:20 +00:00
}
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-12-04 14:55:45 +00:00
res . Header ( ) . Add ( "Access-Control-Allow-Credentials" , "true" )
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-11-11 23:59:05 +00:00
func TrackRequestPerf ( c * RequestContext ) ( 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
}