Clean up TODOs

This commit is contained in:
Ben Visness 2021-08-28 12:07:45 -05:00
parent 57d4216d2d
commit bc39b4c0b7
35 changed files with 172 additions and 114 deletions

View File

@ -72,7 +72,6 @@ function switchTab(container, slug) {
for (const tab of tabs) {
const slugMatches = tab.getAttribute("data-slug") === slug;
tab.classList.toggle('dn', !slugMatches);
// TODO: Also update the tab button styles
if (slugMatches) {
didMatch = true;

View File

@ -1173,7 +1173,7 @@ img, video {
.br1 {
border-radius: 0.125rem; }
.br2, .hmn-code, .notice {
.br2, .hmn-code {
border-radius: 0.25rem; }
.br3 {
@ -1209,7 +1209,7 @@ img, video {
border-radius: 0; }
.br1-ns {
border-radius: 0.125rem; }
.br2-ns {
.br2-ns, .notice {
border-radius: 0.25rem; }
.br3-ns {
border-radius: 0.5rem; }
@ -4581,7 +4581,7 @@ code, .code {
.pa1 {
padding: 0.25rem; }
.pa2, header .menu-bar .items a, header .user-options a, .tab {
.pa2, header .menu-bar .items a, .tab {
padding: 0.5rem; }
.pa3, header #login-popup {
@ -7675,7 +7675,8 @@ header #login-popup {
.content p {
-moz-text-size-adjust: auto;
-webkit-text-size-adjust: auto;
text-size-adjust: auto; }
text-size-adjust: auto;
margin: 0.6rem 0; }
.content .description {
line-height: 1.42em;
text-align: left;
@ -8232,6 +8233,9 @@ nav.timecodes {
text-align: center;
margin: 10px 0; }
form {
margin: 0; }
.radio, .checkbox {
position: relative;
display: flex;

View File

@ -155,12 +155,13 @@ func (bot *botInstance) Run(ctx context.Context) (err error) {
for {
msg, err := bot.receiveGatewayMessage(ctx)
if err != nil {
// TODO: Are there other kinds of connection close events that we need to handle? Probably?
if errors.Is(err, net.ErrClosed) {
// If the connection is closed, that's our cue to shut down the bot. Any errors
// related to the closure will have been logged elsewhere anyway.
return nil
} else {
// NOTE(ben): I don't know what events we might get in the future that we might
// want to handle gracefully (like above). Keep an eye out.
return oops.New(err, "failed to receive message from the gateway")
}
}
@ -573,7 +574,7 @@ func (bot *botInstance) processEventMsg(ctx context.Context, msg *GatewayMessage
return nil
}
// TODO: Should this return an error? Or just log errors?
// Only return an error if we want to restart the bot.
func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message) error {
if msg.OriginalHasFields("author") && msg.Author.ID == config.Config.Discord.BotUserID {
// Don't process your own messages
@ -583,7 +584,8 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
if msg.ChannelID == config.Config.Discord.ShowcaseChannelID {
err := bot.processShowcaseMsg(ctx, msg)
if err != nil {
return oops.New(err, "failed to process showcase message")
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process showcase message")
return nil
}
return nil
}
@ -591,7 +593,8 @@ func (bot *botInstance) messageCreateOrUpdate(ctx context.Context, msg *Message)
if msg.ChannelID == config.Config.Discord.LibraryChannelID {
err := bot.processLibraryMsg(ctx, msg)
if err != nil {
return oops.New(err, "failed to process library message")
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to process library message")
return nil
}
return nil
}

View File

@ -137,7 +137,8 @@ func Scrape(ctx context.Context, dbConn *pgxpool.Pool, channelID string, earlies
Before: before,
})
if err != nil {
panic(err) // TODO
logging.Error().Err(err).Msg("failed to get messages while scraping")
return
}
if len(msgs) == 0 {

View File

@ -59,9 +59,6 @@ func (m *GatewayMessage) ToJSON() []byte {
panic(err)
}
// TODO: check if the payload is too big, either here or where we actually send
// https://discord.com/developers/docs/topics/gateway#sending-payloads
return mBytes
}
@ -70,7 +67,6 @@ type Hello struct {
}
func HelloFromMap(m interface{}) Hello {
// TODO: This should probably have some error handling, right?
return Hello{
HeartbeatIntervalMs: int(m.(map[string]interface{})["heartbeat_interval"].(float64)),
}

View File

@ -404,6 +404,8 @@ func saveAttachment(
return iDiscordAttachment.(*models.DiscordMessageAttachment), nil
}
// Saves an embed from Discord. NOTE: This is _not_ idempotent, so only call it
// if you do not have any embeds saved for this message yet.
func saveEmbed(
ctx context.Context,
tx db.ConnOrTx,
@ -411,9 +413,6 @@ func saveEmbed(
hmnUserID int,
discordMessageID string,
) (*models.DiscordMessageEmbed, error) {
// TODO: Does this need to be idempotent? Embeds don't have IDs...
// Maybe Discord will never actually send us the same embed twice?
isOkImageType := func(contentType string) bool {
return strings.HasPrefix(contentType, "image/")
}

View File

@ -308,7 +308,7 @@ func TestPublic(t *testing.T) {
}
func TestForumMarkRead(t *testing.T) {
AssertRegexMatch(t, BuildForumMarkRead(5), RegexForumMarkRead, map[string]string{"sfid": "5"})
AssertRegexMatch(t, BuildForumMarkRead(c.CurrentProject.Slug, 5), RegexForumMarkRead, map[string]string{"sfid": "5"})
}
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {

View File

@ -354,8 +354,8 @@ func BuildPodcastEpisodeFile(projectSlug string, filename string) string {
* Forums
*/
// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page)
// This shouldn't be a problem since we will match Thread before Subforum in the router, but should we enforce it here?
// NOTE(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page)
// Make sure to match Thread before Subforum in the router.
var RegexForum = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
func BuildForum(projectSlug string, subforums []string, page int) string {
@ -433,7 +433,6 @@ func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, po
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/reply$`)
// TODO: It's kinda weird that we have "replies" to a specific post. That's not how the data works. I guess this just affects what you see as the "post you're replying to" on the post composer page?
func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string {
defer CatchPanic()
builder := buildForumPostPath(subforums, threadId, postId)
@ -689,7 +688,7 @@ func BuildUserFile(filepath string) string {
var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P<sfid>\d+)$`)
// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS
func BuildForumMarkRead(subforumId int) string {
func BuildForumMarkRead(projectSlug string, subforumId int) string {
defer CatchPanic()
if subforumId < 0 {
panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId))
@ -699,7 +698,7 @@ func BuildForumMarkRead(subforumId int) string {
builder.WriteString("/markread/")
builder.WriteString(strconv.Itoa(subforumId))
return Url(builder.String(), nil)
return ProjectUrl(builder.String(), nil, projectSlug)
}
var RegexCatchAll = regexp.MustCompile("")

View File

@ -96,8 +96,6 @@ func NewPrettyZerologWriter() *PrettyZerologWriter {
}
func (w *PrettyZerologWriter) Write(p []byte) (int, error) {
// TODO: panic recovery so we log _something_
var fields map[string]interface{}
err := json.Unmarshal(p, &fields)
if err != nil {

View File

@ -8,10 +8,9 @@ import (
type Post struct {
ID int `db:"id"`
// TODO: Document each of these
AuthorID *int `db:"author_id"`
ThreadID int `db:"thread_id"`
CurrentID int `db:"current_id"`
CurrentID int `db:"current_id"` // The id of the current PostVersion
ProjectID int `db:"project_id"`
ThreadType ThreadType `db:"thread_type"`

View File

@ -21,7 +21,7 @@ import (
"github.com/yuin/goldmark/util"
)
var BBCodePriority = 1 // TODO: This is maybe too high a priority?
var BBCodePriority = 1
var reOpenTag = regexp.MustCompile(`^\[\s*(?P<name>[a-zA-Z0-9]+)`)
var reTag = regexp.MustCompile(`\[\s*(?P<opentagname>[a-zA-Z0-9]+)|\[\s*\/\s*(?P<closetagname>[a-zA-Z0-9]+)\s*\]`)

View File

@ -505,10 +505,6 @@ header {
.user-options {
position: relative;
a {
@extend .pa2;
}
}
.login, .register {
@ -571,6 +567,8 @@ footer {
-moz-text-size-adjust:auto;
-webkit-text-size-adjust:auto;
text-size-adjust:auto;
margin: 0.6rem 0;
}
.description {

View File

@ -1,3 +1,7 @@
form {
margin: 0;
}
.radio, .checkbox {
$size: 1.3rem;
$margin: 0.5rem;

View File

@ -1,7 +1,7 @@
.notice {
@include usevar(color, notice-text-color);
@extend .ph3, .pv2;
@extend .br2;
@extend .br2-ns;
a {
@include usevar(color, notice-text-color);

View File

@ -273,7 +273,7 @@ func TimelineItemsToJSON(items []TimelineItem) string {
builder.WriteString(`",`)
builder.WriteString(`"owner_name":"`)
builder.WriteString(item.OwnerName) // TODO: Do we need to do escaping on these other string fields too? Feels like someone could use this for XSS.
builder.WriteString(item.OwnerName)
builder.WriteString(`",`)
builder.WriteString(`"owner_avatar":"`)

View File

@ -93,6 +93,9 @@
{{ end }}
<a class="reply action button" href="{{ .ReplyUrl }}" title="Reply">&hookrightarrow;</a>&nbsp;
{{ end }}
<span class="postid">
<a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a>
</span>
</div>
{{ end }}
</div>

View File

@ -3,7 +3,6 @@
{{ define "extrahead" }}
{{/* TODO: These are no longer useful? */}}
<link rel="stylesheet" href="{{ static "editor.css" }}" />
<script src="{{ static "util.js" }}"></script>
<script src="{{ static "editor.js" }}"></script>
<script src="{{ static "go_wasm_exec.js" }}"></script>

View File

@ -34,13 +34,12 @@
<div class="dn db-l w-60 pv2">
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
</div>
{{/* TODO: Aggregate user data
<div class="c--dim f7">
{{ post.author.posts }} posts
{% if post.author.public_projects.values|length > 0 %}
/ {{ post.author.public_projects.values|length }} project{%if post.author.public_projects.values|length > 1 %}s{% endif %}
{% endif %}
</div> */}}
{{ .AuthorNumPosts }} posts
{{ if gt .AuthorNumProjects 0 }}
/ {{ .AuthorNumProjects }} project{{ if gt .AuthorNumProjects 1 }}s{{ end }}
{{ end }}
</div>
<!-- Large badges -->
<div class="dn db-l pv2">
{{ if .Author.IsStaff }}

View File

@ -6,7 +6,7 @@
</div>
<div class="pl3 flex flex-column">
<div>
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a> {{/* TODO: Text scale stuff? Seems unnecessary. */}}
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a>
<!-- Mobile badges -->
<div class="di ph1">
{{ if .Author.IsStaff }}

View File

@ -2,13 +2,16 @@
<div class="user-options flex justify-center justify-end-ns">
{{ if .User }}
{{ if .User.IsStaff }}
<a class="admin-panel" href="{{ .Header.AdminUrl }}"><span class="icon-settings"> Admin</span></a>
<a class="admin-panel pa2" href="{{ .Header.AdminUrl }}"><span class="icon-settings"> Admin</span></a>
{{ end }}
<a class="username settings" href="{{ .Header.UserSettingsUrl }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
<a class="logout" href="{{ .Header.LogoutActionUrl }}"><span class="icon-logout"></span> Log Out</a>
<div>
<a class="dib pv2 pl2" href="{{ .Header.UserProfileUrl }}">{{ .User.Username }}</a>
<a class="dib pv2 pr2" href="{{ .Header.UserSettingsUrl }}">(settings)</a>
</div>
<a class="logout pa2" href="{{ .Header.LogoutActionUrl }}"><span class="icon-logout"></span> Log Out</a>
{{ else }}
<a class="register" id="register-link" href="{{ .Header.RegisterUrl }}">Register</a>
<a class="login" id="login-link" href="{{ .LoginPageUrl }}">Log in</a>
<a class="register pa2" id="register-link" href="{{ .Header.RegisterUrl }}">Register</a>
<a class="login pa2" id="login-link" href="{{ .LoginPageUrl }}">Log in</a>
<div id="login-popup">
<form action="{{ .Header.LoginActionUrl }}" method="post" class="ma0">
<input type="text" name="username" class="w-100" value="" placeholder="Username" />
@ -58,7 +61,7 @@
</div>
<form onsubmit="this.querySelector('input[name=q]').value = this.querySelector('#searchstring').value + ' site:handmade.network';" class="dn ma0 flex-l flex-column justify-center items-end" method="GET" action="{{ .Header.SearchActionUrl }}" target="_blank">
<input type="hidden" name="q" />
<input class="site-search bn lite pa2 fira" type="text" id="searchstring" value="" placeholder="Search with DuckDuckGo" size="17" />
<input class="site-search bn lite pa2 fira" type="text" id="searchstring" value="" placeholder="Search with DuckDuckGo" size="18" />
<input id="search_button_homepage" type="submit" value="Go"/>
</form>
</div>

View File

@ -12,7 +12,7 @@
{{ end }}
{{ end }}
{{ if .Title }}
<title>{{ .Title }} | Handmade Network</title> {{/* TODO: Some parts of the site replace "Handmade Network" with other things like "4coder Forums". */}}
<title>{{ .Title }} | Handmade Network</title>
{{ else }}
<title>Handmade Network</title>
{{ end }}

View File

@ -47,7 +47,6 @@
</div>
</div>
{{ end }}
{{/* TODO(asaf): Add timeline items for project */}}
</div>
<div class="sidebar flex-shrink-0 mw6 w-30-l self-center self-start-l mh3 mh0-ns ml3-l overflow-hidden">
<div class="content-block">

View File

@ -1,7 +1,7 @@
{{ template "base.html" . }}
{{ define "content" }}
<div class="description">
<div class="description ph2 ph0-ns">
<p>
<span class="big">Hi there, {{ if .User }}{{ .User.Name }}{{ else }}visitor{{ end }}!</span>
</p>

View File

@ -39,6 +39,7 @@ func (bd *BaseData) AddImmediateNotice(class, content string) {
type Header struct {
AdminUrl string
UserProfileUrl string
UserSettingsUrl string
LoginActionUrl string
LogoutActionUrl string
@ -90,6 +91,9 @@ type Post struct {
Content template.HTML
PostDate time.Time
AuthorNumPosts int
AuthorNumProjects int
Editor *User
EditDate time.Time
EditReason string

View File

@ -204,7 +204,7 @@ func BlogPostRedirectToThread(c *RequestContext) ResponseData {
thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
threadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, cd.ThreadID, thread.Title)
threadUrl := hmnurl.BuildBlogThreadWithPostHash(c.CurrentProject.Slug, cd.ThreadID, thread.Title, cd.PostID)
return c.Redirect(threadUrl, http.StatusFound)
}

View File

@ -5,7 +5,6 @@ import (
"net/http"
"time"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/discord"
@ -22,25 +21,16 @@ func DiscordOAuthCallback(c *RequestContext) ResponseData {
if state != c.CurrentSession.CSRFToken {
// CSRF'd!!!!
// TODO(compression): Should this and the CSRF middleware be pulled out to
// a separate function?
c.Logger.Warn().Str("userId", c.CurrentUser.Username).Msg("user failed Discord OAuth state 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 Discord OAuth state failure")
}
res := c.Redirect("/", http.StatusSeeOther)
res.SetCookie(auth.DeleteSessionCookie)
logoutUser(c, &res)
return res
}
// Check for error values and redirect back to ????
// Check for error values and redirect back to user settings
if errCode := query.Get("error"); errCode != "" {
// TODO: actually handle these errors
if errCode == "access_denied" {
// This occurs when the user cancels. Just go back to the profile page.
return c.Redirect(hmnurl.BuildUserSettings("discord"), http.StatusSeeOther)
@ -49,7 +39,7 @@ func DiscordOAuthCallback(c *RequestContext) ResponseData {
}
}
// Do the actual token exchange and redirect back to ????
// Do the actual token exchange
code := query.Get("code")
res, err := discord.ExchangeOAuthCode(c.Context(), code, hmnurl.BuildDiscordOAuthCallback())
if err != nil {

View File

@ -98,7 +98,7 @@ func Feed(c *RequestContext) ResponseData {
BaseData: baseData,
AtomFeedUrl: hmnurl.BuildAtomFeed(),
MarkAllReadUrl: hmnurl.BuildForumMarkRead(0),
MarkAllReadUrl: hmnurl.BuildForumMarkRead(c.CurrentProject.Slug, 0),
Posts: posts,
Pagination: pagination,
}, c.Perf)

View File

@ -1,6 +1,7 @@
package website
import (
"context"
"fmt"
"net/http"
"strconv"
@ -9,6 +10,7 @@ import (
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
@ -283,7 +285,7 @@ func Forum(c *RequestContext) ResponseData {
res.MustWriteTemplate("forum.html", forumData{
BaseData: baseData,
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
MarkReadUrl: hmnurl.BuildForumMarkRead(cd.SubforumID),
MarkReadUrl: hmnurl.BuildForumMarkRead(c.CurrentProject.Slug, cd.SubforumID),
Threads: threads,
Pagination: templates.Pagination{
Current: page,
@ -470,6 +472,7 @@ func ForumThread(c *RequestContext) ResponseData {
)
c.Perf.EndBlock()
c.Perf.StartBlock("TEMPLATE", "Create template posts")
var posts []templates.Post
for _, p := range postsAndStuff {
post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
@ -482,8 +485,11 @@ func ForumThread(c *RequestContext) ResponseData {
post.ReplyPost = &reply
}
addAuthorCountsToPost(c.Context(), c.Conn, &post)
posts = append(posts, post)
}
c.Perf.EndBlock()
// Update thread last read info
if c.CurrentUser != nil {
@ -774,9 +780,11 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData {
defer tx.Rollback(c.Context())
c.Req.ParseForm()
// TODO(ben): Validation
unparsed := c.Req.Form.Get("body")
editReason := c.Req.Form.Get("editreason")
if unparsed == "" {
return RejectRequest(c, "You must provide a body for your post.")
}
CreatePostVersion(c.Context(), tx, cd.PostID, unparsed, c.Req.Host, editReason, &c.CurrentUser.ID)
@ -999,3 +1007,44 @@ func addForumUrlsToPost(p *templates.Post, projectSlug string, subforums []strin
p.EditUrl = hmnurl.BuildForumPostEdit(projectSlug, subforums, threadId, postId)
p.ReplyUrl = hmnurl.BuildForumPostReply(projectSlug, subforums, threadId, postId)
}
func addAuthorCountsToPost(ctx context.Context, conn db.ConnOrTx, p *templates.Post) {
numPosts, err := db.QueryInt(ctx, conn,
`
SELECT COUNT(*)
FROM
handmade_post AS post
JOIN handmade_project AS project ON post.project_id = project.id
WHERE
post.author_id = $1
AND NOT post.deleted
AND project.lifecycle = ANY ($2)
`,
p.Author.ID,
models.VisibleProjectLifecycles,
)
if err != nil {
logging.ExtractLogger(ctx).Warn().Err(err).Msg("failed to get count of user posts")
} else {
p.AuthorNumPosts = numPosts
}
numProjects, err := db.QueryInt(ctx, conn,
`
SELECT COUNT(*)
FROM
handmade_project AS project
JOIN handmade_user_projects AS uproj ON uproj.project_id = project.id
WHERE
project.lifecycle = ANY ($1)
AND uproj.user_id = $2
`,
models.VisibleProjectLifecycles,
p.Author.ID,
)
if err != nil {
logging.ExtractLogger(ctx).Warn().Err(err).Msg("failed to get count of user projects")
} else {
p.AuthorNumProjects = numProjects
}
}

View File

@ -14,38 +14,49 @@ import (
"git.handmade.network/hmn/hmn/src/oops"
)
// If a helper method returns this, you should call RejectRequest with the value.
type RejectRequestError error
type SaveImageFileResult struct {
ImageFileID int
ValidationError string
FatalError error
}
/*
Reads an image file from form data and saves it to the filesystem and the database.
If the file doesn't exist, this does nothing.
If the file doesn't exist, this does nothing and returns 0 for the image file id.
NOTE(ben): Someday we should replace this with the asset system.
*/
func SaveImageFile(c *RequestContext, dbConn db.ConnOrTx, fileFieldName string, maxSize int64, filepath string) (imageFileId int, err error) {
func SaveImageFile(c *RequestContext, dbConn db.ConnOrTx, fileFieldName string, maxSize int64, filepath string) SaveImageFileResult {
img, header, err := c.Req.FormFile(fileFieldName)
filename := ""
width := 0
height := 0
if err != nil && !errors.Is(err, http.ErrMissingFile) {
return 0, oops.New(err, "failed to read uploaded file")
return SaveImageFileResult{
FatalError: oops.New(err, "failed to read uploaded file"),
}
}
if header != nil {
if header.Size > maxSize {
return 0, RejectRequestError(fmt.Errorf("Image filesize too big. Max size: %d bytes", maxSize))
return SaveImageFileResult{
ValidationError: fmt.Sprintf("Image filesize too big. Max size: %d bytes", maxSize),
}
} else {
c.Perf.StartBlock("IMAGE", "Decoding image")
config, format, err := image.DecodeConfig(img)
c.Perf.EndBlock()
if err != nil {
return 0, RejectRequestError(errors.New("Image type not supported"))
return SaveImageFileResult{
ValidationError: "Image type not supported",
}
}
width = config.Width
height = config.Height
if width == 0 || height == 0 {
return 0, RejectRequestError(errors.New("Image has zero size"))
return SaveImageFileResult{
ValidationError: "Image has zero size",
}
}
filename = fmt.Sprintf("%s.%s", filepath, format)
@ -53,12 +64,16 @@ func SaveImageFile(c *RequestContext, dbConn db.ConnOrTx, fileFieldName string,
c.Perf.StartBlock("IMAGE", "Writing image file")
file, err := os.Create(storageFilename)
if err != nil {
return 0, oops.New(err, "Failed to create local image file")
return SaveImageFileResult{
FatalError: oops.New(err, "Failed to create local image file"),
}
}
img.Seek(0, io.SeekStart)
_, err = io.Copy(file, img)
if err != nil {
return 0, oops.New(err, "Failed to write image to file")
return SaveImageFileResult{
FatalError: oops.New(err, "Failed to write image to file"),
}
}
file.Close()
img.Close()
@ -83,11 +98,15 @@ func SaveImageFile(c *RequestContext, dbConn db.ConnOrTx, fileFieldName string,
filename, header.Size, hex.EncodeToString(sha1sum), false, width, height,
).Scan(&imageId)
if err != nil {
return 0, oops.New(err, "Failed to insert image file row")
return SaveImageFileResult{
FatalError: oops.New(err, "Failed to insert image file row"),
}
}
return imageId, nil
return SaveImageFileResult{
ImageFileID: imageId,
}
}
return 0, nil
return SaveImageFileResult{}
}

View File

@ -334,7 +334,7 @@ func Index(c *RequestContext) ResponseData {
Url: hmnurl.BuildBlogThread(models.HMNProjectSlug, newsPostResult.Thread.ID, newsPostResult.Thread.Title),
User: templates.UserToTemplate(&newsPostResult.User, c.Theme),
Date: newsPostResult.Post.PostDate,
Unread: true, // TODO
Unread: true,
Content: template.HTML(newsPostResult.PostVersion.TextParsed),
},
PostColumns: cols,

View File

@ -147,17 +147,14 @@ func PodcastEditSubmit(c *RequestContext) ResponseData {
}
defer tx.Rollback(c.Context())
imageId, err := SaveImageFile(c, tx, "podcast_image", maxFileSize, fmt.Sprintf("podcast/%s/logo%d", c.CurrentProject.Slug, time.Now().UTC().Unix()))
if err != nil {
var rejectErr RejectRequestError
if errors.As(err, &rejectErr) {
return RejectRequest(c, rejectErr.Error())
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to save podcast image"))
}
imageSaveResult := SaveImageFile(c, tx, "podcast_image", maxFileSize, fmt.Sprintf("podcast/%s/logo%d", c.CurrentProject.Slug, time.Now().UTC().Unix()))
if imageSaveResult.ValidationError != "" {
return RejectRequest(c, imageSaveResult.ValidationError)
} else if imageSaveResult.FatalError != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(imageSaveResult.FatalError, "Failed to save podcast image"))
}
if imageId != 0 {
if imageSaveResult.ImageFileID != 0 {
_, err = tx.Exec(c.Context(),
`
UPDATE handmade_podcast
@ -169,7 +166,7 @@ func PodcastEditSubmit(c *RequestContext) ResponseData {
`,
title,
description,
imageId,
imageSaveResult.ImageFileID,
podcastResult.Podcast.ID,
)
if err != nil {

View File

@ -107,8 +107,6 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
// TODO(asaf): Replace this with a 404 handler? Isn't this going to crash the server?
// We're doing panic recovery in doRequest, but not here. I don't think we should have this line in production.
panic(fmt.Sprintf("Path '%s' did not match any routes! Make sure to register a wildcard route to act as a 404.", path))
}

View File

@ -112,13 +112,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
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)
logoutUser(c, &res)
return res
}
@ -275,7 +270,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
episodeGuideUrl = hmnurl.BuildEpisodeList(c.CurrentProject.Slug, defaultTopic)
}
return templates.BaseData{
baseData := templates.BaseData{
Theme: c.Theme,
CurrentUrl: c.FullUrl(),
@ -322,6 +317,12 @@ func getBaseData(c *RequestContext) templates.BaseData {
SitemapUrl: hmnurl.BuildSiteMap(),
},
}
if c.CurrentUser != nil {
baseData.Header.UserProfileUrl = hmnurl.BuildUserProfile(c.CurrentUser.Username)
}
return baseData
}
func buildDefaultOpenGraphItems(project *models.Project) []templates.OpenGraphItem {

View File

@ -454,27 +454,24 @@ func UserSettingsSave(c *RequestContext) ResponseData {
}
// Update avatar
_, err = SaveImageFile(c, tx, "avatar", 1*1024*1024, fmt.Sprintf("members/avatars/%s", c.CurrentUser.Username))
if err != nil {
var rejectErr RejectRequestError
if errors.As(err, &rejectErr) {
return RejectRequest(c, rejectErr.Error())
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save new avatar"))
}
imageSaveResult := SaveImageFile(c, tx, "avatar", 1*1024*1024, fmt.Sprintf("members/avatars/%s", c.CurrentUser.Username))
if imageSaveResult.ValidationError != "" {
return RejectRequest(c, imageSaveResult.ValidationError)
} else if imageSaveResult.FatalError != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(imageSaveResult.FatalError, "failed to save new avatar"))
}
// TODO: Success message
err = tx.Commit(c.Context())
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to save user settings"))
}
return c.Redirect(hmnurl.BuildUserSettings(""), http.StatusSeeOther)
res := c.Redirect(hmnurl.BuildUserSettings(""), http.StatusSeeOther)
res.AddFutureNotice("success", "User profile updated.")
return res
}
// TODO: Rework this to use that RejectRequestError thing
func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData {
if new != confirm {
res := RejectRequest(c, "Your password and password confirmation did not match.")