diff options
-rw-r--r-- | srv/src/api/api.go | 12 | ||||
-rw-r--r-- | srv/src/api/apiutil/apiutil.go | 21 | ||||
-rw-r--r-- | srv/src/api/assets.go | 28 | ||||
-rw-r--r-- | srv/src/api/csrf.go | 3 | ||||
-rw-r--r-- | srv/src/api/middleware.go | 6 | ||||
-rw-r--r-- | srv/src/api/render.go | 68 | ||||
-rw-r--r-- | srv/src/api/tpl/admin-assets.html | 26 | ||||
-rw-r--r-- | srv/src/api/tpl/index.html | 12 | ||||
-rw-r--r-- | srv/src/api/tpl/post.html | 32 |
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" }} • - {{ if not .LastUpdatedAt.IsZero }} - (Updated {{ .LastUpdatedAt.Format "2006-01-02" }}) + {{ if not .Payload.LastUpdatedAt.IsZero }} + (Updated {{ .Payload.LastUpdatedAt.Format "2006-01-02" }}) • {{ 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 }} |