diff --git a/README.md b/README.md
index f95aea1..6b09331 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ Accepted form format:
target=https://aaronpk.example/post-by-aaron
```
-Will result in a `202 Accepted` - it handles things async. Stores in `.json` files in `[dataPath]/domain`.
+Will result in a `202 Accepted` - it handles things async. Stores mentions in a **to approve** and **approved** database separately.
This also saves the author picture/avatar locally - if present in the microformat. It does _not_ resize images, however, if it's bigger than 5 MB, it falls back to a default one.
diff --git a/app/admin/dashboard.html b/app/admin/dashboard.html
new file mode 100644
index 0000000..1d66ea4
--- /dev/null
+++ b/app/admin/dashboard.html
@@ -0,0 +1,69 @@
+
+
+
+
+ Go-Jamming admin dashboard
+
+
+
+
+🥞 Go-Jamming Admin
+
+
+Mentions To Approve
+
+{{ range $domain, $mentions := .Mentions }}
+ 🌐 Domain {{ $domain }} »
+
+ {{ if $mentions }}
+
+
+
+ Source |
+ Target |
+ Content |
+ Approve? |
+ Reject? |
+
+
+
+ {{ range $mentions }}
+
+ {{ .Source }} |
+ {{ .Target }} |
+ {{ .Content }} |
+ ✅ Yes! |
+ ❌ Nop! |
+
+ {{ end }}
+
+
+ {{ else }}
+ No mentions to approve, all done.
+ {{ end }}
+{{ end }}
+
+
+Config
+
+Current config.json
contents:
+
+
+{{ .Config }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/admin/handler.go b/app/admin/handler.go
index 0c7986c..5437aeb 100644
--- a/app/admin/handler.go
+++ b/app/admin/handler.go
@@ -1,25 +1,108 @@
package admin
import (
+ "brainbaking.com/go-jamming/app/mf"
"brainbaking.com/go-jamming/common"
"brainbaking.com/go-jamming/db"
"brainbaking.com/go-jamming/rest"
"fmt"
"github.com/gorilla/mux"
+ "github.com/rs/zerolog/log"
"net/http"
+ "text/template"
)
-func HandleGet(repo db.MentionRepo) http.HandlerFunc {
+import _ "embed"
+
+//go:embed dashboard.html
+var dashboardTemplate []byte
+
+//go:embed moderated.html
+var moderatedTemplate []byte
+
+type dashboardMention struct {
+ Source string
+ Target string
+ Content string
+ ApproveURL string
+ RejectURL string
+}
+
+type dashboardData struct {
+ Config string
+ Mentions map[string][]dashboardMention
+}
+
+type dashboardModerated struct {
+ Action string
+ Item string
+ RedirectURL string
+}
+
+func indiewebDataToDashboardMention(c *common.Config, dbMentions []*mf.IndiewebData) []dashboardMention {
+ var mentions []dashboardMention
+ for _, dbMention := range dbMentions {
+ wm := dbMention.AsMention()
+ // TODO move this to somewhere else? the wm? duplicate in notifier.go
+ approveUrl := fmt.Sprintf("%sadmin/approve/%s/%s", c.BaseURL, c.Token, wm.Key())
+ rejectUrl := fmt.Sprintf("%sadmin/reject/%s/%s", c.BaseURL, c.Token, wm.Key())
+
+ mentions = append(mentions, dashboardMention{
+ Source: dbMention.Source,
+ Target: dbMention.Target,
+ Content: dbMention.Content,
+ ApproveURL: approveUrl,
+ RejectURL: rejectUrl,
+ })
+ }
+
+ return mentions
+}
+
+func getDashboardData(c *common.Config, repo db.MentionRepo) *dashboardData {
+ data := &dashboardData{
+ Config: c.String(),
+ Mentions: map[string][]dashboardMention{},
+ }
+ for _, domain := range c.AllowedWebmentionSources {
+ data.Mentions[domain] = indiewebDataToDashboardMention(c, repo.GetAllToModerate(domain).Data)
+ }
+
+ return data
+}
+
+func asTemplate(name string, data []byte) *template.Template {
+ tmpl, err := template.New(name).Parse(string(data))
+ if err != nil {
+ log.Fatal().Err(err).Str("name", name).Msg("Template invalid")
+ }
+ return tmpl
+}
+
+func HandleGet(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
+ tmpl := asTemplate("dashboard", dashboardTemplate)
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ err := tmpl.Execute(w, getDashboardData(c, repo))
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Error().Err(err).Msg("Unable to fill in dashboard template")
+ }
+ }
+}
+
+func HandleGetToApprove(repo db.MentionRepo) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
domain := mux.Vars(r)["domain"]
rest.Json(w, repo.GetAllToModerate(domain))
}
}
-// TODO unit tests
// HandleApprove approves the Mention (by key in URL) and adds to the whitelist.
// Returns 200 OK with approved source/target or 404 if key is invalid.
func HandleApprove(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
+ tmpl := asTemplate("moderated", moderatedTemplate)
+
return func(w http.ResponseWriter, r *http.Request) {
key := mux.Vars(r)["key"]
@@ -30,14 +113,19 @@ func HandleApprove(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
}
c.AddToWhitelist(approved.AsMention().SourceDomain())
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, "Approved: %s", approved.AsMention().String())
+ err := tmpl.Execute(w, asDashboardModerated("Approved", approved, c))
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Error().Err(err).Msg("Unable to fill in dashboard template")
+ }
}
}
// HandleReject rejects the Mention (by key in URL) and adds to the blacklist.
// Returns 200 OK with rejected source/target or 404 if key is invalid.
func HandleReject(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
+ tmpl := asTemplate("moderated", moderatedTemplate)
+
return func(w http.ResponseWriter, r *http.Request) {
key := mux.Vars(r)["key"]
@@ -48,7 +136,18 @@ func HandleReject(c *common.Config, repo db.MentionRepo) http.HandlerFunc {
}
c.AddToBlacklist(rejected.AsMention().SourceDomain())
- w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, "Rejected: %s", rejected.AsMention().String())
+ err := tmpl.Execute(w, asDashboardModerated("Rejected", rejected, c))
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ log.Error().Err(err).Msg("Unable to fill in dashboard template")
+ }
+ }
+}
+
+func asDashboardModerated(action string, mention *mf.IndiewebData, c *common.Config) dashboardModerated {
+ return dashboardModerated{
+ Action: action,
+ Item: mention.AsMention().String(),
+ RedirectURL: fmt.Sprintf("%sadmin/%s", c.BaseURL, c.Token),
}
}
diff --git a/app/admin/handler_test.go b/app/admin/handler_test.go
new file mode 100644
index 0000000..5764809
--- /dev/null
+++ b/app/admin/handler_test.go
@@ -0,0 +1,127 @@
+package admin
+
+import (
+ "brainbaking.com/go-jamming/app/mf"
+ "brainbaking.com/go-jamming/common"
+ "brainbaking.com/go-jamming/db"
+ "fmt"
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+)
+
+var (
+ cnf = common.Configure()
+ repo db.MentionRepo
+)
+
+func init() {
+ repo = db.NewMentionRepo(cnf)
+}
+
+func TestHandleGet(t *testing.T) {
+ wm := mf.Mention{
+ Source: "https://infos.by/markdown-v-nauke/",
+ Target: "https://brainbaking.com/post/2021/02/writing-academic-papers-in-markdown/",
+ }
+
+ repo.InModeration(wm, &mf.IndiewebData{
+ Source: wm.Source,
+ Target: wm.Target,
+ Name: "mytest",
+ })
+ r := mux.NewRouter()
+ r.HandleFunc("/admin/{token}", HandleGet(cnf, repo)).Methods("GET")
+ ts := httptest.NewServer(r)
+
+ t.Cleanup(func() {
+ os.Remove("config.json")
+ ts.Close()
+ db.Purge()
+ })
+
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/admin/%s", ts.URL, cnf.Token), nil)
+
+ resp, err := client.Do(req)
+ assert.NoError(t, err)
+
+ contentBytes, _ := ioutil.ReadAll(resp.Body)
+ content := string(contentBytes)
+ defer resp.Body.Close()
+
+ assert.Contains(t, content, "admin dashboard")
+ assert.Contains(t, content, wm.Source)
+ assert.Contains(t, content, wm.Target)
+}
+
+func TestHandleReject(t *testing.T) {
+ wm := mf.Mention{
+ Source: "https://infos.by/markdown-v-nauke/",
+ Target: "https://brainbaking.com/post/2021/02/writing-academic-papers-in-markdown/",
+ }
+
+ key, _ := repo.InModeration(wm, &mf.IndiewebData{
+ Source: wm.Source,
+ Target: wm.Target,
+ Name: "mytest",
+ })
+ assert.NotEmpty(t, repo.GetAllToModerate("brainbaking.com").Data)
+
+ r := mux.NewRouter()
+ r.HandleFunc("/admin/reject/{token}/{key}", HandleReject(cnf, repo)).Methods("GET")
+ ts := httptest.NewServer(r)
+
+ t.Cleanup(func() {
+ os.Remove("config.json")
+ ts.Close()
+ db.Purge()
+ })
+
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/admin/reject/%s/%s", ts.URL, cnf.Token, key), nil)
+
+ _, err = client.Do(req)
+ assert.NoError(t, err)
+
+ assert.Empty(t, repo.GetAllToModerate("brainbaking.com").Data)
+ assert.Empty(t, repo.GetAll("brainbaking.com").Data)
+}
+
+func TestHandleApprove(t *testing.T) {
+ wm := mf.Mention{
+ Source: "https://infos.by/markdown-v-nauke/",
+ Target: "https://brainbaking.com/post/2021/02/writing-academic-papers-in-markdown/",
+ }
+
+ key, _ := repo.InModeration(wm, &mf.IndiewebData{
+ Source: wm.Source,
+ Target: wm.Target,
+ Name: "mytest",
+ })
+ assert.NotEmpty(t, repo.GetAllToModerate("brainbaking.com").Data)
+
+ r := mux.NewRouter()
+ // just using httptest.NewServer(r.HandleFunc("url", HandleApprove(...)) won't get the context vars into the mux
+ r.HandleFunc("/admin/approve/{token}/{key}", HandleApprove(cnf, repo)).Methods("GET")
+ ts := httptest.NewServer(r)
+
+ t.Cleanup(func() {
+ os.Remove("config.json")
+ ts.Close()
+ db.Purge()
+ })
+
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/admin/approve/%s/%s", ts.URL, cnf.Token, key), nil)
+
+ _, err = client.Do(req)
+ assert.NoError(t, err)
+
+ assert.Empty(t, repo.GetAllToModerate("brainbaking.com").Data)
+ assert.NotEmpty(t, repo.GetAll("brainbaking.com").Data)
+}
diff --git a/app/admin/moderated.html b/app/admin/moderated.html
new file mode 100644
index 0000000..6489fdc
--- /dev/null
+++ b/app/admin/moderated.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Go-Jamming moderation: {{ .Action }}
+
+
+
+
+🥞 Go-Jamming Moderation done!
+
+{{ .Action }}: {{ .Item }}
+
+
+
+Thanks 👍. You'll be redirected to the admin dashboard...
+If you cannot wait, please click here.
+
+
+
\ No newline at end of file
diff --git a/app/routes.go b/app/routes.go
index cb68d2f..2d15ae6 100644
--- a/app/routes.go
+++ b/app/routes.go
@@ -24,7 +24,8 @@ func (s *server) routes() {
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandlePut(c, db))).Methods("PUT")
s.router.HandleFunc("/webmention/{domain}/{token}", s.domainAndTokenOnly(webmention.HandleDelete(db))).Methods("DELETE")
- s.router.HandleFunc("/admin/{domain}/{token}", s.domainAndTokenOnly(admin.HandleGet(db))).Methods("GET")
+ s.router.HandleFunc("/admin/{token}", s.authorizedOnly(admin.HandleGet(c, db))).Methods("GET")
+ s.router.HandleFunc("/admin/{domain}/{token}", s.domainAndTokenOnly(admin.HandleGetToApprove(db))).Methods("GET")
s.router.HandleFunc("/admin/approve/{token}/{key}", s.authorizedOnly(admin.HandleApprove(c, db))).Methods("GET")
s.router.HandleFunc("/admin/reject/{token}/{key}", s.authorizedOnly(admin.HandleReject(c, db))).Methods("GET")
}
diff --git a/app/webmention/handler_test.go b/app/webmention/handler_test.go
index bdf6515..c3f338a 100644
--- a/app/webmention/handler_test.go
+++ b/app/webmention/handler_test.go
@@ -46,6 +46,7 @@ func TestHandleDelete(t *testing.T) {
ts := httptest.NewServer(HandleDelete(repo))
defer ts.Close()
+ defer db.Purge()
client := &http.Client{}
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s?source=%s&target=%s", ts.URL, wm.Source, wm.Target), nil)
@@ -59,6 +60,8 @@ func TestHandleDelete(t *testing.T) {
func TestHandlePostWithInvalidUrlsShouldReturnBadRequest(t *testing.T) {
ts := httptest.NewServer(HandlePost(cnf, repo))
defer ts.Close()
+ defer db.Purge()
+
res, err := http.PostForm(ts.URL, postWm("https://haha.be/woof/said/the/dog.txt", "https://pussies.nl/mycatjustthrewup/gottacleanup.html"))
assert.NoError(t, err)
@@ -72,6 +75,8 @@ func TestHandlePostWithInvalidUrlsShouldReturnBadRequest(t *testing.T) {
func TestHandlePostWithTestServer_Parallel(t *testing.T) {
ts := httptest.NewServer(HandlePost(cnf, repo))
defer ts.Close()
+ defer db.Purge()
+
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
diff --git a/common/config.go b/common/config.go
index a101087..6bc253b 100644
--- a/common/config.go
+++ b/common/config.go
@@ -17,7 +17,6 @@ type Config struct {
Port int `json:"port"`
Token string `json:"token"`
UtcOffset int `json:"utcOffset"`
- DataPath string `json:"dataPath"`
AllowedWebmentionSources []string `json:"allowedWebmentionSources"`
Blacklist []string `json:"blacklist"`
Whitelist []string `json:"whitelist"`
@@ -103,9 +102,13 @@ func addToList(key string, arr []string) []string {
return append(arr, key)
}
+func (c *Config) String() string {
+ bytes, _ := json.MarshalIndent(c, "", " ")
+ return string(bytes)
+}
+
func (c *Config) Save() {
- bytes, _ := json.Marshal(c) // we assume a correct internral state here
- err := ioutil.WriteFile("config.json", bytes, fs.ModePerm)
+ err := ioutil.WriteFile("config.json", []byte(c.String()), fs.ModePerm)
if err != nil {
log.Err(err).Msg("Unable to save config.json to disk!")
}