Start forum editing experience, including bbcode parser

This commit is contained in:
Ben Visness 2021-06-11 22:51:07 -05:00
parent 582ad9ee9e
commit 00b0383030
17 changed files with 413 additions and 102 deletions

View File

@ -7462,6 +7462,24 @@ article code {
.mh-80vh { .mh-80vh {
max-height: 80vh; } max-height: 80vh; }
.minw-100 {
min-width: 100%; }
.minh-1 {
min-height: 1rem; }
.minh-2 {
min-height: 2rem; }
.minh-3 {
min-height: 4rem; }
.minh-4 {
min-height: 8rem; }
.minh-5 {
min-height: 16rem; }
.fira { .fira {
font-family: "Fira Sans", sans-serif; } font-family: "Fira Sans", sans-serif; }
@ -7981,10 +7999,6 @@ header {
pre { pre {
font-family: monospace; } font-family: monospace; }
.post-edit {
width: 90%;
margin: auto; }
.toolbar { .toolbar {
background-color: #fff; background-color: #fff;
background-color: var(--editor-toolbar-background); background-color: var(--editor-toolbar-background);
@ -8019,27 +8033,6 @@ pre {
border: 0px solid transparent; border: 0px solid transparent;
/* Not themed */ } /* Not themed */ }
.actionbar {
text-align: center; }
.editor .body {
width: 100%;
font-size: 13pt;
height: 25em; }
.editor .title-edit {
width: 100%; }
.editor .title-edit label {
font-weight: bold; }
.editor .title-edit input {
width: 100%; }
.editor .editreason label {
font-weight: bold; }
.editor .editreason input {
width: 100%; }
.editor .toolbar { .editor .toolbar {
width: 95%; width: 95%;
margin: 10px auto; } margin: 10px auto; }
@ -8062,10 +8055,6 @@ pre {
text-decoration: underline; text-decoration: underline;
font-style: italic; } font-style: italic; }
.editor .actionbar input[type=button] {
margin-left: 10px;
margin-right: 10px; }
.edit-form .error { .edit-form .error {
margin-left: 5em; margin-left: 5em;
padding: 10px; padding: 10px;

View File

@ -31,6 +31,16 @@ func makeSessionId() string {
return base64.StdEncoding.EncodeToString(idBytes)[:40] return base64.StdEncoding.EncodeToString(idBytes)[:40]
} }
func makeCSRFToken() string {
idBytes := make([]byte, 30)
_, err := io.ReadFull(rand.Reader, idBytes)
if err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(idBytes)[:30]
}
var ErrNoSession = errors.New("no session found") var ErrNoSession = errors.New("no session found")
func GetSession(ctx context.Context, conn *pgxpool.Pool, id string) (*models.Session, error) { func GetSession(ctx context.Context, conn *pgxpool.Pool, id string) (*models.Session, error) {
@ -52,11 +62,12 @@ func CreateSession(ctx context.Context, conn *pgxpool.Pool, username string) (*m
ID: makeSessionId(), ID: makeSessionId(),
Username: username, Username: username,
ExpiresAt: time.Now().Add(sessionDuration), ExpiresAt: time.Now().Add(sessionDuration),
CSRFToken: makeCSRFToken(),
} }
_, err := conn.Exec(ctx, _, err := conn.Exec(ctx,
"INSERT INTO sessions (id, username, expires_at) VALUES ($1, $2, $3)", "INSERT INTO sessions (id, username, expires_at, csrf_token) VALUES ($1, $2, $3, $4)",
session.ID, session.Username, session.ExpiresAt, session.ID, session.Username, session.ExpiresAt, session.CSRFToken,
) )
if err != nil { if err != nil {
return nil, oops.New(err, "failed to persist session") return nil, oops.New(err, "failed to persist session")

View File

@ -267,12 +267,16 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/new?$`) var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new$`)
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
func BuildForumNewThread(projectSlug string, subforums []string) string { func BuildForumNewThread(projectSlug string, subforums []string, submit bool) string {
defer CatchPanic() defer CatchPanic()
builder := buildForumCategoryPath(subforums) builder := buildForumCategoryPath(subforums)
builder.WriteString("/new") builder.WriteString("/t/new")
if submit {
builder.WriteString("/submit")
}
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }

View File

@ -0,0 +1,44 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4"
)
func init() {
registerMigration(AddCSRFToken{})
}
type AddCSRFToken struct{}
func (m AddCSRFToken) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 6, 12, 1, 42, 31, 0, time.UTC))
}
func (m AddCSRFToken) Name() string {
return "AddCSRFToken"
}
func (m AddCSRFToken) Description() string {
return "Adds a CSRF token to user sessions"
}
func (m AddCSRFToken) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
ALTER TABLE sessions
ADD csrf_token VARCHAR(30) NOT NULL;
`)
if err != nil {
return oops.New(err, "failed to add CSRF token column")
}
return nil
}
func (m AddCSRFToken) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me")
}

View File

@ -6,4 +6,5 @@ type Session struct {
ID string `db:"id"` ID string `db:"id"`
Username string `db:"username"` Username string `db:"username"`
ExpiresAt time.Time `db:"expires_at"` ExpiresAt time.Time `db:"expires_at"`
CSRFToken string `db:"csrf_token"`
} }

87
src/parsing/parsing.go Normal file
View File

@ -0,0 +1,87 @@
package parsing
import (
"regexp"
"sort"
)
func StripNames(regexStr string) string {
return regexp.MustCompile(`\(\?P<[a-zA-Z0-9_]+>`).ReplaceAllString(regexStr, `(?:`)
}
var reArgStr = `(?P<name>[a-zA-Z0-9]+)(?:\s*=\s*(?:'(?P<single_quoted_val>.*?)'|"(?P<double_quoted_val>.*?)"|(?P<bare_val>[^\s\]]+)))?`
var reTagOpenStr = `\[\s*(?P<args>(?:` + StripNames(reArgStr) + `)(?:\s+(?:` + StripNames(reArgStr) + `))*)\s*\]`
var reTagCloseStr = `\[/\s*(?P<name>[a-zA-Z0-9]+)?\s*\]`
var reArg = regexp.MustCompile(reArgStr)
var reTagOpen = regexp.MustCompile(reTagOpenStr)
var reTagClose = regexp.MustCompile(reTagCloseStr)
const tokenTypeString = "string"
const tokenTypeOpenTag = "openTag"
const tokenTypeCloseTag = "closeTag"
type token struct {
Type string
StartIndex int
EndIndex int
Contents string
}
func ParseBBCode(input string) string {
return input
}
func tokenizeBBCode(input string) []token {
openMatches := reTagOpen.FindAllStringIndex(input, -1)
closeMatches := reTagClose.FindAllStringIndex(input, -1)
// Build tokens from regex matches
var tagTokens []token
for _, match := range openMatches {
tagTokens = append(tagTokens, token{
Type: tokenTypeOpenTag,
StartIndex: match[0],
EndIndex: match[1],
Contents: input[match[0]:match[1]],
})
}
for _, match := range closeMatches {
tagTokens = append(tagTokens, token{
Type: tokenTypeCloseTag,
StartIndex: match[0],
EndIndex: match[1],
Contents: input[match[0]:match[1]],
})
}
// Sort those tokens together
sort.Slice(tagTokens, func(i, j int) bool {
return tagTokens[i].StartIndex < tagTokens[j].StartIndex
})
// Make a new list of tokens that fills in the gaps with plain old text
var tokens []token
for i, tagToken := range tagTokens {
prevEnd := 0
if i > 0 {
prevEnd = tagTokens[i-1].EndIndex
}
tokens = append(tokens, token{
Type: tokenTypeString,
StartIndex: prevEnd,
EndIndex: tagToken.StartIndex,
Contents: input[prevEnd:tagToken.StartIndex],
})
tokens = append(tokens, tagToken)
}
tokens = append(tokens, token{
Type: tokenTypeString,
StartIndex: tokens[len(tokens)-1].EndIndex,
EndIndex: len(input),
Contents: input[tokens[len(tokens)-1].EndIndex:],
})
return tokens
}

View File

@ -0,0 +1,26 @@
package parsing
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseBBCode(t *testing.T) {
const testDoc = `Hello, [b]amazing[/b] [i]incredible[/i] [b][i][u]world!!![/u][/i][/b]
Too many opening tags: [b]wow.[b]
Too many closing tags: [/i]wow.[/i]
Mix 'em: [u][/i]wow.[/i][/u]
[url=https://google.com/]Google![/url]
`
t.Run("hello world", func(t *testing.T) {
bbcode := "Hello, [b]amazing[/b] [i]incredible[/i] [b][i][u]world!!![/u][/i][/b]"
expected := "Hello, <strong>amazing</strong> <em>incredible</em> <strong><em><u>world!!!</u></em></strong>"
assert.Equal(t, expected, ParseBBCode(bbcode))
})
}

View File

@ -280,6 +280,30 @@ article code {
max-height: 80vh; max-height: 80vh;
} }
.minw-100 {
min-width: 100%;
}
.minh-1 {
min-height: $height-1;
}
.minh-2 {
min-height: $height-2;
}
.minh-3 {
min-height: $height-3;
}
.minh-4 {
min-height: $height-4;
}
.minh-5 {
min-height: $height-5;
}
.fira { .fira {
font-family: "Fira Sans", sans-serif; font-family: "Fira Sans", sans-serif;
} }

View File

@ -1,8 +1,3 @@
.post-edit {
width:90%;
margin:auto;
}
.toolbar { .toolbar {
@include usevar('background-color', 'editor-toolbar-background'); @include usevar('background-color', 'editor-toolbar-background');
@include usevar('border-color', 'editor-toolbar-border-color'); @include usevar('border-color', 'editor-toolbar-border-color');
@ -40,39 +35,7 @@
} }
} }
.actionbar {
text-align:center;
}
.editor { .editor {
.body {
width:100%;
font-size:13pt;
height:25em;
}
.title-edit {
width:100%;
label {
font-weight:bold;
}
input {
width:100%;
}
}
.editreason {
label {
font-weight:bold;
}
input {
width:100%;
}
}
.toolbar { .toolbar {
width:95%; width:95%;
margin:10px auto; margin:10px auto;
@ -111,11 +74,6 @@
font-style: italic; font-style: italic;
} }
} }
.actionbar input[type=button] {
margin-left:10px;
margin-right:10px;
}
} }
.edit-form { .edit-form {

View File

@ -115,6 +115,12 @@ func ProjectToTemplate(p *models.Project, theme string) Project {
} }
} }
func SessionToTemplate(s *models.Session) Session {
return Session{
CSRFToken: s.CSRFToken,
}
}
func ThreadToTemplate(t *models.Thread) Thread { func ThreadToTemplate(t *models.Thread) Thread {
return Thread{ return Thread{
Title: t.Title, Title: t.Title,

View File

@ -0,0 +1,94 @@
{{ template "base.html" . }}
{{ define "extrahead" }}
<link rel="stylesheet" href="{{ static "editor.css" }}" />
<script type="text/javascript" src="{{ static "util.js" }}"></script>
<script type="text/javascript" src="{{ static "editor.js" }}"></script>
{{ end }}
{{ define "content" }}
<div class="content-block">
<form action="{{ .SubmitUrl }}" method="post">
{{ csrftoken .Session }}
<input class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .PostTitle }}"/>
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
{{/*
<div class="toolbar" id="toolbar">
<input type="button" id="bold" value="B" />
<input type="button" id="italic" value="I" />
<input type="button" id="underline" value="U" />
<input type="button" id="monospace" value="monospace" />
<input type="button" id="url" value="url" />
<input type="button" id="img" value="img" />
<input type="button" id="code" value="code" />
<input type="button" id="quote_simple" value="quote (anon)" />
<input type="button" id="quote_member" value="quote (member)" />
<input type="button" id="spoiler" value="spoiler" />
<input type="button" id="lalign" value="Left" />
<input type="button" id="calign" value="Center" />
<input type="button" id="ralign" value="Right" />
<input type="button" id="ulist" value="ul" />
<input type="button" id="olist" value="ol" />
<input type="button" id="litem" value="li" />
<input type="button" id="youtube" value="youtube" />
</div>
*/}}
<textarea class="w-100 minw-100 mw-100 h5 minh-5" name="body">{{ .PostBody }}</textarea>
<div class="flex flex-row-reverse justify-start mt2">
<input type="submit" class="button ml2" name="submit" value="{{ .SubmitLabel }}" />
<input type="submit" class="button ml2" name="preview" value="{{ .PreviewLabel }}" />
</div>
{{/*
{% if are_editing %}
<span class="editreason">
<label for="editreason">Edit reason:</label>
<input name="editreason" maxlength="255" type="text" id="editreason" value="{% if preview_edit_reason|length > 0 %}{{preview_edit_reason}}{% endif %}"/>
</span>
{% endif %}
{% if user.is_staff and post and post.depth == 0 %}
<div class="checkbox sticky">
<input type="checkbox" name="sticky" id="sticky" {% if thread.sticky %}checked{% endif%} />
<label for="sticky">Sticky thread</label>
</div>
{% endif %}
{% if are_previewing %}
{% include "edit_preview.html" %}
{% endif %}
{% if context_reply_to %}
<h4>The post you're replying to:</h4>
<div class="recent-posts">
{% with post=post_reply_to %}
{% include "forum_thread_single_post.html" %}
{% endwith %}
</div>
{% endif %}
{% if context_newer %}
<h4>Replies since then:</h4>
<div class="recent-posts">
{% for post in posts_newer %}
{% include "forum_thread_single_post.html" %}
{% endfor %}
</div>
{% endif %}
{% if context_older %}
<h4>Replies before then:</h4>
<div class="recent-posts">
{% for post in posts_older %}
{% include "forum_thread_single_post.html" %}
{% endfor %}
</div>
{% endif %}
*/}}
</form>
</div>
{{ end }}

View File

@ -24,7 +24,7 @@
{{/* TODO: Forgot password flow? Or just on standalone page? */}} {{/* TODO: Forgot password flow? Or just on standalone page? */}}
</table> </table>
<input type="hidden" name="redirect" value="{{ $.CurrentUrl }}"> <input type="hidden" name="redirect" value="{{ $.CurrentUrl }}">
<div class="actionbar pt2"> <div class="pt2">
<input type="submit" value="Log In" /> <input type="submit" value="Log In" />
</div> </div>
</form> </form>

View File

@ -112,6 +112,9 @@ var HMNTemplateFuncs = template.FuncMap{
"color2css": func(color noire.Color) template.CSS { "color2css": func(color noire.Color) template.CSS {
return template.CSS(color.HTML()) return template.CSS(color.HTML())
}, },
"csrftoken": func(s Session) template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="csrf_token" value="%s">`, s.CSRFToken))
},
"darken": func(amount float64, color noire.Color) noire.Color { "darken": func(amount float64, color noire.Color) noire.Color {
return color.Shade(amount) return color.Shade(amount)
}, },

View File

@ -20,6 +20,7 @@ type BaseData struct {
Project Project Project Project
User *User User *User
Session *Session
IsProjectPage bool IsProjectPage bool
Header Header Header Header
@ -147,6 +148,10 @@ type Link struct {
Value string Value string
} }
type Session struct {
CSRFToken string
}
type OpenGraphItem struct { type OpenGraphItem struct {
Property string Property string
Name string Name string

View File

@ -260,7 +260,7 @@ func ForumCategory(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
err = res.WriteTemplate("forum_category.html", forumCategoryData{ err = res.WriteTemplate("forum_category.html", forumCategoryData{
BaseData: baseData, BaseData: baseData,
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs), NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId), MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
Threads: threads, Threads: threads,
Pagination: templates.Pagination{ Pagination: templates.Pagination{
@ -515,6 +515,50 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
), http.StatusSeeOther) ), http.StatusSeeOther)
} }
type editorData struct {
templates.BaseData
SubmitUrl string
PostTitle string
PostBody string
SubmitLabel string
PreviewLabel string
}
func ForumNewThread(c *RequestContext) ResponseData {
if c.Req.Method == http.MethodPost {
// TODO: Get preview data
}
baseData := getBaseData(c)
baseData.Title = "Create New Thread"
// TODO(ben): Set breadcrumbs
var res ResponseData
err := res.WriteTemplate("editor.html", editorData{
BaseData: baseData,
SubmitLabel: "Post New Thread",
PreviewLabel: "Preview",
}, c.Perf)
// err := res.WriteTemplate("forum_thread.html", forumThreadData{
// BaseData: baseData,
// Thread: templates.ThreadToTemplate(&thread),
// Posts: posts,
// CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
// ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
// Pagination: pagination,
// }, c.Perf)
if err != nil {
panic(err)
}
return res
}
func ForumNewThreadSubmit(c *RequestContext) ResponseData {
var res ResponseData
return res
}
func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) { func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) {
if project.ForumID == nil { if project.ForumID == nil {
return -1, false return -1, false

View File

@ -47,29 +47,31 @@ func WrapStdHandler(h http.Handler) Handler {
type Middleware func(h Handler) Handler type Middleware func(h Handler) Handler
func (rb *RouteBuilder) Handle(method string, regex *regexp.Regexp, h Handler) { func (rb *RouteBuilder) Handle(methods []string, regex *regexp.Regexp, h Handler) {
h = rb.Middleware(h) h = rb.Middleware(h)
rb.Router.Routes = append(rb.Router.Routes, Route{ for _, method := range methods {
Method: method, rb.Router.Routes = append(rb.Router.Routes, Route{
Regex: regex, Method: method,
Handler: h, Regex: regex,
}) Handler: h,
})
}
} }
func (rb *RouteBuilder) AnyMethod(regex *regexp.Regexp, h Handler) { func (rb *RouteBuilder) AnyMethod(regex *regexp.Regexp, h Handler) {
rb.Handle("", regex, h) rb.Handle([]string{""}, regex, h)
} }
func (rb *RouteBuilder) GET(regex *regexp.Regexp, h Handler) { func (rb *RouteBuilder) GET(regex *regexp.Regexp, h Handler) {
rb.Handle(http.MethodGet, regex, h) rb.Handle([]string{http.MethodGet}, regex, h)
} }
func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) { func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) {
rb.Handle(http.MethodPost, regex, h) rb.Handle([]string{http.MethodPost}, regex, h)
} }
func (rb *RouteBuilder) StdHandler(regex *regexp.Regexp, h http.Handler) { func (rb *RouteBuilder) StdHandler(regex *regexp.Regexp, h http.Handler) {
rb.Handle("", regex, WrapStdHandler(h)) rb.Handle([]string{""}, regex, WrapStdHandler(h))
} }
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -122,6 +124,7 @@ type RequestContext struct {
Conn *pgxpool.Pool Conn *pgxpool.Pool
CurrentProject *models.Project CurrentProject *models.Project
CurrentUser *models.User CurrentUser *models.User
CurrentSession *models.Session
Theme string Theme string
Perf *perf.RequestPerf Perf *perf.RequestPerf

View File

@ -80,6 +80,16 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
} }
} }
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)
}
}
// TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware. // TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware.
routes.POST(hmnurl.RegexLoginAction, Login) routes.POST(hmnurl.RegexLoginAction, Login)
routes.GET(hmnurl.RegexLogoutAction, Logout) routes.GET(hmnurl.RegexLogoutAction, Logout)
@ -113,6 +123,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile) mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
// NOTE(asaf): Any-project routes: // NOTE(asaf): Any-project routes:
mainRoutes.Handle([]string{http.MethodGet, http.MethodPost}, hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(ForumNewThreadSubmit))
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread) mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory) mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect) mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
@ -127,14 +139,12 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
func getBaseData(c *RequestContext) templates.BaseData { func getBaseData(c *RequestContext) templates.BaseData {
var templateUser *templates.User var templateUser *templates.User
var templateSession *templates.Session
if c.CurrentUser != nil { if c.CurrentUser != nil {
templateUser = &templates.User{ u := templates.UserToTemplate(c.CurrentUser, c.Theme)
Username: c.CurrentUser.Username, s := templates.SessionToTemplate(c.CurrentSession)
Name: c.CurrentUser.Name, templateUser = &u
Email: c.CurrentUser.Email, templateSession = &s
IsSuperuser: c.CurrentUser.IsSuperuser,
IsStaff: c.CurrentUser.IsStaff,
}
} }
return templates.BaseData{ return templates.BaseData{
@ -146,6 +156,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
Project: templates.ProjectToTemplate(c.CurrentProject, c.Theme), Project: templates.ProjectToTemplate(c.CurrentProject, c.Theme),
User: templateUser, User: templateUser,
Session: templateSession,
IsProjectPage: !c.CurrentProject.IsHMN(), IsProjectPage: !c.CurrentProject.IsHMN(),
Header: templates.Header{ Header: templates.Header{
@ -293,12 +304,13 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
{ {
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName) sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
if err == nil { if err == nil {
user, err := getCurrentUser(c, sessionCookie.Value) user, session, err := getCurrentUserAndSession(c, sessionCookie.Value)
if err != nil { if err != nil {
return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user")) return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user"))
} }
c.CurrentUser = user c.CurrentUser = user
c.CurrentSession = session
} }
// http.ErrNoCookie is the only error Cookie ever returns, so no further handling to do here. // http.ErrNoCookie is the only error Cookie ever returns, so no further handling to do here.
} }
@ -315,13 +327,13 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
// Given a session id, fetches user data from the database. Will return nil if // 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. // the user cannot be found, and will only return an error if it's serious.
func getCurrentUser(c *RequestContext, sessionId string) (*models.User, error) { func getCurrentUserAndSession(c *RequestContext, sessionId string) (*models.User, *models.Session, error) {
session, err := auth.GetSession(c.Context(), c.Conn, sessionId) session, err := auth.GetSession(c.Context(), c.Conn, sessionId)
if err != nil { if err != nil {
if errors.Is(err, auth.ErrNoSession) { if errors.Is(err, auth.ErrNoSession) {
return nil, nil return nil, nil, nil
} else { } else {
return nil, oops.New(err, "failed to get current session") return nil, nil, oops.New(err, "failed to get current session")
} }
} }
@ -329,14 +341,14 @@ func getCurrentUser(c *RequestContext, sessionId string) (*models.User, error) {
if err != nil { if err != nil {
if errors.Is(err, db.ErrNoMatchingRows) { 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") logging.Debug().Str("username", session.Username).Msg("returning no current user for this request because the user for the session couldn't be found")
return nil, nil // user was deleted or something return nil, nil, nil // user was deleted or something
} else { } else {
return nil, oops.New(err, "failed to get user for session") return nil, nil, oops.New(err, "failed to get user for session")
} }
} }
user := userRow.(*models.User) user := userRow.(*models.User)
return user, nil return user, session, nil
} }
func TrackRequestPerf(c *RequestContext, perfCollector *perf.PerfCollector) (after func()) { func TrackRequestPerf(c *RequestContext, perfCollector *perf.PerfCollector) (after func()) {