TBD
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

264 lines
8.0 KiB

package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/boltdb/bolt"
)
func setupAdminPanelRoutes(mux *http.ServeMux) {
htmlBoilerplateStart := []byte(`<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>graffiti admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: 'Ubuntu','Roboto','Open Sans', sans-serif;
}
.delete-button {
border-radius:4px;border:none;background:red;color:white;padding:4px;font-weight:bold;
}
</style>
</head>
<body>
`)
outputPostHTML := func(response http.ResponseWriter, post *Post) {
postTime, _ := TimeIn(TimeFromMillisecondsSinceUnixEpoch(post.MillisecondsSinceUnixEpoch), "America/Chicago")
postTimeString := postTime.Format(time.RFC1123)
response.Write([]byte(fmt.Sprintf(`
<div>
<a href="/admin/%s">%s aka '%s'</a> at %s:
<button
class="delete-button"
onclick="if(confirm('are you sure you want to delete?')){ window.location='/admin/delete-post/%s'; }"
>
DELETE
</button>
<br/>
TextContent: %s<br/>
<img src="/user-content/%s-small.jpeg" />
</div><hr/>
`,
post.AuthorId, post.AuthorId, strings.ReplaceAll(post.AuthorDisplayName, "<", "&lt;"), postTimeString,
post.SpatialKey64,
strings.ReplaceAll(post.TextContent, "<", "&lt;"),
post.Filename,
)))
}
htmlBoilerplateEnd := []byte(`</body></html>`)
mux.HandleFunc("/admin/delete-post/", func(response http.ResponseWriter, request *http.Request) {
author := getAuthor(db, request)
if author == nil || !author.Admin {
log.Println("/admin 404 unauthorized: graffiti_author_id cookie is missing or invalid")
http.Error(response, "404 not found", http.StatusNotFound)
return
}
spatialKey64 := getLastElementInURLPath(request.URL)
spatialKey, err := base64.StdEncoding.DecodeString(spatialKey64)
if err != nil {
http.Error(response, "400 bad request: malformed spatialKey64", http.StatusBadRequest)
return
}
var postBytes []byte
err = db.View(func(tx *bolt.Tx) error {
postBytes = tx.Bucket([]byte("posts_spatial")).Get(spatialKey)
return nil
})
if postBytes == nil {
http.Error(response, "404 post not found", http.StatusNotFound)
return
}
var post Post
err = json.Unmarshal(postBytes, &post)
if postBytes == nil {
http.Error(response, "500 cant parse post json", http.StatusInternalServerError)
return
}
spatialKey, byAuthorKey, chronologicalKey, err := getIndexKeysForPost(&post)
if err != nil {
http.Error(response, "500 cant getIndexKeysForPost", http.StatusInternalServerError)
return
}
fedi.Publish("content", &FederationEvent{
DeletePosts: &DeletePosts{
//HardDelete: request.URL.Query().Get("hardDelete") == "true",
Keys: []DeleteKey{
{Bucket: "posts_spatial", Key: spatialKey},
{Bucket: "posts_by_author", Key: byAuthorKey},
{Bucket: "posts_chronological", Key: chronologicalKey},
},
},
})
time.Sleep(time.Second)
http.Redirect(response, request, "/admin", http.StatusFound)
})
mux.HandleFunc("/admin/ban-author/", func(response http.ResponseWriter, request *http.Request) {
author := getAuthor(db, request)
if author == nil || !author.Admin {
log.Println("/admin 404 unauthorized: graffiti_author_id cookie is missing or invalid")
http.Error(response, "404 not found", http.StatusNotFound)
return
}
objectAuthorId := getLastElementInURLPath(request.URL)
objectAuthor := getAuthorById(db, objectAuthorId)
if objectAuthor == nil {
http.Error(response, "404 post not found", http.StatusNotFound)
return
}
fedi.Publish("accounts", &FederationEvent{
BanAuthor: &BanAuthor{AuthorId: objectAuthorId},
})
time.Sleep(time.Second)
http.Redirect(response, request, "/admin", http.StatusFound)
})
mux.HandleFunc("/admin/", func(response http.ResponseWriter, request *http.Request) {
author := getAuthor(db, request)
if author == nil || !author.Admin {
log.Println("/admin 404 unauthorized: graffiti_author_id cookie is missing or invalid")
http.Error(response, "404 not found", http.StatusNotFound)
return
}
objectAuthorId := getLastElementInURLPath(request.URL)
objectAuthor := getAuthorById(db, objectAuthorId)
if objectAuthor == nil {
http.Error(response, "404 post not found", http.StatusNotFound)
return
}
posts := []Post{}
err := db.View(func(tx *bolt.Tx) error {
byAuthorBucket := tx.Bucket([]byte("posts_by_author"))
cursor := byAuthorBucket.Cursor()
key, value := cursor.Seek([]byte(fmt.Sprintf("%s", objectAuthorId)))
var post Post
for key != nil && strings.HasPrefix(string(key), objectAuthorId) {
post = Post{}
err := json.Unmarshal(value, &post)
if err != nil {
return err
}
if !post.Deleted {
posts = append(posts, post)
}
key, value = cursor.Prev()
}
return nil
})
if err != nil {
log.Printf("/admin: 500 internal server error: %+v\n", err)
http.Error(response, "500 internal server error", http.StatusInternalServerError)
return
}
response.Header().Set("Content-Type", "text/html")
response.Write(htmlBoilerplateStart)
response.Write([]byte(fmt.Sprintf(
`
<h1>author %s aka '%s'</h1>
<button
class="delete-button"
onclick="if(confirm('are you sure you want to BAN this author and everyone they invited?')){ window.location='/admin/ban-author/%s'; }"
>
BAN THIS AUTHOR & EVERYONE THEY INVITED
</button>
<br/>
<br/><h3>This author's ancestry (chain of authors who invited THIS author):</h3><br/>
`,
objectAuthorId, strings.ReplaceAll(objectAuthor.DisplayName, "<", "&lt;"), objectAuthorId,
)))
objectAuthor.Ancestry = append(objectAuthor.Ancestry, objectAuthorId)
for i, ancestor := range objectAuthor.Ancestry {
response.Write([]byte(fmt.Sprintf(
`<div>%s--&gt; <a href="/admin/%s">%s</a></div>`,
strings.Repeat("&nbsp;&nbsp;&nbsp;&nbsp;", i), ancestor, ancestor,
)))
}
response.Write([]byte("<br/><h3>All posts by this author:</h3><hr/>"))
for _, post := range posts {
outputPostHTML(response, &post)
}
response.Write(htmlBoilerplateEnd)
})
mux.HandleFunc("/admin", func(response http.ResponseWriter, request *http.Request) {
author := getAuthor(db, request)
if author == nil || !author.Admin {
log.Println("/admin 404 unauthorized: graffiti_author_id cookie is missing or invalid")
http.Error(response, "404 not found", http.StatusNotFound)
return
}
page := 0
postsPerPage := 50
pageParam, err := strconv.Atoi(request.URL.Query().Get("page"))
if err == nil {
page = pageParam
}
postsChronological := []Post{}
err = db.View(func(tx *bolt.Tx) error {
chronologicalBucket := tx.Bucket([]byte("posts_chronological"))
cursor := chronologicalBucket.Cursor()
cursor.Seek([]byte(fmt.Sprintf("%d", MillisecondsSinceUnixEpoch())))
key, value := cursor.Prev()
seeked := 0
for key != nil && seeked < page*postsPerPage {
seeked++
key, _ = cursor.Prev()
}
var post Post
for key != nil && len(postsChronological) < postsPerPage {
post = Post{}
err := json.Unmarshal(value, &post)
if err != nil {
return err
}
if !post.Deleted {
postsChronological = append(postsChronological, post)
}
key, value = cursor.Prev()
}
return nil
})
if err != nil {
log.Printf("/admin: 500 internal server error: %+v\n", err)
http.Error(response, "500 internal server error", http.StatusInternalServerError)
return
}
pager := []byte(fmt.Sprintf("<a style=\"font-weight: bold;\" href=\"?page=%d\">NEXT PAGE&gt;&gt;&gt;</a>", page+1))
response.Header().Set("Content-Type", "text/html")
response.Write(htmlBoilerplateStart)
response.Write([]byte(fmt.Sprintf("<h1>postsChronological ?page=%d</h1>\n\n", page)))
response.Write(pager)
response.Write([]byte("<hr/>"))
for _, post := range postsChronological {
outputPostHTML(response, &post)
}
response.Write(pager)
response.Write(htmlBoilerplateEnd)
})
}