mirror of
https://github.com/github-tijlxyz/khatru-pyramid.git
synced 2025-04-19 10:21:18 +00:00
rework data model for invitations.
This commit is contained in:
parent
f850a485cb
commit
0c94ef4e34
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
whitelist.json
|
||||
users.json
|
||||
khatru-invite
|
||||
.env
|
||||
khatru-badgern-db
|
||||
|
@ -25,22 +25,22 @@ func addToWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
pubkey = value.(string)
|
||||
}
|
||||
|
||||
if err := addToWhitelist(r.Context(), pubkey, loggedUser); err != nil {
|
||||
if err := addToWhitelist(pubkey, loggedUser); err != nil {
|
||||
http.Error(w, "failed to add to whitelist: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
|
||||
content := buildInviteTree(r.Context(), "", loggedUser)
|
||||
htmlgo.Fprint(w, content, r.Context())
|
||||
}
|
||||
|
||||
func removeFromWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
loggedUser := getLoggedUser(r)
|
||||
pubkey := r.PostFormValue("pubkey")
|
||||
if err := removeFromWhitelist(r.Context(), pubkey, loggedUser); err != nil {
|
||||
if err := removeFromWhitelist(pubkey, loggedUser); err != nil {
|
||||
http.Error(w, "failed to remove from whitelist: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
|
||||
content := buildInviteTree(r.Context(), "", loggedUser)
|
||||
htmlgo.Fprint(w, content, r.Context())
|
||||
}
|
||||
|
||||
|
14
main.go
14
main.go
@ -22,9 +22,10 @@ type Settings struct {
|
||||
}
|
||||
|
||||
var (
|
||||
db badgern.BadgerBackend
|
||||
s Settings
|
||||
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||
db badgern.BadgerBackend
|
||||
s Settings
|
||||
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||
whitelist = make(Whitelist)
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -44,20 +45,21 @@ func main() {
|
||||
|
||||
// load whitelist storage
|
||||
if err := loadWhitelist(); err != nil {
|
||||
panic(err)
|
||||
log.Fatal().Err(err).Msg("failed to load whitelist")
|
||||
return
|
||||
}
|
||||
|
||||
// load db
|
||||
db = badgern.BadgerBackend{Path: "./khatru-badgern-db"}
|
||||
if err := db.Init(); err != nil {
|
||||
panic(err)
|
||||
log.Fatal().Err(err).Msg("failed to initialize database")
|
||||
return
|
||||
}
|
||||
|
||||
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||
relay.RejectEvent = append(relay.RejectEvent, whitelistRejecter)
|
||||
|
||||
relay.Router().HandleFunc("/reports", reportsViewerHandler)
|
||||
relay.Router().HandleFunc("/add-to-whitelist", addToWhitelistHandler)
|
||||
|
27
pages.go
27
pages.go
@ -78,31 +78,38 @@ func inviteTreePageHTML(ctx context.Context, params InviteTreePageParams) HTMLCo
|
||||
Input("pubkey").Type("text").Placeholder("npub1...").Class("w-96 rounded-md border-0 p-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600"),
|
||||
Button("invite").Class(buttonClass+" p-2 bg-white hover:bg-gray-50"),
|
||||
Div(
|
||||
buildInviteTree(ctx, s.RelayPubkey, params.LoggedUser),
|
||||
buildInviteTree(ctx, "", params.LoggedUser),
|
||||
).Id("tree").Class("mt-3"),
|
||||
).Attr("hx-post", "/add-to-whitelist", "hx-trigger", "submit", "hx-target", "#tree")
|
||||
).Attr(
|
||||
"hx-post", "/add-to-whitelist",
|
||||
"hx-trigger", "submit",
|
||||
"hx-target", "#tree",
|
||||
"_", "on htmx:afterRequest(elt, successful) if successful and elt is I call I.reset()",
|
||||
)
|
||||
}
|
||||
|
||||
func buildInviteTree(ctx context.Context, invitedBy string, loggedUser string) HTMLComponent {
|
||||
func buildInviteTree(ctx context.Context, inviter string, loggedUser string) HTMLComponent {
|
||||
children := make([]HTMLComponent, 0, len(whitelist))
|
||||
for _, entry := range whitelist {
|
||||
if entry.InvitedBy == invitedBy {
|
||||
user := getUserInfo(ctx, entry.PublicKey)
|
||||
for pubkey, invitedBy := range whitelist {
|
||||
if invitedBy == inviter {
|
||||
user := getUserInfo(ctx, pubkey)
|
||||
button := Span("")
|
||||
if invitedBy == loggedUser {
|
||||
if isAncestorOf(loggedUser, pubkey) && loggedUser != pubkey {
|
||||
button = Button("remove").
|
||||
Class(buttonClass+" px-2 bg-red-100 hover:bg-red-300").
|
||||
Attr("hx-post", "/remove-from-whitelist",
|
||||
Attr(
|
||||
"hx-post", "/remove-from-whitelist",
|
||||
"hx-trigger", "click",
|
||||
"hx-target", "#tree",
|
||||
"hx-vals", `{"pubkey": "`+entry.PublicKey+`"}`)
|
||||
"hx-vals", `{"pubkey": "`+pubkey+`"}`,
|
||||
)
|
||||
}
|
||||
|
||||
children = append(children,
|
||||
Li(
|
||||
A().Href("nostr:"+user.Npub).Text(user.Name).Class("font-mono py-1"),
|
||||
button,
|
||||
buildInviteTree(ctx, entry.PublicKey, loggedUser),
|
||||
buildInviteTree(ctx, pubkey, loggedUser),
|
||||
).Class("ml-4"),
|
||||
)
|
||||
}
|
||||
|
49
utils.go
49
utils.go
@ -1,58 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func isPublicKeyInWhitelist(pubkey string) bool {
|
||||
if pubkey == s.RelayPubkey {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := 0; i < len(whitelist); i++ {
|
||||
if whitelist[i].PublicKey == pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func deleteFromWhitelistRecursively(ctx context.Context, target string) {
|
||||
var updatedWhitelist []WhitelistEntry
|
||||
var queue []string
|
||||
|
||||
// Remove from whitelist
|
||||
for _, user := range whitelist {
|
||||
if user.PublicKey != target {
|
||||
updatedWhitelist = append(updatedWhitelist, user)
|
||||
}
|
||||
if user.InvitedBy == target {
|
||||
queue = append(queue, user.PublicKey)
|
||||
}
|
||||
}
|
||||
whitelist = updatedWhitelist
|
||||
|
||||
// Remove all events
|
||||
filter := nostr.Filter{
|
||||
Authors: []string{target},
|
||||
}
|
||||
events, _ := db.QueryEvents(ctx, filter)
|
||||
for ev := range events {
|
||||
err := db.DeleteEvent(ctx, ev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to delete event")
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive
|
||||
for _, pk := range queue {
|
||||
deleteFromWhitelistRecursively(ctx, pk)
|
||||
}
|
||||
}
|
||||
|
||||
func getProfileInfoFromJson(jsonStr string) (string, string) {
|
||||
fieldOrder := []string{"displayName", "display_name", "username", "name"}
|
||||
|
||||
|
130
whitelist.go
130
whitelist.go
@ -1,111 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type WhitelistEntry struct {
|
||||
InvitedBy string `json:"invited_by"`
|
||||
PublicKey string `json:"pk"`
|
||||
}
|
||||
const WHITELIST_FILE = "users.json"
|
||||
|
||||
var whitelist []WhitelistEntry
|
||||
type Whitelist map[string]string // { [user_pubkey]: [invited_by] }
|
||||
|
||||
func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
|
||||
// check if user in whitelist
|
||||
if !isPublicKeyInWhitelist(evt.PubKey) {
|
||||
return true, "You are not invited to this relay"
|
||||
}
|
||||
|
||||
/*
|
||||
kind 20201
|
||||
invited/whitelisted user invites new user
|
||||
*/
|
||||
if evt.Kind == 20201 {
|
||||
}
|
||||
|
||||
/*
|
||||
kind 20202
|
||||
p tag = user removes user they invited OR admin removes user
|
||||
e tag = admin removes event
|
||||
*/
|
||||
if evt.Kind == 20202 {
|
||||
pTags := evt.Tags.GetAll([]string{"p"})
|
||||
for _, tag := range pTags {
|
||||
for _, user := range whitelist {
|
||||
/*
|
||||
1: User in whitelist
|
||||
2: Cant remove self
|
||||
3: User should have invited user OR be relay admin
|
||||
*/
|
||||
if user.PublicKey == tag.Value() && evt.PubKey != tag.Value() && (user.InvitedBy == evt.PubKey || evt.PubKey == s.RelayPubkey) {
|
||||
log.Info().Str("user", tag.Value()).Msg("deleting user")
|
||||
deleteFromWhitelistRecursively(ctx, tag.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
if evt.PubKey == s.RelayPubkey {
|
||||
eTags := evt.Tags.GetAll([]string{"e"})
|
||||
for _, tag := range eTags {
|
||||
filter := nostr.Filter{
|
||||
IDs: []string{tag.Value()},
|
||||
}
|
||||
events, _ := db.QueryEvents(ctx, filter)
|
||||
|
||||
for evt := range events {
|
||||
log.Info().Str("event", evt.ID).Msg("deleting event")
|
||||
err := db.DeleteEvent(ctx, evt)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to delete event")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func addToWhitelist(ctx context.Context, pubkey string, inviter string) error {
|
||||
func addToWhitelist(pubkey string, inviter string) error {
|
||||
if nostr.IsValidPublicKeyHex(pubkey) && isPublicKeyInWhitelist(inviter) && !isPublicKeyInWhitelist(pubkey) {
|
||||
whitelist = append(whitelist, WhitelistEntry{PublicKey: pubkey, InvitedBy: inviter})
|
||||
whitelist[pubkey] = inviter
|
||||
}
|
||||
return saveWhitelist()
|
||||
}
|
||||
|
||||
func removeFromWhitelist(ctx context.Context, pubkey string, deleter string) error {
|
||||
idx := slices.IndexFunc(whitelist, func(we WhitelistEntry) bool { return we.PublicKey == pubkey })
|
||||
if idx == -1 {
|
||||
return nil
|
||||
func isPublicKeyInWhitelist(pubkey string) bool {
|
||||
_, ok := whitelist[pubkey]
|
||||
return ok
|
||||
}
|
||||
|
||||
func isAncestorOf(pubkey string, target string) bool {
|
||||
ancestor := target // we must find out if we are an ancestor of the target, but we can delete ourselves
|
||||
for {
|
||||
if ancestor == pubkey {
|
||||
break
|
||||
}
|
||||
|
||||
parent, ok := whitelist[ancestor]
|
||||
if !ok {
|
||||
// parent is not in whitelist, this means this is a top-level user and can
|
||||
// only be deleted by manually editing the users.json file
|
||||
return false
|
||||
}
|
||||
|
||||
ancestor = parent
|
||||
}
|
||||
if whitelist[idx].InvitedBy != deleter {
|
||||
return fmt.Errorf("can't remove a user you haven't invited")
|
||||
return true
|
||||
}
|
||||
|
||||
func removeFromWhitelist(target string, deleter string) error {
|
||||
// check if this user is a descendant of the user who issued the delete command
|
||||
if !isAncestorOf(deleter, target) {
|
||||
return fmt.Errorf("insufficient permissions to delete this")
|
||||
}
|
||||
|
||||
whitelist = append(whitelist[0:idx], whitelist[idx+1:]...)
|
||||
// if we got here that means we have permission to delete the target
|
||||
delete(whitelist, target)
|
||||
|
||||
// delete all people who were invited by the target
|
||||
removeDescendantsFromWhitelist(target)
|
||||
|
||||
return saveWhitelist()
|
||||
}
|
||||
|
||||
func removeDescendantsFromWhitelist(ancestor string) {
|
||||
for pubkey, inviter := range whitelist {
|
||||
if inviter == ancestor {
|
||||
delete(whitelist, pubkey)
|
||||
removeDescendantsFromWhitelist(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadWhitelist() error {
|
||||
if _, err := os.Stat("whitelist.json"); os.IsNotExist(err) {
|
||||
whitelist = []WhitelistEntry{}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileContent, err := os.ReadFile("whitelist.json")
|
||||
b, err := os.ReadFile(WHITELIST_FILE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(fileContent, &whitelist); err != nil {
|
||||
if err := json.Unmarshal(b, &whitelist); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -118,7 +86,7 @@ func saveWhitelist() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile("whitelist.json", jsonBytes, 0644); err != nil {
|
||||
if err := os.WriteFile(WHITELIST_FILE, jsonBytes, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user