summaryrefslogtreecommitdiff
path: root/srv/src/api
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2022-05-17 15:54:20 -0600
committerBrian Picciano <mediocregopher@gmail.com>2022-05-17 15:54:20 -0600
commit69de76cb32cfd638672d4d5846d0659bf102316f (patch)
treefd2a1f9fdd815a70eb514b54dbcacc24b8e402cc /srv/src/api
parente406ad6e7c82592d3bbaa1cf93ffc1612e4f196c (diff)
Add asset file upload form, plus related necessary refactors
Diffstat (limited to 'srv/src/api')
-rw-r--r--srv/src/api/api.go12
-rw-r--r--srv/src/api/apiutil/apiutil.go21
-rw-r--r--srv/src/api/assets.go28
-rw-r--r--srv/src/api/csrf.go3
-rw-r--r--srv/src/api/middleware.go6
-rw-r--r--srv/src/api/render.go68
-rw-r--r--srv/src/api/tpl/admin-assets.html26
-rw-r--r--srv/src/api/tpl/index.html12
-rw-r--r--srv/src/api/tpl/post.html32
9 files changed, 152 insertions, 56 deletions
diff --git a/srv/src/api/api.go b/srv/src/api/api.go
index b0948ac..cf34157 100644
--- a/srv/src/api/api.go
+++ b/srv/src/api/api.go
@@ -11,6 +11,7 @@ import (
"net/url"
"os"
+ "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
@@ -158,9 +159,9 @@ func (a *api) handler() http.Handler {
return a.requirePowMiddleware(h)
}
- postFormMiddleware := func(h http.Handler) http.Handler {
+ formMiddleware := func(h http.Handler) http.Handler {
h = checkCSRFMiddleware(h)
- h = postOnlyMiddleware(h)
+ h = disallowGetMiddleware(h)
h = logReqMiddleware(h)
h = addResponseHeaders(map[string]string{
"Cache-Control": "no-store, max-age=0",
@@ -193,14 +194,17 @@ func (a *api) handler() http.Handler {
a.requirePowMiddleware,
)))
- mux.Handle("/api/", http.StripPrefix("/api", postFormMiddleware(apiMux)))
+ mux.Handle("/api/", http.StripPrefix("/api", formMiddleware(apiMux)))
}
{
v2Mux := http.NewServeMux()
v2Mux.Handle("/follow.html", a.renderDumbHandler("follow.html"))
v2Mux.Handle("/posts/", a.renderPostHandler())
- v2Mux.Handle("/assets", a.renderPostAssetsIndexHandler())
+ v2Mux.Handle("/assets", apiutil.MethodMux(map[string]http.Handler{
+ "GET": a.renderPostAssetsIndexHandler(),
+ "POST": formMiddleware(a.uploadPostAssetHandler()),
+ }))
v2Mux.Handle("/assets/", a.servePostAssetHandler())
v2Mux.Handle("/", a.renderIndexHandler())
diff --git a/srv/src/api/apiutil/apiutil.go b/srv/src/api/apiutil/apiutil.go
index c9f8795..f7830ae 100644
--- a/srv/src/api/apiutil/apiutil.go
+++ b/srv/src/api/apiutil/apiutil.go
@@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"strconv"
+ "strings"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
@@ -110,3 +111,23 @@ func RandStr(numBytes int) string {
}
return hex.EncodeToString(b)
}
+
+// MethodMux will take the request method (GET, POST, etc...) and handle the
+// request using the corresponding Handler in the given map.
+//
+// If no Handler is defined for a method then a 405 Method Not Allowed error is
+// returned.
+func MethodMux(handlers map[string]http.Handler) http.Handler {
+
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+
+ handler, ok := handlers[strings.ToUpper(r.Method)]
+
+ if !ok {
+ http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ handler.ServeHTTP(rw, r)
+ })
+}
diff --git a/srv/src/api/assets.go b/srv/src/api/assets.go
index e94d324..c0a6fd9 100644
--- a/srv/src/api/assets.go
+++ b/srv/src/api/assets.go
@@ -111,3 +111,31 @@ func (a *api) servePostAssetHandler() http.Handler {
})
}
+
+func (a *api) uploadPostAssetHandler() http.Handler {
+
+ renderIndex := a.renderPostAssetsIndexHandler()
+
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+
+ id := r.PostFormValue("id")
+ if id == "" {
+ apiutil.BadRequest(rw, r, errors.New("id is required"))
+ return
+ }
+
+ file, _, err := r.FormFile("file")
+ if err != nil {
+ apiutil.BadRequest(rw, r, fmt.Errorf("reading multipart file: %w", err))
+ return
+ }
+ defer file.Close()
+
+ if err := a.params.PostAssetStore.Set(id, file); err != nil {
+ apiutil.InternalServerError(rw, r, fmt.Errorf("storing file: %w", err))
+ return
+ }
+
+ renderIndex.ServeHTTP(rw, r)
+ })
+}
diff --git a/srv/src/api/csrf.go b/srv/src/api/csrf.go
index 9717030..2a93ed7 100644
--- a/srv/src/api/csrf.go
+++ b/srv/src/api/csrf.go
@@ -10,6 +10,7 @@ import (
const (
csrfTokenCookieName = "csrf_token"
csrfTokenHeaderName = "X-CSRF-Token"
+ csrfTokenFormName = "csrfToken"
)
func setCSRFMiddleware(h http.Handler) http.Handler {
@@ -45,7 +46,7 @@ func checkCSRFMiddleware(h http.Handler) http.Handler {
givenCSRFTok := r.Header.Get(csrfTokenHeaderName)
if givenCSRFTok == "" {
- givenCSRFTok = r.FormValue("csrfToken")
+ givenCSRFTok = r.FormValue(csrfTokenFormName)
}
if csrfTok == "" || givenCSRFTok != csrfTok {
diff --git a/srv/src/api/middleware.go b/srv/src/api/middleware.go
index fcd29b3..974889b 100644
--- a/srv/src/api/middleware.go
+++ b/srv/src/api/middleware.go
@@ -80,11 +80,11 @@ func logReqMiddleware(h http.Handler) http.Handler {
})
}
-func postOnlyMiddleware(h http.Handler) http.Handler {
+func disallowGetMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- // we allow websockets to not be POSTs because, well, they can't be
- if r.Method == "POST" || r.Header.Get("Upgrade") == "websocket" {
+ // we allow websockets to be GETs because, well, they must be
+ if r.Method != "GET" || r.Header.Get("Upgrade") == "websocket" {
h.ServeHTTP(rw, r)
return
}
diff --git a/srv/src/api/render.go b/srv/src/api/render.go
index dfa665f..c0d0777 100644
--- a/srv/src/api/render.go
+++ b/srv/src/api/render.go
@@ -51,6 +51,39 @@ func (a *api) mustParseTpl(name string) *template.Template {
return tpl
}
+type tplData struct {
+ Payload interface{}
+ CSRFToken string
+}
+
+func (t tplData) CSRFFormInput() template.HTML {
+ return template.HTML(fmt.Sprintf(
+ `<input type="hidden" name="%s" value="%s" />`,
+ csrfTokenFormName, t.CSRFToken,
+ ))
+}
+
+// executeTemplate expects to be the final action in an http.Handler
+func executeTemplate(
+ rw http.ResponseWriter, r *http.Request,
+ tpl *template.Template, payload interface{},
+) {
+
+ csrfToken, _ := apiutil.GetCookie(r, csrfTokenCookieName, "")
+
+ tplData := tplData{
+ Payload: payload,
+ CSRFToken: csrfToken,
+ }
+
+ if err := tpl.Execute(rw, tplData); err != nil {
+ apiutil.InternalServerError(
+ rw, r, fmt.Errorf("rendering template: %w", err),
+ )
+ return
+ }
+}
+
func (a *api) renderIndexHandler() http.Handler {
tpl := a.mustParseTpl("index.html")
@@ -79,7 +112,7 @@ func (a *api) renderIndexHandler() http.Handler {
return
}
- tplData := struct {
+ tplPayload := struct {
Posts []post.StoredPost
PrevPage, NextPage int
}{
@@ -89,19 +122,14 @@ func (a *api) renderIndexHandler() http.Handler {
}
if page > 0 {
- tplData.PrevPage = page - 1
+ tplPayload.PrevPage = page - 1
}
if hasMore {
- tplData.NextPage = page + 1
+ tplPayload.NextPage = page + 1
}
- if err := tpl.Execute(rw, tplData); err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf("rendering index: %w", err),
- )
- return
- }
+ executeTemplate(rw, r, tpl, tplPayload)
})
}
@@ -133,7 +161,7 @@ func (a *api) renderPostHandler() http.Handler {
renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer)
- tplData := struct {
+ tplPayload := struct {
post.StoredPost
SeriesPrevious, SeriesNext *post.StoredPost
Body template.HTML
@@ -165,21 +193,16 @@ func (a *api) renderPostHandler() http.Handler {
}
if !foundThis {
- tplData.SeriesPrevious = &seriesPost
+ tplPayload.SeriesPrevious = &seriesPost
continue
}
- tplData.SeriesNext = &seriesPost
+ tplPayload.SeriesNext = &seriesPost
break
}
}
- if err := tpl.Execute(rw, tplData); err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf("rendering post with id %q: %w", id, err),
- )
- return
- }
+ executeTemplate(rw, r, tpl, tplPayload)
})
}
@@ -212,17 +235,12 @@ func (a *api) renderPostAssetsIndexHandler() http.Handler {
return
}
- tplData := struct {
+ tplPayload := struct {
IDs []string
}{
IDs: ids,
}
- if err := tpl.Execute(rw, tplData); err != nil {
- apiutil.InternalServerError(
- rw, r, fmt.Errorf("rendering: %w", err),
- )
- return
- }
+ executeTemplate(rw, r, tpl, tplPayload)
})
}
diff --git a/srv/src/api/tpl/admin-assets.html b/srv/src/api/tpl/admin-assets.html
index d871a3e..036002e 100644
--- a/srv/src/api/tpl/admin-assets.html
+++ b/srv/src/api/tpl/admin-assets.html
@@ -1,8 +1,32 @@
{{ define "body" }}
+<h2>Upload Asset</h2>
+
+<p>
+ If the given ID is the same as an existing asset's ID, then that asset will be
+ overwritten.
+</p>
+
+<form action={{ BlogURL "assets" }} method="POST" enctype="multipart/form-data">
+ {{ .CSRFFormInput }}
+ <div class="row">
+ <div class="four columns">
+ <input type="text" placeholder="Unique ID" name="id" />
+ </div>
+ <div class="four columns">
+ <input type="file" name="file" /><br/>
+ </div>
+ <div class="four columns">
+ <input type="submit" value="Upload" />
+ </div>
+ </div>
+</form>
+
+<h2>Existing Assets</h2>
+
<table>
- {{ range .IDs }}
+ {{ range .Payload.IDs }}
<tr>
<td><a href="{{ AssetURL . }}" target="_blank">{{ . }}</a></td>
<td>
diff --git a/srv/src/api/tpl/index.html b/srv/src/api/tpl/index.html
index 1858ea8..b634169 100644
--- a/srv/src/api/tpl/index.html
+++ b/srv/src/api/tpl/index.html
@@ -2,7 +2,7 @@
<ul id="posts-list">
- {{ range .Posts }}
+ {{ range .Payload.Posts }}
<li>
<h2>
<a href="posts/{{ .HTTPPath }}">{{ .Title }}</a>
@@ -17,15 +17,15 @@
</ul>
- {{ if or (ge .PrevPage 0) (ge .NextPage 0) }}
+ {{ if or (ge .Payload.PrevPage 0) (ge .Payload.NextPage 0) }}
<div id="page-turner">
- {{ if ge .PrevPage 0 }}
- <a style="float: left;" href="?p={{ .PrevPage}}">Newer</a>
+ {{ if ge .Payload.PrevPage 0 }}
+ <a style="float: left;" href="?p={{ .Payload.PrevPage}}">Newer</a>
{{ end }}
- {{ if ge .NextPage 0 }}
- <a style="float:right;" href="?p={{ .NextPage}}">Older</a>
+ {{ if ge .Payload.NextPage 0 }}
+ <a style="float:right;" href="?p={{ .Payload.NextPage}}">Older</a>
{{ end }}
</div>
diff --git a/srv/src/api/tpl/post.html b/srv/src/api/tpl/post.html
index 22a5b97..c5c3c96 100644
--- a/srv/src/api/tpl/post.html
+++ b/srv/src/api/tpl/post.html
@@ -2,43 +2,43 @@
<header id="post-header">
<h1 id="post-headline">
- {{ .Title }}
+ {{ .Payload.Title }}
</h1>
<div class="light">
- {{ .PublishedAt.Format "2006-01-02" }}
+ {{ .Payload.PublishedAt.Format "2006-01-02" }}
&nbsp;•&nbsp;
- {{ if not .LastUpdatedAt.IsZero }}
- (Updated {{ .LastUpdatedAt.Format "2006-01-02" }})
+ {{ if not .Payload.LastUpdatedAt.IsZero }}
+ (Updated {{ .Payload.LastUpdatedAt.Format "2006-01-02" }})
&nbsp;•&nbsp;
{{ end }}
- <em>{{ .Description }}</em>
+ <em>{{ .Payload.Description }}</em>
</div>
</header>
-{{ if (or .SeriesPrevious .SeriesNext) }}
+{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
<p class="light"><em>
This post is part of a series:<br/>
- {{ if .SeriesPrevious }}
- Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
+ {{ if .Payload.SeriesPrevious }}
+ Previously: <a href="{{ .Payload.SeriesPrevious.HTTPPath }}">{{ .Payload.SeriesPrevious.Title }}</a></br>
{{ end }}
- {{ if .SeriesNext }}
- Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
+ {{ if .Payload.SeriesNext }}
+ Next: <a href="{{ .Payload.SeriesNext.HTTPPath }}">{{ .Payload.SeriesNext.Title }}</a></br>
{{ end }}
</em></p>
{{ end }}
<div id="post-content">
- {{ .Body }}
+ {{ .Payload.Body }}
</div>
-{{ if (or .SeriesPrevious .SeriesNext) }}
+{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
<p class="light"><em>
If you liked this post, consider checking out other posts in the series:<br/>
- {{ if .SeriesPrevious }}
- Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
+ {{ if .Payload.SeriesPrevious }}
+ Previously: <a href="{{ .Payload.SeriesPrevious.HTTPPath }}">{{ .Payload.SeriesPrevious.Title }}</a></br>
{{ end }}
- {{ if .SeriesNext }}
- Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
+ {{ if .Payload.SeriesNext }}
+ Next: <a href="{{ .Payload.SeriesNext.HTTPPath }}">{{ .Payload.SeriesNext.Title }}</a></br>
{{ end }}
</em></p>
{{ end }}