mirror of
https://github.com/github-tijlxyz/khatru-pyramid.git
synced 2025-06-06 18:31:02 +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
|
whitelist.json
|
||||||
|
users.json
|
||||||
khatru-invite
|
khatru-invite
|
||||||
.env
|
.env
|
||||||
khatru-badgern-db
|
khatru-badgern-db
|
||||||
|
@ -25,22 +25,22 @@ func addToWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
pubkey = value.(string)
|
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)
|
http.Error(w, "failed to add to whitelist: "+err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
|
content := buildInviteTree(r.Context(), "", loggedUser)
|
||||||
htmlgo.Fprint(w, content, r.Context())
|
htmlgo.Fprint(w, content, r.Context())
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFromWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
func removeFromWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
loggedUser := getLoggedUser(r)
|
loggedUser := getLoggedUser(r)
|
||||||
pubkey := r.PostFormValue("pubkey")
|
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)
|
http.Error(w, "failed to remove from whitelist: "+err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content := buildInviteTree(r.Context(), s.RelayPubkey, loggedUser)
|
content := buildInviteTree(r.Context(), "", loggedUser)
|
||||||
htmlgo.Fprint(w, content, r.Context())
|
htmlgo.Fprint(w, content, r.Context())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
main.go
8
main.go
@ -25,6 +25,7 @@ var (
|
|||||||
db badgern.BadgerBackend
|
db badgern.BadgerBackend
|
||||||
s Settings
|
s Settings
|
||||||
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger()
|
||||||
|
whitelist = make(Whitelist)
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -44,20 +45,21 @@ func main() {
|
|||||||
|
|
||||||
// load whitelist storage
|
// load whitelist storage
|
||||||
if err := loadWhitelist(); err != nil {
|
if err := loadWhitelist(); err != nil {
|
||||||
panic(err)
|
log.Fatal().Err(err).Msg("failed to load whitelist")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// load db
|
// load db
|
||||||
db = badgern.BadgerBackend{Path: "./khatru-badgern-db"}
|
db = badgern.BadgerBackend{Path: "./khatru-badgern-db"}
|
||||||
if err := db.Init(); err != nil {
|
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.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
|
||||||
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
|
||||||
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
relay.CountEvents = append(relay.CountEvents, db.CountEvents)
|
||||||
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
|
||||||
relay.RejectEvent = append(relay.RejectEvent, whitelistRejecter)
|
|
||||||
|
|
||||||
relay.Router().HandleFunc("/reports", reportsViewerHandler)
|
relay.Router().HandleFunc("/reports", reportsViewerHandler)
|
||||||
relay.Router().HandleFunc("/add-to-whitelist", addToWhitelistHandler)
|
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"),
|
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"),
|
Button("invite").Class(buttonClass+" p-2 bg-white hover:bg-gray-50"),
|
||||||
Div(
|
Div(
|
||||||
buildInviteTree(ctx, s.RelayPubkey, params.LoggedUser),
|
buildInviteTree(ctx, "", params.LoggedUser),
|
||||||
).Id("tree").Class("mt-3"),
|
).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))
|
children := make([]HTMLComponent, 0, len(whitelist))
|
||||||
for _, entry := range whitelist {
|
for pubkey, invitedBy := range whitelist {
|
||||||
if entry.InvitedBy == invitedBy {
|
if invitedBy == inviter {
|
||||||
user := getUserInfo(ctx, entry.PublicKey)
|
user := getUserInfo(ctx, pubkey)
|
||||||
button := Span("")
|
button := Span("")
|
||||||
if invitedBy == loggedUser {
|
if isAncestorOf(loggedUser, pubkey) && loggedUser != pubkey {
|
||||||
button = Button("remove").
|
button = Button("remove").
|
||||||
Class(buttonClass+" px-2 bg-red-100 hover:bg-red-300").
|
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-trigger", "click",
|
||||||
"hx-target", "#tree",
|
"hx-target", "#tree",
|
||||||
"hx-vals", `{"pubkey": "`+entry.PublicKey+`"}`)
|
"hx-vals", `{"pubkey": "`+pubkey+`"}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
children = append(children,
|
children = append(children,
|
||||||
Li(
|
Li(
|
||||||
A().Href("nostr:"+user.Npub).Text(user.Name).Class("font-mono py-1"),
|
A().Href("nostr:"+user.Npub).Text(user.Name).Class("font-mono py-1"),
|
||||||
button,
|
button,
|
||||||
buildInviteTree(ctx, entry.PublicKey, loggedUser),
|
buildInviteTree(ctx, pubkey, loggedUser),
|
||||||
).Class("ml-4"),
|
).Class("ml-4"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
49
utils.go
49
utils.go
@ -1,58 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"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) {
|
func getProfileInfoFromJson(jsonStr string) (string, string) {
|
||||||
fieldOrder := []string{"displayName", "display_name", "username", "name"}
|
fieldOrder := []string{"displayName", "display_name", "username", "name"}
|
||||||
|
|
||||||
|
132
whitelist.go
132
whitelist.go
@ -1,111 +1,79 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WhitelistEntry struct {
|
const WHITELIST_FILE = "users.json"
|
||||||
InvitedBy string `json:"invited_by"`
|
|
||||||
PublicKey string `json:"pk"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var whitelist []WhitelistEntry
|
type Whitelist map[string]string // { [user_pubkey]: [invited_by] }
|
||||||
|
|
||||||
func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg string) {
|
func addToWhitelist(pubkey string, inviter string) error {
|
||||||
// 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 {
|
|
||||||
if nostr.IsValidPublicKeyHex(pubkey) && isPublicKeyInWhitelist(inviter) && !isPublicKeyInWhitelist(pubkey) {
|
if nostr.IsValidPublicKeyHex(pubkey) && isPublicKeyInWhitelist(inviter) && !isPublicKeyInWhitelist(pubkey) {
|
||||||
whitelist = append(whitelist, WhitelistEntry{PublicKey: pubkey, InvitedBy: inviter})
|
whitelist[pubkey] = inviter
|
||||||
}
|
}
|
||||||
return saveWhitelist()
|
return saveWhitelist()
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFromWhitelist(ctx context.Context, pubkey string, deleter string) error {
|
func isPublicKeyInWhitelist(pubkey string) bool {
|
||||||
idx := slices.IndexFunc(whitelist, func(we WhitelistEntry) bool { return we.PublicKey == pubkey })
|
_, ok := whitelist[pubkey]
|
||||||
if idx == -1 {
|
return ok
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if whitelist[idx].InvitedBy != deleter {
|
|
||||||
return fmt.Errorf("can't remove a user you haven't invited")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
whitelist = append(whitelist[0:idx], whitelist[idx+1:]...)
|
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
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
return saveWhitelist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeDescendantsFromWhitelist(ancestor string) {
|
||||||
|
for pubkey, inviter := range whitelist {
|
||||||
|
if inviter == ancestor {
|
||||||
|
delete(whitelist, pubkey)
|
||||||
|
removeDescendantsFromWhitelist(pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadWhitelist() error {
|
func loadWhitelist() error {
|
||||||
if _, err := os.Stat("whitelist.json"); os.IsNotExist(err) {
|
b, err := os.ReadFile(WHITELIST_FILE)
|
||||||
whitelist = []WhitelistEntry{}
|
|
||||||
return nil
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileContent, err := os.ReadFile("whitelist.json")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(fileContent, &whitelist); err != nil {
|
if err := json.Unmarshal(b, &whitelist); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +86,7 @@ func saveWhitelist() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile("whitelist.json", jsonBytes, 0644); err != nil {
|
if err := os.WriteFile(WHITELIST_FILE, jsonBytes, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user