diff --git a/.aiassistant/rules/rules.md b/.aiassistant/rules/rules.md index 5dcded8..d9f13d2 100644 --- a/.aiassistant/rules/rules.md +++ b/.aiassistant/rules/rules.md @@ -38,7 +38,7 @@ describing how the item is used. For documentation on package, summarise in up to 3 sentences the functions and purpose of the package -Do not use markdown ** or __ or any similar things in initial words of a bullet +Do not use markdown \*\* or \_\_ or any similar things in initial words of a bullet point, instead use standard godoc style # prefix for header sections ALWAYS separate each bullet point with an empty line, and ALWAYS indent them @@ -90,10 +90,10 @@ A good typical example: ``` -use the source of the relay-tester to help guide what expectations the test has, -and use context7 for information about the nostr protocol, and use additional +use the source of the relay-tester to help guide what expectations the test has, +and use context7 for information about the nostr protocol, and use additional log statements to help locate the cause of bugs always use Go v1.25.1 for everything involving Go -always use the nips repository also for information, found at ../github.com/nostr-protocol/nips attached to the project \ No newline at end of file +always use the nips repository also for information, found at ../github.com/nostr-protocol/nips attached to the project diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8768db4..2c92e4f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,10 +16,9 @@ name: Go on: push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + - "v[0-9]+.[0-9]+.[0-9]+" jobs: - build: runs-on: ubuntu-latest steps: @@ -28,26 +27,25 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.25' + go-version: "1.25" - name: Install libsecp256k1 - run: ./scripts/ubuntu_install_libsecp256k1.sh + run: ./scripts/ubuntu_install_libsecp256k1.sh - name: Build with cgo - run: go build -v ./... + run: go build -v ./... - name: Test with cgo - run: go test -v ./... + run: go test -v ./... - name: Set CGO off - run: echo "CGO_ENABLED=0" >> $GITHUB_ENV + run: echo "CGO_ENABLED=0" >> $GITHUB_ENV - name: Build - run: go build -v ./... + run: go build -v ./... - name: Test - run: go test -v ./... - + run: go test -v ./... # release: # needs: build # runs-on: ubuntu-latest diff --git a/app/server.go b/app/server.go index 7660b92..32df932 100644 --- a/app/server.go +++ b/app/server.go @@ -186,10 +186,8 @@ func (s *Server) UserInterface() { s.mux.HandleFunc("/api/auth/status", s.handleAuthStatus) s.mux.HandleFunc("/api/auth/logout", s.handleAuthLogout) s.mux.HandleFunc("/api/permissions/", s.handlePermissions) - // Export endpoints + // Export endpoint s.mux.HandleFunc("/api/export", s.handleExport) - s.mux.HandleFunc("/api/export/mine", s.handleExportMine) - s.mux.HandleFunc("/export", s.handleExportAll) // Events endpoints s.mux.HandleFunc("/api/events/mine", s.handleEventsMine) // Import endpoint (admin only) @@ -442,9 +440,10 @@ func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) { w.Write(jsonData) } -// handleExport streams all events as JSONL (NDJSON) using NIP-98 authentication. Admins only. +// handleExport streams events as JSONL (NDJSON) using NIP-98 authentication. +// Supports both GET (query params) and POST (JSON body) for pubkey filtering. func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { + if r.Method != http.MethodGet && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } @@ -467,93 +466,55 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { return } - // Optional filtering by pubkey(s) + // Parse pubkeys from request var pks [][]byte - q := r.URL.Query() - for _, pkHex := range q["pubkey"] { - if pkHex == "" { - continue - } - if pk, err := hex.Dec(pkHex); !chk.E(err) { - pks = append(pks, pk) - } - } - - w.Header().Set("Content-Type", "application/x-ndjson") - filename := "events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" - w.Header().Set( - "Content-Disposition", "attachment; filename=\""+filename+"\"", - ) - - // Stream export - s.D.Export(s.Ctx, w, pks...) -} - -// handleExportMine streams only the authenticated user's events as JSONL (NDJSON) using NIP-98 authentication. -func (s *Server) handleExportMine(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - // Validate NIP-98 authentication - valid, pubkey, err := httpauth.CheckAuth(r) - if chk.E(err) || !valid { - errorMsg := "NIP-98 authentication validation failed" - if err != nil { - errorMsg = err.Error() + if r.Method == http.MethodPost { + // Parse JSON body for pubkeys + var requestBody struct { + Pubkeys []string `json:"pubkeys"` } - http.Error(w, errorMsg, http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/x-ndjson") - filename := "my-events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" - w.Header().Set( - "Content-Disposition", "attachment; filename=\""+filename+"\"", - ) - - // Stream export for this user's pubkey only - s.D.Export(s.Ctx, w, pubkey) -} -// handleExportAll streams all events as JSONL (NDJSON) using NIP-98 authentication. Owner only. -func (s *Server) handleExportAll(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Validate NIP-98 authentication - valid, pubkey, err := httpauth.CheckAuth(r) - if chk.E(err) || !valid { - errorMsg := "NIP-98 authentication validation failed" - if err != nil { - errorMsg = err.Error() + if err := json.NewDecoder(r.Body).Decode(&requestBody); err == nil { + // If JSON parsing succeeds, use pubkeys from body + for _, pkHex := range requestBody.Pubkeys { + if pkHex == "" { + continue + } + if pk, err := hex.Dec(pkHex); !chk.E(err) { + pks = append(pks, pk) + } + } + } + // If JSON parsing fails, fall back to empty pubkeys (export all) + } else { + // GET method - parse query parameters + q := r.URL.Query() + for _, pkHex := range q["pubkey"] { + if pkHex == "" { + continue + } + if pk, err := hex.Dec(pkHex); !chk.E(err) { + pks = append(pks, pk) + } } - http.Error(w, errorMsg, http.StatusUnauthorized) - return } - // Check if user has owner permission - accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr) - if accessLevel != "owner" { - http.Error(w, "Owner permission required", http.StatusForbidden) - return + // Determine filename based on whether filtering by pubkeys + var filename string + if len(pks) == 0 { + filename = "all-events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" + } else if len(pks) == 1 { + filename = "my-events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" + } else { + filename = "filtered-events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" } - // Set response headers for file download w.Header().Set("Content-Type", "application/x-ndjson") - filename := "all-events-" + time.Now().UTC().Format("20060102-150405Z") + ".jsonl" w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"") - // Disable write timeouts for this operation - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - - // Stream export of all events - s.D.Export(s.Ctx, w) + // Stream export + s.D.Export(s.Ctx, w, pks...) } // handleEventsMine returns the authenticated user's events in JSON format with pagination using NIP-98 authentication. diff --git a/app/web/public/global.css b/app/web/public/global.css index bb28a94..da4a4db 100644 --- a/app/web/public/global.css +++ b/app/web/public/global.css @@ -1,63 +1,69 @@ -html, body { - position: relative; - width: 100%; - height: 100%; +html, +body { + position: relative; + width: 100%; + height: 100%; } body { - color: #333; - margin: 0; - padding: 8px; - box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: #333; + margin: 0; + padding: 8px; + box-sizing: border-box; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, + Cantarell, "Helvetica Neue", sans-serif; } a { - color: rgb(0,100,200); - text-decoration: none; + color: rgb(0, 100, 200); + text-decoration: none; } a:hover { - text-decoration: underline; + text-decoration: underline; } a:visited { - color: rgb(0,80,160); + color: rgb(0, 80, 160); } label { - display: block; + display: block; } -input, button, select, textarea { - font-family: inherit; - font-size: inherit; - -webkit-padding: 0.4em 0; - padding: 0.4em; - margin: 0 0 0.5em 0; - box-sizing: border-box; - border: 1px solid #ccc; - border-radius: 2px; +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + -webkit-padding: 0.4em 0; + padding: 0.4em; + margin: 0 0 0.5em 0; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 2px; } input:disabled { - color: #ccc; + color: #ccc; } button { - color: #333; - background-color: #f4f4f4; - outline: none; + color: #333; + background-color: #f4f4f4; + outline: none; } button:disabled { - color: #999; + color: #999; } button:not(:disabled):active { - background-color: #ddd; + background-color: #ddd; } button:focus { - border-color: #666; + border-color: #666; } diff --git a/app/web/public/index.html b/app/web/public/index.html index e261e8d..4915348 100644 --- a/app/web/public/index.html +++ b/app/web/public/index.html @@ -1,18 +1,17 @@ - + -
- - + + + -