implement finalize and unsubscribe endpoints

This commit is contained in:
Brian Picciano 2021-08-03 16:28:22 -06:00
parent ec4aac24ab
commit 9c3ea8dd80
5 changed files with 133 additions and 11 deletions

View File

@ -32,18 +32,24 @@ func mailingListSubscribeHandler(ml mailinglist.MailingList) http.Handler {
}
func mailingListFinalizeHandler(ml mailinglist.MailingList) 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 {
badRequest(rw, r, errors.New("invalid subToken"))
badRequest(rw, r, errInvalidSubToken)
return
}
err := ml.FinalizeSubscription(subToken)
if errors.Is(err, mailinglist.ErrNotFound) ||
errors.Is(err, mailinglist.ErrAlreadyVerified) {
badRequest(rw, r, err)
if errors.Is(err, mailinglist.ErrNotFound) {
badRequest(rw, r, errInvalidSubToken)
return
} else if errors.Is(err, mailinglist.ErrAlreadyVerified) {
// no problem
} else if err != nil {
internalServerError(rw, r, err)
return
@ -54,17 +60,21 @@ func mailingListFinalizeHandler(ml mailinglist.MailingList) http.Handler {
}
func mailingListUnsubscribeHandler(ml mailinglist.MailingList) 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 {
badRequest(rw, r, errors.New("invalid unsubToken"))
badRequest(rw, r, errInvalidUnsubToken)
return
}
err := ml.Unsubscribe(unsubToken)
if errors.Is(err, mailinglist.ErrNotFound) {
badRequest(rw, r, err)
badRequest(rw, r, errInvalidUnsubToken)
return
} else if err != nil {
internalServerError(rw, r, err)
return

View File

@ -27,7 +27,7 @@ func main() {
logger := mlog.NewLogger(nil)
hostname := flag.String("hostname", "localhost:4000", "Hostname to advertise this server as")
publicURLStr := flag.String("public-url", "http://localhost:4000", "URL this service is accessible at")
listenAddr := flag.String("listen-addr", ":4000", "Address to listen for HTTP requests on")
dataDir := flag.String("data-dir", ".", "Directory to use for long term storage")
@ -55,6 +55,11 @@ func main() {
logger.Fatal(context.Background(), "-ml-smtp-auth is required")
}
publicURL, err := url.Parse(*publicURLStr)
if err != nil {
loggerFatalErr(context.Background(), logger, "parsing -public-url", err)
}
var staticProxyURL *url.URL
if *staticProxyURLStr != "" {
var err error
@ -79,7 +84,7 @@ func main() {
// initialization
ctx := mctx.Annotate(context.Background(),
"hostname", *hostname,
"publicURL", publicURL.String(),
"listenAddr", *listenAddr,
"dataDir", *dataDir,
"powTarget", fmt.Sprintf("%x", powTarget),
@ -124,8 +129,8 @@ func main() {
Store: mlStore,
Mailer: mailer,
Clock: clock,
FinalizeSubURL: *hostname + "/mailinglist/finalize.html",
UnsubURL: *hostname + "/mailinglist/unsubscribe.html",
FinalizeSubURL: path.Join(publicURL.String(), "/mailinglist/finalize.html"),
UnsubURL: path.Join(publicURL.String(), "/mailinglist/unsubscribe.html"),
})
mux := http.NewServeMux()

View File

@ -17,7 +17,7 @@ import (
var (
// ErrNotFound is used to indicate an email could not be found in the
// database.
ErrNotFound = errors.New("no record for given email found")
ErrNotFound = errors.New("no record found")
)
// EmailIterator will iterate through a sequence of emails, returning the next

View File

@ -0,0 +1,53 @@
---
layout: page
title: ""
nofollow: true
---
<style>
#result.success { color: green; }
#result.fail { color: red; }
</style>
<span id="result"></span>
<script>
(async () => {
const resultSpan = document.getElementById("result");
function setErr(errStr) {
resultSpan.className = "fail";
resultSpan.innerHTML = errStr;
}
const urlParams = new URLSearchParams(window.location.search);
const subToken = urlParams.get('subToken');
if (!subToken) {
setErr("No subscription token provided");
return;
}
const finalizeForm = new FormData();
finalizeForm.append('subToken', subToken);
const finalizeReq = new Request('/api/mailinglist/finalize', {
method: 'POST',
body: finalizeForm,
});
const res = await fetch(finalizeReq)
.then(response => response.json());
if (res.error) {
setErr(res.error);
return;
}
resultSpan.className = "success";
resultSpan.innerHTML = "Your email subscription has been finalized! Please go on about your day.";
})();
</script>

View File

@ -0,0 +1,54 @@
---
layout: page
title: ""
nofollow: true
---
<style>
#result.success { color: green; }
#result.fail { color: red; }
</style>
<span id="result"></span>
<script>
(async () => {
const resultSpan = document.getElementById("result");
function setErr(errStr) {
resultSpan.className = "fail";
resultSpan.innerHTML = errStr;
}
const urlParams = new URLSearchParams(window.location.search);
const unsubToken = urlParams.get('unsubToken');
if (!unsubToken) {
setErr("No unsubscribe token provided");
return;
}
const unsubscribeForm = new FormData();
unsubscribeForm.append('unsubToken', unsubToken);
const unsubscribeReq = new Request('/api/mailinglist/unsubscribe', {
method: 'POST',
body: unsubscribeForm,
});
const res = await fetch(unsubscribeReq)
.then(response => response.json());
if (res.error) {
setErr(res.error);
return;
}
resultSpan.className = "success";
resultSpan.innerHTML = "You have been unsubscribed! Please go on about your day.";
})();
</script>