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
264 lines
8.0 KiB
3 years ago
|
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, "<", "<"), postTimeString,
|
||
|
post.SpatialKey64,
|
||
|
strings.ReplaceAll(post.TextContent, "<", "<"),
|
||
|
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, "<", "<"), objectAuthorId,
|
||
|
)))
|
||
|
|
||
|
objectAuthor.Ancestry = append(objectAuthor.Ancestry, objectAuthorId)
|
||
|
for i, ancestor := range objectAuthor.Ancestry {
|
||
|
response.Write([]byte(fmt.Sprintf(
|
||
|
`<div>%s--> <a href="/admin/%s">%s</a></div>`,
|
||
|
strings.Repeat(" ", 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>>></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)
|
||
|
|
||
|
})
|
||
|
}
|