diff --git a/handler.go b/handler.go index e9a78a1..6deb792 100644 --- a/handler.go +++ b/handler.go @@ -19,6 +19,26 @@ func inviteTreeHandler(w http.ResponseWriter, r *http.Request) { htmlgo.Fprint(w, baseHTML(content), r.Context()) } +func addToWhitelistHandler(w http.ResponseWriter, r *http.Request) { + pubkey := r.PostFormValue("pubkey") + if err := addToWhitelist(r.Context(), pubkey, s.RelayPubkey); err != nil { + http.Error(w, "failed to add to whitelist: "+err.Error(), 500) + return + } + content := buildInviteTree(r.Context(), s.RelayPubkey) + htmlgo.Fprint(w, content, r.Context()) +} + +func removeFromWhitelistHandler(w http.ResponseWriter, r *http.Request) { + pubkey := r.PostFormValue("pubkey") + if err := removeFromWhitelist(r.Context(), pubkey); err != nil { + http.Error(w, "failed to remove from whitelist: "+err.Error(), 500) + return + } + content := buildInviteTree(r.Context(), s.RelayPubkey) + htmlgo.Fprint(w, content, r.Context()) +} + func reportsViewerHandler(w http.ResponseWriter, r *http.Request) { // var formattedReportsData template.HTML = "" diff --git a/justfile b/justfile new file mode 100644 index 0000000..b3b9673 --- /dev/null +++ b/justfile @@ -0,0 +1,2 @@ +dev: + godotenv go run . diff --git a/main.go b/main.go index 8e20e08..b964c40 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,9 @@ package main import ( "net/http" "os" - "os/signal" - "sync" - "syscall" - "time" "github.com/fiatjaf/khatru" "github.com/fiatjaf/khatru/plugins/storage/badgern" - "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "github.com/rs/zerolog" ) @@ -36,25 +31,6 @@ func main() { return } - // save whitelist on shutdown - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - handleSignals() - }() - - // backup whitelist every hour - go func() { - for { - time.Sleep(time.Hour) - saveWhitelist() - } - }() - - // init env config - godotenv.Load(".env") - // init relay relay := khatru.NewRelay() @@ -78,11 +54,11 @@ func main() { 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) - // ui relay.Router().HandleFunc("/reports", reportsViewerHandler) + relay.Router().HandleFunc("/add-to-whitelist", addToWhitelistHandler) + relay.Router().HandleFunc("/remove-from-whitelist", removeFromWhitelistHandler) relay.Router().HandleFunc("/users", inviteTreeHandler) relay.Router().HandleFunc("/", homePageHandler) @@ -91,12 +67,3 @@ func main() { log.Fatal().Err(err).Msg("failed to serve") } } - -// save whitelist on shutdown -func handleSignals() { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - <-sigCh - saveWhitelist() - os.Exit(0) -} diff --git a/pages.go b/pages.go index cb04c81..17566fd 100644 --- a/pages.go +++ b/pages.go @@ -6,8 +6,10 @@ import ( . "github.com/theplant/htmlgo" ) +const buttonClass = "rounded-md bg-white p-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + func baseHTML(inside HTMLComponent) HTMLComponent { - navItemClass := "text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 font-medium" + navItemClass := "text-gray-600 hover:bg-gray-200 rounded-md px-3 py-2 font-medium" return HTML( Head( @@ -15,6 +17,8 @@ func baseHTML(inside HTMLComponent) HTMLComponent { Meta().Name("viewport").Content("width=device-width, initial-scale=1"), Title(s.RelayName), Script("").Src("https://cdn.tailwindcss.com"), + Script("").Src("https://unpkg.com/htmx.org@1.9.6"), + Script("").Src("https://unpkg.com/hyperscript.org@0.9.12"), ), Body( Div( @@ -22,12 +26,12 @@ func baseHTML(inside HTMLComponent) HTMLComponent { P().Text(s.RelayDescription).Class("text-lg"), ).Class("mx-auto my-6 text-center"), Nav( - A().Text("information").Href("/").Class(navItemClass), - A().Text("invite tree").Href("/users").Class(navItemClass), - A().Text("reports").Href("/reports").Class(navItemClass), + A().Text("information").Href("/").Class(navItemClass).Attr("hx-boost", "true", "hx-target", "main", "hx-select", "main"), + A().Text("invite tree").Href("/users").Class(navItemClass).Attr("hx-boost", "true", "hx-target", "main", "hx-select", "main"), + A().Text("reports").Href("/reports").Class(navItemClass).Attr("hx-boost", "true", "hx-target", "main", "hx-select", "main"), ).Class("flex flex-1 items-center justify-center"), - Div(inside).Class("m-4"), - ).Class("bg-gray-800 mx-4 my-6 text-white"), + Main(inside).Class("m-4"), + ).Class("mx-4 my-6"), ) } @@ -67,26 +71,28 @@ func homePageHTML(ctx context.Context, params HomePageParams) HTMLComponent { type InviteTreePageParams struct{} func inviteTreePageHTML(ctx context.Context, params InviteTreePageParams) HTMLComponent { - return Div( - Input("").Type("text").Placeholder("npub1..."), - Button("invite"), - buildInviteTree(ctx, ""), - ) + return Form( + 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), + Div( + buildInviteTree(ctx, s.RelayPubkey), + ).Id("tree").Class("mt-3"), + ).Attr("hx-post", "/add-to-whitelist", "hx-trigger", "submit", "hx-target", "#tree") } func buildInviteTree(ctx context.Context, invitedBy string) HTMLComponent { - tree := Ul() + children := make([]HTMLComponent, 0, len(whitelist)) for _, entry := range whitelist { if entry.InvitedBy == invitedBy { user := getUserInfo(ctx, entry.PublicKey) - tree = tree.Children( + children = append(children, Li( - A().Href("nostr:"+user.Npub).Text(user.Name), - A().Text("remove"), + A().Href("nostr:"+user.Npub).Text(user.Name).Class("font-mono"), + Button("remove").Class(buttonClass).Attr("hx-post", "/remove-from-whitelist", "hx-trigger", "click", "hx-target", "#tree"), buildInviteTree(ctx, entry.PublicKey), - ), + ).Class("ml-3"), ) } } - return tree + return Ul(children...) } diff --git a/whitelist.go b/whitelist.go index cb31bf9..7b8d61b 100644 --- a/whitelist.go +++ b/whitelist.go @@ -6,11 +6,12 @@ import ( "os" "github.com/nbd-wtf/go-nostr" + "golang.org/x/exp/slices" ) type WhitelistEntry struct { - PublicKey string `json:"pk"` InvitedBy string `json:"invited_by"` + PublicKey string `json:"pk"` } var whitelist []WhitelistEntry @@ -26,12 +27,6 @@ func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg invited/whitelisted user invites new user */ if evt.Kind == 20201 { - pTags := evt.Tags.GetAll([]string{"p"}) - for _, tag := range pTags { - if nostr.IsValidPublicKeyHex(tag.Value()) && !isPublicKeyInWhitelist(tag.Value()) { - whitelist = append(whitelist, WhitelistEntry{PublicKey: tag.Value(), InvitedBy: evt.PubKey}) - } - } } /* @@ -76,6 +71,23 @@ func whitelistRejecter(ctx context.Context, evt *nostr.Event) (reject bool, msg return false, "" } +func addToWhitelist(ctx context.Context, pubkey string, invitedBy string) error { + if nostr.IsValidPublicKeyHex(pubkey) && !isPublicKeyInWhitelist(pubkey) { + whitelist = append(whitelist, WhitelistEntry{PublicKey: pubkey, InvitedBy: invitedBy}) + } + return saveWhitelist() +} + +func removeFromWhitelist(ctx context.Context, pubkey string) error { + idx := slices.IndexFunc(whitelist, func(we WhitelistEntry) bool { return we.PublicKey == pubkey }) + if idx == -1 { + return nil + } + + whitelist = append(whitelist[0:idx], whitelist[idx+1:]...) + return saveWhitelist() +} + func loadWhitelist() error { if _, err := os.Stat("whitelist.json"); os.IsNotExist(err) { whitelist = []WhitelistEntry{}