Start forum editing experience, including bbcode parser
This commit is contained in:
parent
582ad9ee9e
commit
00b0383030
|
@ -7462,6 +7462,24 @@ article code {
|
|||
.mh-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 {
|
||||
font-family: "Fira Sans", sans-serif; }
|
||||
|
||||
|
@ -7981,10 +7999,6 @@ header {
|
|||
pre {
|
||||
font-family: monospace; }
|
||||
|
||||
.post-edit {
|
||||
width: 90%;
|
||||
margin: auto; }
|
||||
|
||||
.toolbar {
|
||||
background-color: #fff;
|
||||
background-color: var(--editor-toolbar-background);
|
||||
|
@ -8019,27 +8033,6 @@ pre {
|
|||
border: 0px solid transparent;
|
||||
/* 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 {
|
||||
width: 95%;
|
||||
margin: 10px auto; }
|
||||
|
@ -8062,10 +8055,6 @@ pre {
|
|||
text-decoration: underline;
|
||||
font-style: italic; }
|
||||
|
||||
.editor .actionbar input[type=button] {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px; }
|
||||
|
||||
.edit-form .error {
|
||||
margin-left: 5em;
|
||||
padding: 10px;
|
||||
|
|
|
@ -31,6 +31,16 @@ func makeSessionId() string {
|
|||
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")
|
||||
|
||||
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(),
|
||||
Username: username,
|
||||
ExpiresAt: time.Now().Add(sessionDuration),
|
||||
CSRFToken: makeCSRFToken(),
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx,
|
||||
"INSERT INTO sessions (id, username, expires_at) VALUES ($1, $2, $3)",
|
||||
session.ID, session.Username, session.ExpiresAt,
|
||||
"INSERT INTO sessions (id, username, expires_at, csrf_token) VALUES ($1, $2, $3, $4)",
|
||||
session.ID, session.Username, session.ExpiresAt, session.CSRFToken,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, oops.New(err, "failed to persist session")
|
||||
|
|
|
@ -267,12 +267,16 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
|
|||
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()
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder.WriteString("/new")
|
||||
builder.WriteString("/t/new")
|
||||
if submit {
|
||||
builder.WriteString("/submit")
|
||||
}
|
||||
|
||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -6,4 +6,5 @@ type Session struct {
|
|||
ID string `db:"id"`
|
||||
Username string `db:"username"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
CSRFToken string `db:"csrf_token"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
}
|
|
@ -280,6 +280,30 @@ article code {
|
|||
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 {
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
.post-edit {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@include usevar('background-color', 'editor-toolbar-background');
|
||||
@include usevar('border-color', 'editor-toolbar-border-color');
|
||||
|
@ -40,39 +35,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.actionbar {
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width:95%;
|
||||
margin:10px auto;
|
||||
|
@ -111,11 +74,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.actionbar input[type=button] {
|
||||
margin-left:10px;
|
||||
margin-right:10px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
|
|
|
@ -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 {
|
||||
return Thread{
|
||||
Title: t.Title,
|
||||
|
|
|
@ -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 }}
|
|
@ -24,7 +24,7 @@
|
|||
{{/* TODO: Forgot password flow? Or just on standalone page? */}}
|
||||
</table>
|
||||
<input type="hidden" name="redirect" value="{{ $.CurrentUrl }}">
|
||||
<div class="actionbar pt2">
|
||||
<div class="pt2">
|
||||
<input type="submit" value="Log In" />
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -112,6 +112,9 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
"color2css": func(color noire.Color) template.CSS {
|
||||
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 {
|
||||
return color.Shade(amount)
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ type BaseData struct {
|
|||
|
||||
Project Project
|
||||
User *User
|
||||
Session *Session
|
||||
|
||||
IsProjectPage bool
|
||||
Header Header
|
||||
|
@ -147,6 +148,10 @@ type Link struct {
|
|||
Value string
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
CSRFToken string
|
||||
}
|
||||
|
||||
type OpenGraphItem struct {
|
||||
Property string
|
||||
Name string
|
||||
|
|
|
@ -260,7 +260,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
var res ResponseData
|
||||
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
||||
BaseData: baseData,
|
||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs),
|
||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
||||
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
|
||||
Threads: threads,
|
||||
Pagination: templates.Pagination{
|
||||
|
@ -515,6 +515,50 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
|
|||
), 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) {
|
||||
if project.ForumID == nil {
|
||||
return -1, false
|
||||
|
|
|
@ -47,29 +47,31 @@ func WrapStdHandler(h http.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)
|
||||
for _, method := range methods {
|
||||
rb.Router.Routes = append(rb.Router.Routes, Route{
|
||||
Method: method,
|
||||
Regex: regex,
|
||||
Handler: h,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
rb.Handle(http.MethodGet, regex, h)
|
||||
rb.Handle([]string{http.MethodGet}, regex, h)
|
||||
}
|
||||
|
||||
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) {
|
||||
rb.Handle("", regex, WrapStdHandler(h))
|
||||
rb.Handle([]string{""}, regex, WrapStdHandler(h))
|
||||
}
|
||||
|
||||
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
@ -122,6 +124,7 @@ type RequestContext struct {
|
|||
Conn *pgxpool.Pool
|
||||
CurrentProject *models.Project
|
||||
CurrentUser *models.User
|
||||
CurrentSession *models.Session
|
||||
Theme string
|
||||
|
||||
Perf *perf.RequestPerf
|
||||
|
|
|
@ -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.
|
||||
routes.POST(hmnurl.RegexLoginAction, Login)
|
||||
routes.GET(hmnurl.RegexLogoutAction, Logout)
|
||||
|
@ -113,6 +123,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile)
|
||||
|
||||
// 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.RegexForumCategory, ForumCategory)
|
||||
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
||||
|
@ -127,14 +139,12 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
|
||||
func getBaseData(c *RequestContext) templates.BaseData {
|
||||
var templateUser *templates.User
|
||||
var templateSession *templates.Session
|
||||
if c.CurrentUser != nil {
|
||||
templateUser = &templates.User{
|
||||
Username: c.CurrentUser.Username,
|
||||
Name: c.CurrentUser.Name,
|
||||
Email: c.CurrentUser.Email,
|
||||
IsSuperuser: c.CurrentUser.IsSuperuser,
|
||||
IsStaff: c.CurrentUser.IsStaff,
|
||||
}
|
||||
u := templates.UserToTemplate(c.CurrentUser, c.Theme)
|
||||
s := templates.SessionToTemplate(c.CurrentSession)
|
||||
templateUser = &u
|
||||
templateSession = &s
|
||||
}
|
||||
|
||||
return templates.BaseData{
|
||||
|
@ -146,6 +156,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
|
||||
Project: templates.ProjectToTemplate(c.CurrentProject, c.Theme),
|
||||
User: templateUser,
|
||||
Session: templateSession,
|
||||
|
||||
IsProjectPage: !c.CurrentProject.IsHMN(),
|
||||
Header: templates.Header{
|
||||
|
@ -293,12 +304,13 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
|
|||
{
|
||||
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
|
||||
if err == nil {
|
||||
user, err := getCurrentUser(c, sessionCookie.Value)
|
||||
user, session, err := getCurrentUserAndSession(c, sessionCookie.Value)
|
||||
if err != nil {
|
||||
return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user"))
|
||||
}
|
||||
|
||||
c.CurrentUser = user
|
||||
c.CurrentSession = session
|
||||
}
|
||||
// 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
|
||||
// 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)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrNoSession) {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
} 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 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")
|
||||
return nil, nil // user was deleted or something
|
||||
return nil, nil, nil // user was deleted or something
|
||||
} 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)
|
||||
|
||||
return user, nil
|
||||
return user, session, nil
|
||||
}
|
||||
|
||||
func TrackRequestPerf(c *RequestContext, perfCollector *perf.PerfCollector) (after func()) {
|
||||
|
|
Loading…
Reference in New Issue