diff options
Diffstat (limited to 'srv/src/api')
-rw-r--r-- | srv/src/api/api.go | 12 | ||||
-rw-r--r-- | srv/src/api/apiutil/apiutil.go (renamed from srv/src/api/apiutils/apiutils.go) | 4 | ||||
-rw-r--r-- | srv/src/api/chat.go | 36 | ||||
-rw-r--r-- | srv/src/api/csrf.go | 14 | ||||
-rw-r--r-- | srv/src/api/mailinglist.go | 24 | ||||
-rw-r--r-- | srv/src/api/middleware.go | 6 | ||||
-rw-r--r-- | srv/src/api/posts.go | 65 | ||||
-rw-r--r-- | srv/src/api/pow.go | 10 | ||||
-rw-r--r-- | srv/src/api/tpl/base.html | 65 | ||||
-rw-r--r-- | srv/src/api/tpl/post.html | 48 |
10 files changed, 224 insertions, 60 deletions
diff --git a/srv/src/api/api.go b/srv/src/api/api.go index 75147d5..92771a1 100644 --- a/srv/src/api/api.go +++ b/srv/src/api/api.go @@ -3,8 +3,10 @@ package api import ( "context" + "embed" "errors" "fmt" + "html/template" "net" "net/http" "net/http/httputil" @@ -20,14 +22,18 @@ import ( "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) +//go:embed tpl +var fs embed.FS + +var tpls = template.Must(template.ParseFS(fs, "tpl/*")) + // Params are used to instantiate a new API instance. All fields are required // unless otherwise noted. type Params struct { Logger *mlog.Logger PowManager pow.Manager - PostStore post.Store - PostHTTPRenderer post.Renderer + PostStore post.Store MailingList mailinglist.MailingList @@ -190,7 +196,7 @@ func (a *api) handler() http.Handler { mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) - mux.Handle("/posts/", a.postHandler()) + mux.Handle("/v2/posts/", a.postHandler()) return mux } diff --git a/srv/src/api/apiutils/apiutils.go b/srv/src/api/apiutil/apiutil.go index 223c2b9..c9f8795 100644 --- a/srv/src/api/apiutils/apiutils.go +++ b/srv/src/api/apiutil/apiutil.go @@ -1,6 +1,6 @@ -// Package apiutils contains utilities which are useful for implementing api +// Package apiutil contains utilities which are useful for implementing api // endpoints. -package apiutils +package apiutil import ( "context" diff --git a/srv/src/api/chat.go b/srv/src/api/chat.go index a1acc5a..f4b90ef 100644 --- a/srv/src/api/chat.go +++ b/srv/src/api/chat.go @@ -9,7 +9,7 @@ import ( "unicode" "github.com/gorilla/websocket" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/chat" ) @@ -44,9 +44,9 @@ func newChatHandler( func (c *chatHandler) historyHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - limit, err := apiutils.StrToInt(r.PostFormValue("limit"), 0) + limit, err := apiutil.StrToInt(r.PostFormValue("limit"), 0) if err != nil { - apiutils.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err)) + apiutil.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err)) return } @@ -58,13 +58,13 @@ func (c *chatHandler) historyHandler() http.Handler { }) if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) { - apiutils.BadRequest(rw, r, argErr.Err) + apiutil.BadRequest(rw, r, argErr.Err) return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { Cursor string `json:"cursor"` Messages []chat.Message `json:"messages"` }{ @@ -107,11 +107,11 @@ func (c *chatHandler) userIDHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { userID, err := c.userID(r) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { UserID chat.UserID `json:"userID"` }{ UserID: userID, @@ -123,18 +123,18 @@ func (c *chatHandler) appendHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { userID, err := c.userID(r) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } body := r.PostFormValue("body") if l := len(body); l == 0 { - apiutils.BadRequest(rw, r, errors.New("body is required")) + apiutil.BadRequest(rw, r, errors.New("body is required")) return } else if l > 300 { - apiutils.BadRequest(rw, r, errors.New("body too long")) + apiutil.BadRequest(rw, r, errors.New("body too long")) return } @@ -144,11 +144,11 @@ func (c *chatHandler) appendHandler() http.Handler { }) if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { MessageID string `json:"messageID"` }{ MessageID: msg.ID, @@ -164,7 +164,7 @@ func (c *chatHandler) listenHandler() http.Handler { conn, err := c.wsUpgrader.Upgrade(rw, r, nil) if err != nil { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } defer conn.Close() @@ -172,14 +172,14 @@ func (c *chatHandler) listenHandler() http.Handler { it, err := c.room.Listen(ctx, sinceID) if errors.As(err, new(chat.ErrInvalidArg)) { - apiutils.BadRequest(rw, r, err) + apiutil.BadRequest(rw, r, err) return } else if errors.Is(err, context.Canceled) { return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -192,7 +192,7 @@ func (c *chatHandler) listenHandler() http.Handler { return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -203,7 +203,7 @@ func (c *chatHandler) listenHandler() http.Handler { }) if err != nil { - apiutils.GetRequestLogger(r).Error(ctx, "couldn't write message", err) + apiutil.GetRequestLogger(r).Error(ctx, "couldn't write message", err) return } } diff --git a/srv/src/api/csrf.go b/srv/src/api/csrf.go index 13b6ec6..9717030 100644 --- a/srv/src/api/csrf.go +++ b/srv/src/api/csrf.go @@ -4,7 +4,7 @@ import ( "errors" "net/http" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" ) const ( @@ -15,16 +15,16 @@ const ( func setCSRFMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "") + csrfTok, err := apiutil.GetCookie(r, csrfTokenCookieName, "") if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } else if csrfTok == "" { http.SetCookie(rw, &http.Cookie{ Name: csrfTokenCookieName, - Value: apiutils.RandStr(32), + Value: apiutil.RandStr(32), Secure: true, }) } @@ -36,10 +36,10 @@ func setCSRFMiddleware(h http.Handler) http.Handler { func checkCSRFMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - csrfTok, err := apiutils.GetCookie(r, csrfTokenCookieName, "") + csrfTok, err := apiutil.GetCookie(r, csrfTokenCookieName, "") if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } @@ -49,7 +49,7 @@ func checkCSRFMiddleware(h http.Handler) http.Handler { } if csrfTok == "" || givenCSRFTok != csrfTok { - apiutils.BadRequest(rw, r, errors.New("invalid CSRF token")) + apiutil.BadRequest(rw, r, errors.New("invalid CSRF token")) return } diff --git a/srv/src/api/mailinglist.go b/srv/src/api/mailinglist.go index d89fe2a..c12e75d 100644 --- a/srv/src/api/mailinglist.go +++ b/srv/src/api/mailinglist.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" ) @@ -16,7 +16,7 @@ func (a *api) mailingListSubscribeHandler() http.Handler { parts[0] == "" || parts[1] == "" || len(email) >= 512 { - apiutils.BadRequest(rw, r, errors.New("invalid email")) + apiutil.BadRequest(rw, r, errors.New("invalid email")) return } @@ -26,11 +26,11 @@ func (a *api) mailingListSubscribeHandler() http.Handler { // just eat the error, make it look to the user like the // verification email was sent. } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } @@ -40,25 +40,25 @@ func (a *api) mailingListFinalizeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { subToken := r.PostFormValue("subToken") if l := len(subToken); l == 0 || l > 128 { - apiutils.BadRequest(rw, r, errInvalidSubToken) + apiutil.BadRequest(rw, r, errInvalidSubToken) return } err := a.params.MailingList.FinalizeSubscription(subToken) if errors.Is(err, mailinglist.ErrNotFound) { - apiutils.BadRequest(rw, r, errInvalidSubToken) + apiutil.BadRequest(rw, r, errInvalidSubToken) return } else if errors.Is(err, mailinglist.ErrAlreadyVerified) { // no problem } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } @@ -68,21 +68,21 @@ func (a *api) mailingListUnsubscribeHandler() http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { unsubToken := r.PostFormValue("unsubToken") if l := len(unsubToken); l == 0 || l > 128 { - apiutils.BadRequest(rw, r, errInvalidUnsubToken) + apiutil.BadRequest(rw, r, errInvalidUnsubToken) return } err := a.params.MailingList.Unsubscribe(unsubToken) if errors.Is(err, mailinglist.ErrNotFound) { - apiutils.BadRequest(rw, r, errInvalidUnsubToken) + apiutil.BadRequest(rw, r, errInvalidUnsubToken) return } else if err != nil { - apiutils.InternalServerError(rw, r, err) + apiutil.InternalServerError(rw, r, err) return } - apiutils.JSONResult(rw, r, struct{}{}) + apiutil.JSONResult(rw, r, struct{}{}) }) } diff --git a/srv/src/api/middleware.go b/srv/src/api/middleware.go index 6ea0d13..0b3eec7 100644 --- a/srv/src/api/middleware.go +++ b/srv/src/api/middleware.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) @@ -61,7 +61,7 @@ func (lrw *logResponseWriter) WriteHeader(statusCode int) { func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - r = apiutils.SetRequestLogger(r, logger) + r = apiutil.SetRequestLogger(r, logger) lrw := newLogResponseWriter(rw) @@ -90,7 +90,7 @@ func postOnlyMiddleware(h http.Handler) http.Handler { return } - apiutils.GetRequestLogger(r).WarnString(r.Context(), "method not allowed") + apiutil.GetRequestLogger(r).WarnString(r.Context(), "method not allowed") rw.WriteHeader(405) }) } diff --git a/srv/src/api/posts.go b/srv/src/api/posts.go index 995f2fb..cc7a176 100644 --- a/srv/src/api/posts.go +++ b/srv/src/api/posts.go @@ -3,11 +3,15 @@ package api import ( "errors" "fmt" + "html/template" "net/http" "path/filepath" "strings" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/post" ) @@ -22,22 +26,63 @@ func (a *api) postHandler() http.Handler { http.Error(rw, "Post not found", 404) return } else if err != nil { - apiutils.InternalServerError( + apiutil.InternalServerError( rw, r, fmt.Errorf("fetching post with id %q: %w", id, err), ) return } - renderablePost, err := post.NewRenderablePost(a.params.PostStore, storedPost) - if err != nil { - apiutils.InternalServerError( - rw, r, fmt.Errorf("constructing renderable post with id %q: %w", id, err), - ) - return + parserExt := parser.CommonExtensions | parser.AutoHeadingIDs + parser := parser.NewWithExtensions(parserExt) + + htmlFlags := html.CommonFlags | html.HrefTargetBlank + htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) + + renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer) + + tplData := struct { + post.StoredPost + SeriesPrevious, SeriesNext *post.StoredPost + Body template.HTML + }{ + StoredPost: storedPost, + Body: template.HTML(renderedBody), + } + + if series := storedPost.Series; series != "" { + + seriesPosts, err := a.params.PostStore.GetBySeries(series) + if err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf("fetching posts for series %q: %w", series, err), + ) + return + } + + var foundThis bool + + for i := range seriesPosts { + + seriesPost := seriesPosts[i] + + if seriesPost.ID == storedPost.ID { + foundThis = true + continue + } + + if !foundThis { + tplData.SeriesPrevious = &seriesPost + continue + } + + tplData.SeriesNext = &seriesPost + break + } } - if err := a.params.PostHTTPRenderer.Render(rw, renderablePost); err != nil { - apiutils.InternalServerError( + if err := tpls.ExecuteTemplate(rw, "post.html", tplData); err != nil { + apiutil.InternalServerError( rw, r, fmt.Errorf("rendering post with id %q: %w", id, err), ) return diff --git a/srv/src/api/pow.go b/srv/src/api/pow.go index 1b232b1..ae2d2f1 100644 --- a/srv/src/api/pow.go +++ b/srv/src/api/pow.go @@ -6,7 +6,7 @@ import ( "fmt" "net/http" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutils" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" ) func (a *api) newPowChallengeHandler() http.Handler { @@ -14,7 +14,7 @@ func (a *api) newPowChallengeHandler() http.Handler { challenge := a.params.PowManager.NewChallenge() - apiutils.JSONResult(rw, r, struct { + apiutil.JSONResult(rw, r, struct { Seed string `json:"seed"` Target uint32 `json:"target"` }{ @@ -30,21 +30,21 @@ func (a *api) requirePowMiddleware(h http.Handler) http.Handler { seedHex := r.FormValue("powSeed") seed, err := hex.DecodeString(seedHex) if err != nil || len(seed) == 0 { - apiutils.BadRequest(rw, r, errors.New("invalid powSeed")) + apiutil.BadRequest(rw, r, errors.New("invalid powSeed")) return } solutionHex := r.FormValue("powSolution") solution, err := hex.DecodeString(solutionHex) if err != nil || len(seed) == 0 { - apiutils.BadRequest(rw, r, errors.New("invalid powSolution")) + apiutil.BadRequest(rw, r, errors.New("invalid powSolution")) return } err = a.params.PowManager.CheckSolution(seed, solution) if err != nil { - apiutils.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) + apiutil.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err)) return } diff --git a/srv/src/api/tpl/base.html b/srv/src/api/tpl/base.html new file mode 100644 index 0000000..bf81032 --- /dev/null +++ b/srv/src/api/tpl/base.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html lang="en"> + + <head> + <meta charset="utf-8"> + <link rel="stylesheet" href="/assets/normalize.css"> + <link rel="stylesheet" href="/assets/skeleton.css"> + <link rel="stylesheet" href="/assets/friendly.css"> + <link rel="stylesheet" href="/assets/main.css"> + <link rel="stylesheet" href="/assets/fontawesome/css/all.css"> + </head> + + <body> + + <div class="container"> + + <header id="title-header" role="banner"> + <div class="row"> + <div class="seven columns" style="margin-bottom: 3rem;"> + <h1 class="title"> + <a href="/">Mediocre Blog</a> + </h1> + <div class="light social"> + <span>By Brian Picciano</span> + <span> + Even more @ + <a href="https://mediocregopher.eth.link" target="_blank">https://mediocregopher.eth.link</a> + </span> + </div> + </div> + + <div class="five columns light"> + <span style="display:block; margin-bottom:0.5rem;">Get notified when new posts are published!</span> + <a href="/follow.html"> + <button class="button-primary"> + <i class="far fa-envelope"></i> + Follow + </button> + </a> + <a href="/feed.xml"> + <button class="button"> + <i class="fas fa-rss"></i> + RSS + </button> + </a> + </div> + + </div> + </header> + + {{ template "body" . }} + + <footer> + <p class="license light"> + Unless otherwised specified, all works are licensed under the + <a href="/assets/wtfpl.txt">WTFPL</a>. + </p> + </footer> + + </div> + + </body> + +</html> + diff --git a/srv/src/api/tpl/post.html b/srv/src/api/tpl/post.html new file mode 100644 index 0000000..22a5b97 --- /dev/null +++ b/srv/src/api/tpl/post.html @@ -0,0 +1,48 @@ +{{ define "body" }} + +<header id="post-header"> + <h1 id="post-headline"> + {{ .Title }} + </h1> + <div class="light"> + {{ .PublishedAt.Format "2006-01-02" }} + • + {{ if not .LastUpdatedAt.IsZero }} + (Updated {{ .LastUpdatedAt.Format "2006-01-02" }}) + • + {{ end }} + <em>{{ .Description }}</em> + </div> +</header> + +{{ if (or .SeriesPrevious .SeriesNext) }} +<p class="light"><em> + This post is part of a series:<br/> + {{ if .SeriesPrevious }} + Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br> + {{ end }} + {{ if .SeriesNext }} + Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br> + {{ end }} +</em></p> +{{ end }} + +<div id="post-content"> + {{ .Body }} +</div> + +{{ if (or .SeriesPrevious .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> + {{ end }} + {{ if .SeriesNext }} + Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br> + {{ end }} +</em></p> +{{ end }} + +{{ end }} + +{{ template "base.html" . }} |