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 @@ - + - - - + + + - ORLY? + ORLY? - - - + + + - - + + - - + diff --git a/app/web/rollup.config.js b/app/web/rollup.config.js index 1c2e15e..819ad90 100644 --- a/app/web/rollup.config.js +++ b/app/web/rollup.config.js @@ -1,78 +1,78 @@ -import { spawn } from 'child_process'; -import svelte from 'rollup-plugin-svelte'; -import commonjs from '@rollup/plugin-commonjs'; -import terser from '@rollup/plugin-terser'; -import resolve from '@rollup/plugin-node-resolve'; -import livereload from 'rollup-plugin-livereload'; -import css from 'rollup-plugin-css-only'; +import { spawn } from "child_process"; +import svelte from "rollup-plugin-svelte"; +import commonjs from "@rollup/plugin-commonjs"; +import terser from "@rollup/plugin-terser"; +import resolve from "@rollup/plugin-node-resolve"; +import livereload from "rollup-plugin-livereload"; +import css from "rollup-plugin-css-only"; const production = !process.env.ROLLUP_WATCH; function serve() { - let server; + let server; - function toExit() { - if (server) server.kill(0); - } + function toExit() { + if (server) server.kill(0); + } - return { - writeBundle() { - if (server) return; - server = spawn('npm', ['run', 'start', '--', '--dev'], { - stdio: ['ignore', 'inherit', 'inherit'], - shell: true - }); + return { + writeBundle() { + if (server) return; + server = spawn("npm", ["run", "start", "--", "--dev"], { + stdio: ["ignore", "inherit", "inherit"], + shell: true, + }); - process.on('SIGTERM', toExit); - process.on('exit', toExit); - } - }; + process.on("SIGTERM", toExit); + process.on("exit", toExit); + }, + }; } export default { - input: 'src/main.js', - output: { - sourcemap: true, - format: 'iife', - name: 'app', - file: 'dist/bundle.js' - }, - plugins: [ - svelte({ - compilerOptions: { - // enable run-time checks when not in production - dev: !production - } - }), - // we'll extract any component CSS out into - // a separate file - better for performance - css({ output: 'bundle.css' }), + input: "src/main.js", + output: { + sourcemap: true, + format: "iife", + name: "app", + file: "dist/bundle.js", + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production, + }, + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: "bundle.css" }), - // If you have external dependencies installed from - // npm, you'll most likely need these plugins. In - // some cases you'll need additional configuration - - // consult the documentation for details: - // https://github.com/rollup/plugins/tree/master/packages/commonjs - resolve({ - browser: true, - dedupe: ['svelte'], - exportConditions: ['svelte'] - }), - commonjs(), + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ["svelte"], + exportConditions: ["svelte"], + }), + commonjs(), - // In dev mode, call `npm run start` once - // the bundle has been generated - !production && serve(), + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), - // Watch the `public` directory and refresh the - // browser on changes when not in production - !production && livereload('public'), + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload("public"), - // If we're building for production (npm run build - // instead of npm run dev), minify - production && terser() - ], - watch: { - clearScreen: false - } + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser(), + ], + watch: { + clearScreen: false, + }, }; diff --git a/app/web/scripts/setupTypeScript.js b/app/web/scripts/setupTypeScript.js index 4385f65..4aad639 100644 --- a/app/web/scripts/setupTypeScript.js +++ b/app/web/scripts/setupTypeScript.js @@ -13,70 +13,78 @@ rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template */ -import fs from "fs" -import path from "path" -import { argv } from "process" -import url from 'url'; +import fs from "fs"; +import path from "path"; +import { argv } from "process"; +import url from "url"; const __filename = url.fileURLToPath(import.meta.url); -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); -const projectRoot = argv[2] || path.join(__dirname, "..") +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); +const projectRoot = argv[2] || path.join(__dirname, ".."); // Add deps to pkg.json -const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) +const packageJSON = JSON.parse( + fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"), +); packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { "svelte-check": "^3.0.0", "svelte-preprocess": "^5.0.0", "@rollup/plugin-typescript": "^11.0.0", - "typescript": "^4.9.0", - "tslib": "^2.5.0", - "@tsconfig/svelte": "^3.0.0" -}) + typescript: "^4.9.0", + tslib: "^2.5.0", + "@tsconfig/svelte": "^3.0.0", +}); // Add script for checking packageJSON.scripts = Object.assign(packageJSON.scripts, { - "check": "svelte-check" -}) + check: "svelte-check", +}); // Write the package JSON -fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) +fs.writeFileSync( + path.join(projectRoot, "package.json"), + JSON.stringify(packageJSON, null, " "), +); // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too -const beforeMainJSPath = path.join(projectRoot, "src", "main.js") -const afterMainTSPath = path.join(projectRoot, "src", "main.ts") -fs.renameSync(beforeMainJSPath, afterMainTSPath) +const beforeMainJSPath = path.join(projectRoot, "src", "main.js"); +const afterMainTSPath = path.join(projectRoot, "src", "main.ts"); +fs.renameSync(beforeMainJSPath, afterMainTSPath); // Switch the app.svelte file to use TS -const appSveltePath = path.join(projectRoot, "src", "App.svelte") -let appFile = fs.readFileSync(appSveltePath, "utf8") -appFile = appFile.replace("