summaryrefslogtreecommitdiff
path: root/srv/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'srv/src/api')
-rw-r--r--srv/src/api/api.go12
-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.go36
-rw-r--r--srv/src/api/csrf.go14
-rw-r--r--srv/src/api/mailinglist.go24
-rw-r--r--srv/src/api/middleware.go6
-rw-r--r--srv/src/api/posts.go65
-rw-r--r--srv/src/api/pow.go10
-rw-r--r--srv/src/api/tpl/base.html65
-rw-r--r--srv/src/api/tpl/post.html48
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" }}
+ &nbsp;•&nbsp;
+ {{ if not .LastUpdatedAt.IsZero }}
+ (Updated {{ .LastUpdatedAt.Format "2006-01-02" }})
+ &nbsp;•&nbsp;
+ {{ 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" . }}