summaryrefslogtreecommitdiff
path: root/src/http
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2023-08-25 21:04:59 +0200
committerBrian Picciano <mediocregopher@gmail.com>2023-08-25 21:12:57 +0200
commit78bbfa42fa1159bce12c2c1d29eeb0bb9a8a2f75 (patch)
tree041dd938346eddf0a4bcd098403c229555a654cb /src/http
parentc4ec9064063f3b15aeb25feb85a3afaaa02008ba (diff)
Remove mailinglist and proof-of-work functionality
Diffstat (limited to 'src/http')
-rw-r--r--src/http/http.go35
-rw-r--r--src/http/mailinglist.go92
-rw-r--r--src/http/posts.go13
-rw-r--r--src/http/pow.go53
-rw-r--r--src/http/static/api.js118
-rw-r--r--src/http/static/solvePow.js28
-rw-r--r--src/http/static/utils.js12
-rw-r--r--src/http/tpl/finalize.html45
-rw-r--r--src/http/tpl/follow.html108
-rw-r--r--src/http/tpl/unsubscribe.html44
10 files changed, 5 insertions, 543 deletions
diff --git a/src/http/http.go b/src/http/http.go
index ba81577..4b98d2b 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -18,10 +18,8 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
- "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post/asset"
- "github.com/mediocregopher/blog.mediocregopher.com/srv/pow"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
@@ -32,17 +30,14 @@ var staticFS embed.FS
// 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
- Cache cache.Cache
+ Logger *mlog.Logger
+ Cache cache.Cache
PostStore post.Store
PostAssetStore asset.Store
PostAssetLoader asset.Loader
PostDraftStore post.DraftStore
- MailingList mailinglist.MailingList
-
// PublicURL is the base URL which site visitors can navigate to.
PublicURL *url.URL
@@ -176,25 +171,6 @@ func (a *api) Shutdown(ctx context.Context) error {
return nil
}
-func (a *api) apiHandler() http.Handler {
- mux := http.NewServeMux()
-
- mux.Handle("/pow/challenge", a.newPowChallengeHandler())
- mux.Handle("/pow/check",
- a.requirePowMiddleware(
- http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {}),
- ),
- )
-
- mux.Handle("/mailinglist/subscribe", a.requirePowMiddleware(a.mailingListSubscribeHandler()))
- mux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler())
- mux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler())
-
- return apiutil.MethodMux(map[string]http.Handler{
- "POST": mux,
- })
-}
-
func (a *api) blogHandler() http.Handler {
mux := http.NewServeMux()
@@ -237,8 +213,6 @@ func (a *api) blogHandler() http.Handler {
mux.Handle("/static/", http.FileServer(http.FS(staticFS)))
mux.Handle("/follow", a.renderDumbTplHandler("follow.html"))
mux.Handle("/admin", a.renderDumbTplHandler("admin.html"))
- mux.Handle("/mailinglist/unsubscribe", a.renderDumbTplHandler("unsubscribe.html"))
- mux.Handle("/mailinglist/finalize", a.renderDumbTplHandler("finalize.html"))
mux.Handle("/feed.xml", a.renderFeedHandler())
mux.Handle("/", a.renderIndexHandler())
@@ -266,11 +240,6 @@ func (a *api) handler() http.Handler {
mux := http.NewServeMux()
- mux.Handle("/api/", applyMiddlewares(
- http.StripPrefix("/api", a.apiHandler()),
- logReqMiddleware,
- ))
-
mux.Handle("/", a.blogHandler())
noCacheMiddleware := addResponseHeadersMiddleware(map[string]string{
diff --git a/src/http/mailinglist.go b/src/http/mailinglist.go
deleted file mode 100644
index eab2f51..0000000
--- a/src/http/mailinglist.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package http
-
-import (
- "errors"
- "net/http"
- "strings"
-
- "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
- "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
-)
-
-func (a *api) mailingListSubscribeHandler() http.Handler {
- return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- email := r.PostFormValue("email")
- if parts := strings.Split(email, "@"); len(parts) != 2 ||
- parts[0] == "" ||
- parts[1] == "" ||
- len(email) >= 512 {
- apiutil.BadRequest(rw, r, errors.New("invalid email"))
- return
-
- } else if strings.ToLower(parts[1]) == "gmail.com" {
- apiutil.BadRequest(rw, r, errors.New("gmail does not allow its users to receive email from me, sorry"))
- return
- }
-
- err := a.params.MailingList.BeginSubscription(email)
-
- if errors.Is(err, mailinglist.ErrAlreadyVerified) {
- // just eat the error, make it look to the user like the
- // verification email was sent.
- } else if err != nil {
- apiutil.InternalServerError(rw, r, err)
- return
- }
-
- apiutil.JSONResult(rw, r, struct{}{})
- })
-}
-
-func (a *api) mailingListFinalizeHandler() http.Handler {
- var errInvalidSubToken = errors.New("invalid subToken")
-
- return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- subToken := r.PostFormValue("subToken")
- if l := len(subToken); l == 0 || l > 128 {
- apiutil.BadRequest(rw, r, errInvalidSubToken)
- return
- }
-
- err := a.params.MailingList.FinalizeSubscription(subToken)
-
- if errors.Is(err, mailinglist.ErrNotFound) {
- apiutil.BadRequest(rw, r, errInvalidSubToken)
- return
-
- } else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
- // no problem
-
- } else if err != nil {
- apiutil.InternalServerError(rw, r, err)
- return
- }
-
- apiutil.JSONResult(rw, r, struct{}{})
- })
-}
-
-func (a *api) mailingListUnsubscribeHandler() http.Handler {
- var errInvalidUnsubToken = errors.New("invalid unsubToken")
-
- return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- unsubToken := r.PostFormValue("unsubToken")
- if l := len(unsubToken); l == 0 || l > 128 {
- apiutil.BadRequest(rw, r, errInvalidUnsubToken)
- return
- }
-
- err := a.params.MailingList.Unsubscribe(unsubToken)
-
- if errors.Is(err, mailinglist.ErrNotFound) {
- apiutil.BadRequest(rw, r, errInvalidUnsubToken)
- return
-
- } else if err != nil {
- apiutil.InternalServerError(rw, r, err)
- return
- }
-
- apiutil.JSONResult(rw, r, struct{}{})
- })
-}
diff --git a/src/http/posts.go b/src/http/posts.go
index 5c7ac25..b1dbc35 100644
--- a/src/http/posts.go
+++ b/src/http/posts.go
@@ -418,7 +418,7 @@ func postFromPostReq(r *http.Request) (post.Post, error) {
return p, nil
}
-func (a *api) storeAndPublishPost(ctx context.Context, p post.Post) error {
+func (a *api) publishPost(ctx context.Context, p post.Post) error {
first, err := a.params.PostStore.Set(p, time.Now())
@@ -430,13 +430,6 @@ func (a *api) storeAndPublishPost(ctx context.Context, p post.Post) error {
return nil
}
- a.params.Logger.Info(ctx, "publishing blog post to mailing list")
- urlStr := a.postURL(p.ID, true)
-
- if err := a.params.MailingList.Publish(p.Title, urlStr); err != nil {
- return fmt.Errorf("publishing post to mailing list: %w", err)
- }
-
if err := a.params.PostDraftStore.Delete(p.ID); err != nil {
return fmt.Errorf("deleting draft: %w", err)
}
@@ -458,9 +451,9 @@ func (a *api) postPostHandler() http.Handler {
ctx = mctx.Annotate(ctx, "postID", p.ID)
- if err := a.storeAndPublishPost(ctx, p); err != nil {
+ if err := a.publishPost(ctx, p); err != nil {
apiutil.InternalServerError(
- rw, r, fmt.Errorf("storing/publishing post with id %q: %w", p.ID, err),
+ rw, r, fmt.Errorf("publishing post with id %q: %w", p.ID, err),
)
return
}
diff --git a/src/http/pow.go b/src/http/pow.go
deleted file mode 100644
index 1bd5cb5..0000000
--- a/src/http/pow.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package http
-
-import (
- "encoding/hex"
- "errors"
- "fmt"
- "net/http"
-
- "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
-)
-
-func (a *api) newPowChallengeHandler() http.Handler {
- return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-
- challenge := a.params.PowManager.NewChallenge()
-
- apiutil.JSONResult(rw, r, struct {
- Seed string `json:"seed"`
- Target uint32 `json:"target"`
- }{
- Seed: hex.EncodeToString(challenge.Seed),
- Target: challenge.Target,
- })
- })
-}
-
-func (a *api) requirePowMiddleware(h http.Handler) http.Handler {
- return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-
- seedHex := r.FormValue("powSeed")
- seed, err := hex.DecodeString(seedHex)
- if err != nil || len(seed) == 0 {
- apiutil.BadRequest(rw, r, errors.New("invalid powSeed"))
- return
- }
-
- solutionHex := r.FormValue("powSolution")
- solution, err := hex.DecodeString(solutionHex)
- if err != nil || len(seed) == 0 {
- apiutil.BadRequest(rw, r, errors.New("invalid powSolution"))
- return
- }
-
- err = a.params.PowManager.CheckSolution(seed, solution)
-
- if err != nil {
- apiutil.BadRequest(rw, r, fmt.Errorf("checking proof-of-work solution: %w", err))
- return
- }
-
- h.ServeHTTP(rw, r)
- })
-}
diff --git a/src/http/static/api.js b/src/http/static/api.js
deleted file mode 100644
index 55c9ecd..0000000
--- a/src/http/static/api.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import * as utils from "/static/utils.js";
-
-const doFetch = async (req) => {
- let res, jsonRes;
- try {
- res = await fetch(req);
- jsonRes = await res.json();
-
- } catch (e) {
-
- if (e instanceof SyntaxError)
- e = new Error(`status ${res.status}, empty (or invalid) response body`);
-
- console.error(`api call ${req.method} ${req.url}: unexpected error:`, e);
- throw e;
- }
-
- if (jsonRes.error) {
- console.error(
- `api call ${req.method} ${req.url}: application error:`,
- res.status,
- jsonRes.error,
- );
-
- throw jsonRes.error;
- }
-
- return jsonRes;
-}
-
-// may throw
-const solvePow = async () => {
-
- const res = await call('/api/pow/challenge');
-
- const worker = new Worker('/static/solvePow.js');
-
- const p = new Promise((resolve, reject) => {
- worker.postMessage({seedHex: res.seed, target: res.target});
- worker.onmessage = resolve;
- });
-
- const powSol = (await p).data;
- worker.terminate();
-
- return {seed: res.seed, solution: powSol};
-}
-
-const call = async (route, opts = {}) => {
- const {
- method = 'POST',
- body = {},
- requiresPow = false,
- } = opts;
-
- const reqOpts = {
- method,
- };
-
- if (requiresPow) {
- const {seed, solution} = await solvePow();
- body.powSeed = seed;
- body.powSolution = solution;
- }
-
- if (Object.keys(body).length > 0) {
- const form = new FormData();
- for (const key in body) form.append(key, body[key]);
-
- reqOpts.body = form;
- }
-
- const req = new Request(route, reqOpts);
- return doFetch(req);
-}
-
-const ws = async (route, opts = {}) => {
- const {
- requiresPow = false,
- params = {},
- } = opts;
-
- const docURL = new URL(document.URL);
- const protocol = docURL.protocol == "http:" ? "ws:" : "wss:";
-
- const fullParams = new URLSearchParams(params);
-
- if (requiresPow) {
- const {seed, solution} = await solvePow();
- fullParams.set("powSeed", seed);
- fullParams.set("powSolution", solution);
- }
-
- const rawConn = new WebSocket(`${protocol}//${docURL.host}${route}?${fullParams.toString()}`);
-
- const conn = {
- next: () => new Promise((resolve, reject) => {
- rawConn.onmessage = (m) => {
- const mj = JSON.parse(m.data);
- resolve(mj);
- };
- rawConn.onerror = reject;
- rawConn.onclose = reject;
- }),
-
- close: rawConn.close,
- };
-
- return new Promise((resolve, reject) => {
- rawConn.onopen = () => resolve(conn);
- rawConn.onerror = reject;
- });
-}
-
-export {
- call,
- ws
-}
diff --git a/src/http/static/solvePow.js b/src/http/static/solvePow.js
deleted file mode 100644
index 900400c..0000000
--- a/src/http/static/solvePow.js
+++ /dev/null
@@ -1,28 +0,0 @@
-const fromHexString = hexString =>
- new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
-
-const toHexString = bytes =>
- bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
-
-onmessage = async (e) => {
- const seed = fromHexString(e.data.seedHex);
- const target = e.data.target;
-
- const fullBuf = new ArrayBuffer(seed.byteLength*2);
-
- const fullBufSeed = new Uint8Array(fullBuf, 0, seed.byteLength);
- seed.forEach((v, i) => fullBufSeed[i] = v);
-
- const randBuf = new Uint8Array(fullBuf, seed.byteLength);
-
- while (true) {
- crypto.getRandomValues(randBuf);
- const digest = await crypto.subtle.digest('SHA-512', fullBuf);
- const digestView = new DataView(digest);
- if (digestView.getUint32(0) < target) {
- postMessage(toHexString(randBuf));
- return;
- }
- }
-
-};
diff --git a/src/http/static/utils.js b/src/http/static/utils.js
deleted file mode 100644
index 96a2950..0000000
--- a/src/http/static/utils.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const cookies = {};
-const cookieKVs = document.cookie
- .split(';')
- .map(cookie => cookie.trim().split('=', 2));
-
-for (const i in cookieKVs) {
- cookies[cookieKVs[i][0]] = cookieKVs[i][1];
-}
-
-export {
- cookies,
-}
diff --git a/src/http/tpl/finalize.html b/src/http/tpl/finalize.html
deleted file mode 100644
index 8bdfceb..0000000
--- a/src/http/tpl/finalize.html
+++ /dev/null
@@ -1,45 +0,0 @@
-{{ define "body" }}
-
-<script async type="module" src="{{ StaticURL "api.js" }}"></script>
-
-<style>
-#result.success { color: green; }
-#result.fail { color: red; }
-</style>
-
-<span id="result"></span>
-
-<script>
-
-(async () => {
-
- const resultSpan = document.getElementById("result");
-
- try {
-
- const urlParams = new URLSearchParams(window.location.search);
- const subToken = urlParams.get('subToken');
-
- if (!subToken) throw "No subscription token provided";
-
- const api = await import("{{ StaticURL "api.js" }}");
-
- await api.call('/api/mailinglist/finalize', {
- body: { subToken },
- });
-
- resultSpan.className = "success";
- resultSpan.innerHTML = "Your email subscription has been finalized! Please go on about your day.";
-
- } catch (e) {
- resultSpan.className = "fail";
- resultSpan.innerHTML = e;
- }
-
-})();
-
-</script>
-
-{{ end }}
-
-{{ template "base.html" . }}
diff --git a/src/http/tpl/follow.html b/src/http/tpl/follow.html
index 88fee46..1958f95 100644
--- a/src/http/tpl/follow.html
+++ b/src/http/tpl/follow.html
@@ -1,113 +1,5 @@
{{ define "body" }}
-<script async type="module" src="{{ StaticURL "api.js" }}"></script>
-
-<p>
- Here's your options for receiving updates about new posts:
-</p>
-
-<h2>Option 1: Email</h2>
-
-<p>
- Email is by far my preferred option for notifying followers of new posts. The
- entire email list system for this site has been designed from scratch and is
- completely self-hosted in my living room.
-</p>
-
-<p>
- I solemnly swear that:
-</p>
-
-<ul>
-
- <li>
- You will never receive an email from me except to notify of a new post.
- </li>
-
- <li>
- Your email will never be provided or sold to anyone else for any reason.
- </li>
-
-</ul>
-
-<p>
- So smash that subscribe button!
-</p>
-
-<p>
- You will need to verify your email, so be sure to check your spam folder to
- complete the process if you don't immediately see anything in your inbox.
-</p>
-
-<p style="color: var(--nc-lk-2);">
- Unfortunately Google considers all emails from my mail server to be spam, so
- gmail emails are not allowed. Sorry (not sorry).
-</p>
-
-<style>
-
-#emailStatus.success {
- color: green;
-}
-
-#emailStatus.fail {
- color: red;
-}
-
-</style>
-
-<form action="javascript:void(0);">
- <input type="email" placeholder="name@host.com" id="emailAddress" />
- <input class="button-primary" type="submit" value="Subscribe" id="emailSubscribe" />
- <span id="emailStatus"></span>
-</form>
-
-<script>
-
-const emailAddress = document.getElementById("emailAddress");
-const emailSubscribe = document.getElementById("emailSubscribe");
-const emailSubscribeOrigValue = emailSubscribe.value;
-const emailStatus = document.getElementById("emailStatus");
-
-emailSubscribe.onclick = async () => {
-
- const api = await import("{{ StaticURL "api.js" }}");
-
- emailSubscribe.disabled = true;
- emailSubscribe.className = "";
- emailSubscribe.value = "Please hold...";
- emailStatus.innerHTML = '';
-
- try {
-
- if (!window.isSecureContext) {
- throw "The browser environment is not secure.";
- }
-
- await api.call('/api/mailinglist/subscribe', {
- body: { email: emailAddress.value },
- requiresPow: true,
- });
-
- emailStatus.className = "success";
- emailStatus.innerHTML = "Verification email sent (check your spam folder)";
-
- } catch (e) {
- emailStatus.className = "fail";
- emailStatus.innerHTML = e;
-
- } finally {
- emailSubscribe.disabled = false;
- emailSubscribe.className = "button-primary";
- emailSubscribe.value = emailSubscribeOrigValue;
- }
-
-};
-
-</script>
-
-<h2>Option 2: RSS</h2>
-
<p>
RSS is the classic way to follow a site's updates, and we're bringing it back!
Just give any RSS reader the following URL:
diff --git a/src/http/tpl/unsubscribe.html b/src/http/tpl/unsubscribe.html
deleted file mode 100644
index ad01735..0000000
--- a/src/http/tpl/unsubscribe.html
+++ /dev/null
@@ -1,44 +0,0 @@
-{{ define "body" }}
-
-<script async type="module" src="{{ StaticURL "api.js" }}"></script>
-
-<style>
-#result.success { color: green; }
-#result.fail { color: red; }
-</style>
-
-<span id="result"></span>
-
-<script>
-
-(async () => {
-
- const resultSpan = document.getElementById("result");
-
- try {
- const urlParams = new URLSearchParams(window.location.search);
- const unsubToken = urlParams.get('unsubToken');
-
- if (!unsubToken) throw "No unsubscribe token provided";
-
- const api = await import("{{ StaticURL "api.js" }}");
-
- await api.call('/api/mailinglist/unsubscribe', {
- body: { unsubToken },
- });
-
- resultSpan.className = "success";
- resultSpan.innerHTML = "You have been unsubscribed! Please go on about your day.";
-
- } catch (e) {
- resultSpan.className = "fail";
- resultSpan.innerHTML = e;
- }
-
-})();
-
-</script>
-
-{{ end }}
-
-{{ template "base.html" . }}