diff --git a/.gitignore b/.gitignore index c3eb12d..98c1348 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ whitelist.json +users.json khatru-invite .env khatru-badgern-db diff --git a/handler.go b/handler.go index 2298fce..3eef717 100644 --- a/handler.go +++ b/handler.go @@ -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()) } diff --git a/main.go b/main.go index 2022a9f..97c3874 100644 --- a/main.go +++ b/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) diff --git a/pages.go b/pages.go index a1bf36f..223d80e 100644 --- a/pages.go +++ b/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"), ) } diff --git a/utils.go b/utils.go index 8c31ee5..5f3c88c 100644 --- a/utils.go +++ b/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"} diff --git a/whitelist.go b/whitelist.go index 563a29c..7f71f8d 100644 --- a/whitelist.go +++ b/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 }