feat: Add API configuration service and enhance security validation in commands
This commit is contained in:
parent
4072bdaf0d
commit
d99dd77449
13 changed files with 1434 additions and 99 deletions
251
CODE_REVIEW_FINDINGS.md
Normal file
251
CODE_REVIEW_FINDINGS.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Code Review Findings - Mining Project
|
||||
|
||||
**Generated:** 2025-12-31
|
||||
**Reviewed by:** 4 Parallel Code Reviewers (2 Opus, 2 Sonnet)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Domain | Critical | High | Medium | Total |
|
||||
|--------|----------|------|--------|-------|
|
||||
| Core Mining (pkg/mining/) | 0 | 2 | 3 | 5 |
|
||||
| P2P Networking (pkg/node/) | 1 | 0 | 0 | 1 |
|
||||
| CLI Commands (cmd/mining/) | 3 | 3 | 2 | 8 |
|
||||
| Angular Frontend (ui/src/app/) | 1 | 1 | 1 | 3 |
|
||||
| **TOTAL** | **5** | **6** | **6** | **17** |
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### CRIT-001: Path Traversal in Tar Extraction (Zip Slip)
|
||||
- **File:** `pkg/node/bundle.go:268`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 95%
|
||||
|
||||
The `extractTarball` function uses `filepath.Join(destDir, hdr.Name)` without validating the path stays within destination. Malicious tar archives can write files anywhere on the filesystem.
|
||||
|
||||
**Attack Vector:** A remote peer could craft a malicious miner bundle with path traversal entries like `../../../etc/cron.d/malicious`.
|
||||
|
||||
**Fix:**
|
||||
```go
|
||||
cleanName := filepath.Clean(hdr.Name)
|
||||
if strings.HasPrefix(cleanName, "..") || filepath.IsAbs(cleanName) {
|
||||
return "", fmt.Errorf("invalid tar entry: %s", hdr.Name)
|
||||
}
|
||||
path := filepath.Join(destDir, cleanName)
|
||||
if !strings.HasPrefix(filepath.Clean(path), filepath.Clean(destDir)+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("path escape attempt: %s", hdr.Name)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CRIT-002: XSS Vulnerability in Console ANSI-to-HTML
|
||||
- **File:** `ui/src/app/pages/console/console.component.ts:501-549`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 95%
|
||||
|
||||
The `ansiToHtml()` method bypasses Angular XSS protection using `bypassSecurityTrustHtml()` while constructing HTML with inline styles from ANSI escape sequences. Malicious log output could inject scripts.
|
||||
|
||||
**Fix:** Use CSS classes instead of inline styles, validate ANSI codes against whitelist.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-003: Resource Leak in `node serve` Command
|
||||
- **File:** `cmd/mining/cmd/node.go:114-161`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 95%
|
||||
|
||||
The `nodeServeCmd` uses `select {}` to block forever without signal handling. Transport connections and goroutines leak on Ctrl+C.
|
||||
|
||||
**Fix:** Add signal handling and call `transport.Stop()` on shutdown.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-004: Path Traversal in `doctor` Command
|
||||
- **File:** `cmd/mining/cmd/doctor.go:49-55`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 90%
|
||||
|
||||
Reads arbitrary files via manipulated signpost file (`~/.installed-miners`).
|
||||
|
||||
**Fix:** Validate that `configPath` is within expected directories using `filepath.Clean()` and prefix checking.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-005: Path Traversal in `update` Command
|
||||
- **File:** `cmd/mining/cmd/update.go:33-39`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 90%
|
||||
|
||||
Same vulnerability as CRIT-004.
|
||||
|
||||
---
|
||||
|
||||
## High Priority Issues
|
||||
|
||||
### HIGH-001: Race Condition in `requestTimeoutMiddleware`
|
||||
- **File:** `pkg/mining/service.go:313-350`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 85%
|
||||
|
||||
Goroutine calls `c.Next()` while timeout handler may also write to response. Gin's Context is not thread-safe for concurrent writes.
|
||||
|
||||
**Fix:** Use mutex or atomic flag to coordinate response writing.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-002: Missing Rollback in `UpdateProfile`
|
||||
- **File:** `pkg/mining/profile_manager.go:123-133`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 82%
|
||||
|
||||
If `saveProfiles()` fails, in-memory state is already modified. Unlike `CreateProfile`, `UpdateProfile` has no rollback logic.
|
||||
|
||||
**Fix:** Store old profile before update, restore on save failure.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-003: Type Confusion in `update` Command
|
||||
- **File:** `cmd/mining/cmd/update.go:44-47`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 85%
|
||||
|
||||
Unmarshals cache as `[]*mining.InstallationDetails` but `doctor` command saves as `mining.SystemInfo`.
|
||||
|
||||
**Fix:** Use consistent types between commands.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-004: Missing Cleanup in `serve` Command
|
||||
- **File:** `cmd/mining/cmd/serve.go:31-173`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 85%
|
||||
|
||||
No explicit `manager.Stop()` call on shutdown. Relies on implicit service cleanup.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-005: Scanner Error Not Checked
|
||||
- **File:** `cmd/mining/cmd/serve.go:72-162`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 80%
|
||||
|
||||
Interactive shell never calls `scanner.Err()` after loop exits.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-006: Hardcoded HTTP URLs Without TLS
|
||||
- **Files:** `ui/src/app/miner.service.ts:49`, `node.service.ts:66`, `websocket.service.ts:53`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 90%
|
||||
|
||||
All API endpoints use `http://localhost` without TLS. Traffic can be intercepted.
|
||||
|
||||
**Fix:** Use environment-based config with HTTPS/WSS support.
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
### MED-001: Missing `rand.Read` Error Check
|
||||
- **File:** `pkg/mining/auth.go:209-212`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 88%
|
||||
|
||||
`generateNonce()` ignores error from `rand.Read`. Could produce weak nonces.
|
||||
|
||||
---
|
||||
|
||||
### MED-002: Metrics Race in WebSocket Connection
|
||||
- **File:** `pkg/mining/service.go:1369-1373`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 80%
|
||||
|
||||
`RecordWSConnection(true)` called before connection is accepted. Brief incorrect metrics on rejection.
|
||||
|
||||
---
|
||||
|
||||
### MED-003: Config Validation Not Called for Profiles
|
||||
- **File:** `pkg/mining/service.go:978-998`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 82%
|
||||
|
||||
`handleStartMinerWithProfile` doesn't call `config.Validate()` after unmarshaling.
|
||||
|
||||
---
|
||||
|
||||
### MED-004: Weak File Permissions
|
||||
- **File:** `cmd/mining/cmd/doctor.go:106,115`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 80%
|
||||
|
||||
Cache files created with 0644 (world-readable). Should be 0600.
|
||||
|
||||
---
|
||||
|
||||
### MED-005: Duplicated Partial ID Matching
|
||||
- **File:** `cmd/mining/cmd/peer.go:124-131`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 80%
|
||||
|
||||
Partial peer ID matching duplicated across commands. Extract to helper function.
|
||||
|
||||
---
|
||||
|
||||
### MED-006: innerHTML for Sidebar Icons
|
||||
- **File:** `ui/src/app/components/sidebar/sidebar.component.ts:64`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 85%
|
||||
|
||||
Uses `bypassSecurityTrustHtml()` for icons. Currently safe (hardcoded), but fragile.
|
||||
|
||||
---
|
||||
|
||||
## Review Completion Status
|
||||
|
||||
- [x] Domain 1: Core Mining (pkg/mining/) - 5 issues found
|
||||
- [x] Domain 2: P2P Networking (pkg/node/) - 1 critical issue found
|
||||
- [x] Domain 3: CLI Commands (cmd/mining/) - 8 issues found
|
||||
- [x] Domain 4: Angular Frontend (ui/src/app/) - 3 issues found
|
||||
|
||||
**Total Issues Identified: 17**
|
||||
|
||||
---
|
||||
|
||||
## Recommended Priority Order
|
||||
|
||||
### Immediate (Security Critical)
|
||||
1. **CRIT-001:** Path traversal in tar extraction - Remote code execution risk
|
||||
2. **CRIT-002:** XSS vulnerability in console - Script injection risk
|
||||
3. **CRIT-003:** Resource leak in node serve - Service stability
|
||||
4. **CRIT-004/005:** Path traversal in CLI - Arbitrary file read
|
||||
|
||||
### This Week (Data Integrity)
|
||||
5. **HIGH-001:** Race condition in timeout middleware
|
||||
6. **HIGH-002:** Missing rollback in UpdateProfile
|
||||
7. **HIGH-003:** Type confusion in update command
|
||||
8. **HIGH-006:** Hardcoded HTTP URLs
|
||||
|
||||
### Next Sprint (Stability)
|
||||
9. **HIGH-004/005:** Missing cleanup and scanner error checks
|
||||
10. **MED-001:** rand.Read error check
|
||||
11. **MED-003:** Config validation for profiles
|
||||
|
||||
### Backlog (Quality)
|
||||
- MED-002, MED-004, MED-005, MED-006
|
||||
|
||||
---
|
||||
|
||||
## Positive Observations
|
||||
|
||||
The codebase demonstrates good practices:
|
||||
- Proper mutex usage for concurrent access
|
||||
- `sync.Once` for safe shutdown patterns
|
||||
- Rate limiting in P2P transport
|
||||
- Challenge-response auth with constant-time comparison
|
||||
- Message size limits and deduplication
|
||||
- Context cancellation handling
|
||||
- No dynamic code execution or localStorage usage in frontend
|
||||
343
CODE_REVIEW_PARALLEL.md
Normal file
343
CODE_REVIEW_PARALLEL.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# Code Review Findings - Mining Project Enterprise Audit
|
||||
|
||||
**Generated:** 2025-12-31
|
||||
**Reviewed by:** 4 Parallel Code Reviewers (2 Opus, 2 Sonnet)
|
||||
|
||||
---
|
||||
|
||||
## Review Domains
|
||||
|
||||
- [x] Domain 1: Core Mining (`pkg/mining/`) - Opus
|
||||
- [x] Domain 2: P2P Networking (`pkg/node/`) - Opus
|
||||
- [x] Domain 3: CLI Commands (`cmd/`) - Sonnet
|
||||
- [x] Domain 4: Angular Frontend (`ui/`) - Sonnet
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Domain | Critical | High | Medium | Total |
|
||||
|--------|----------|------|--------|-------|
|
||||
| Core Mining | 0 | 3 | 2 | 5 |
|
||||
| P2P Networking | 2 | 3 | 0 | 5 |
|
||||
| CLI Commands | 2 | 2 | 0 | 4 |
|
||||
| Angular Frontend | 2 | 3 | 0 | 5 |
|
||||
| **TOTAL** | **6** | **11** | **2** | **19** |
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### CRIT-001: Panic from Short Public Key in peer.go
|
||||
- **File:** `pkg/node/peer.go:159,167`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 95%
|
||||
|
||||
The `AllowPublicKey` and `RevokePublicKey` functions access `publicKey[:16]` for logging without validating length. An attacker providing a short public key will cause a panic.
|
||||
|
||||
```go
|
||||
logging.Debug("public key added to allowlist", logging.Fields{"key": publicKey[:16] + "..."})
|
||||
```
|
||||
|
||||
**Fix:** Add length check before string slicing:
|
||||
```go
|
||||
keyPreview := publicKey
|
||||
if len(publicKey) > 16 {
|
||||
keyPreview = publicKey[:16] + "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CRIT-002: Panic from Short Public Key in transport.go
|
||||
- **File:** `pkg/node/transport.go:470`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 95%
|
||||
|
||||
During handshake rejection logging, `payload.Identity.PublicKey[:16]` is accessed without length validation. Malicious peers can crash the transport.
|
||||
|
||||
**Fix:** Use same safe string prefix function as CRIT-001.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-003: Race Condition on Global Variables in node.go
|
||||
- **File:** `cmd/mining/cmd/node.go:14-17,236-258`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 95%
|
||||
|
||||
Global variables `nodeManager` and `peerRegistry` are initialized with a check-then-act pattern without synchronization, causing race conditions.
|
||||
|
||||
```go
|
||||
func getNodeManager() (*node.NodeManager, error) {
|
||||
if nodeManager == nil { // RACE
|
||||
nodeManager, err = node.NewNodeManager() // Multiple initializations possible
|
||||
}
|
||||
return nodeManager, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Use `sync.Once` for thread-safe lazy initialization:
|
||||
```go
|
||||
var nodeManagerOnce sync.Once
|
||||
func getNodeManager() (*node.NodeManager, error) {
|
||||
nodeManagerOnce.Do(func() {
|
||||
nodeManager, nodeManagerErr = node.NewNodeManager()
|
||||
})
|
||||
return nodeManager, nodeManagerErr
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CRIT-004: Race Condition on Global Variables in remote.go
|
||||
- **File:** `cmd/mining/cmd/remote.go:12-15,323-351`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 95%
|
||||
|
||||
Same check-then-act race condition on `controller` and `transport` global variables.
|
||||
|
||||
**Fix:** Use `sync.Once` pattern.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-005: XSS via bypassSecurityTrustHtml in Console
|
||||
- **File:** `ui/src/app/pages/console/console.component.ts:534-575`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 85%
|
||||
|
||||
The `ansiToHtml()` method uses `DomSanitizer.bypassSecurityTrustHtml()` to render ANSI-formatted log output. A compromised miner or pool could inject malicious payloads.
|
||||
|
||||
**Fix:** Remove `bypassSecurityTrustHtml()`, use property binding with pre-sanitized class names, or use a security-audited ANSI library.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-006: Missing Input Validation on HTTP Endpoints
|
||||
- **File:** `ui/src/app/miner.service.ts:352-356`, `ui/src/app/node.service.ts:220-247`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 90%
|
||||
|
||||
Multiple HTTP requests pass user-controlled data directly to backend without client-side validation, exposing to command injection via `sendStdin()`, path traversal via `minerName`, and SSRF via peer addresses.
|
||||
|
||||
**Fix:** Add validation for `minerName` (whitelist alphanumeric + hyphens), sanitize `input` in `sendStdin()`, validate peer addresses format.
|
||||
|
||||
---
|
||||
|
||||
## High Priority Issues
|
||||
|
||||
### HIGH-001: TTMiner Goroutine Leak
|
||||
- **File:** `pkg/mining/ttminer_start.go:75-108`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 85%
|
||||
|
||||
In TTMiner `Start()`, the inner goroutine that calls `cmd.Wait()` can leak if process kill timeout occurs but Wait() never returns.
|
||||
|
||||
**Fix:** Add secondary timeout for inner goroutine like XMRig implementation.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-002: Request Timeout Middleware Race
|
||||
- **File:** `pkg/mining/service.go:339-357`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 82%
|
||||
|
||||
The `requestTimeoutMiddleware` spawns a goroutine that continues running after timeout, potentially writing to aborted response.
|
||||
|
||||
**Fix:** Use request context cancellation or document handlers must check `c.IsAborted()`.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-003: Peer Registry AllowPublicKey Index Panic
|
||||
- **File:** `pkg/node/peer.go:159,167`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 88%
|
||||
|
||||
Same issue as CRIT-001 (duplicate finding from different reviewer).
|
||||
|
||||
---
|
||||
|
||||
### HIGH-004: Unbounded Tar File Extraction
|
||||
- **File:** `pkg/node/bundle.go:314`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 85%
|
||||
|
||||
`extractTarball` uses `io.Copy(f, tr)` without limiting file size, allowing decompression bombs.
|
||||
|
||||
**Fix:**
|
||||
```go
|
||||
const maxFileSize = 100 * 1024 * 1024
|
||||
limitedReader := io.LimitReader(tr, min(hdr.Size, maxFileSize))
|
||||
io.Copy(f, limitedReader)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HIGH-005: Unvalidated Lines Parameter (DoS)
|
||||
- **File:** `pkg/node/worker.go:266-276`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 82%
|
||||
|
||||
`handleGetLogs` passes `Lines` parameter without validation, allowing memory exhaustion.
|
||||
|
||||
**Fix:** Add validation: `if payload.Lines > 10000 { payload.Lines = 10000 }`
|
||||
|
||||
---
|
||||
|
||||
### HIGH-006: Missing TLS Configuration Hardening
|
||||
- **File:** `pkg/node/transport.go:206-216`
|
||||
- **Domain:** P2P Networking
|
||||
- **Confidence:** 80%
|
||||
|
||||
TLS uses default configuration without minimum version or cipher suite restrictions.
|
||||
|
||||
**Fix:** Add TLS config with `MinVersion: tls.VersionTLS12` and restricted cipher suites.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-007: Missing Input Validation on Pool/Wallet
|
||||
- **File:** `cmd/mining/cmd/serve.go:95-112`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 85%
|
||||
|
||||
Interactive shell accepts pool/wallet without format validation.
|
||||
|
||||
**Fix:** Validate pool URL prefix (stratum+tcp:// or stratum+ssl://), length limits.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-008: Incomplete Signal Handling
|
||||
- **File:** `cmd/mining/cmd/node.go:162-176`
|
||||
- **Domain:** CLI Commands
|
||||
- **Confidence:** 82%
|
||||
|
||||
Missing SIGHUP handling, no force cleanup if Stop() fails.
|
||||
|
||||
**Fix:** Add SIGHUP to signal handling, implement forced cleanup on Stop() failure.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-009: Insecure WebSocket Message Handling
|
||||
- **File:** `ui/src/app/websocket.service.ts:155-168`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 82%
|
||||
|
||||
WebSocket messages parsed without validation or type guards.
|
||||
|
||||
**Fix:** Validate message structure, implement type guards, validate event types against whitelist.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-010: Memory Leaks from Unsubscribed Observables
|
||||
- **File:** `ui/src/app/pages/profiles/profiles.component.ts`, `workers.component.ts`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 85%
|
||||
|
||||
Components subscribe to observables in event handlers without proper cleanup.
|
||||
|
||||
**Fix:** Use `takeUntil(destroy$)` pattern, implement `OnDestroy`.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-011: Error Information Disclosure
|
||||
- **File:** `ui/src/app/pages/profiles/profiles.component.ts:590-593`, `setup-wizard.component.ts:43-52`
|
||||
- **Domain:** Angular Frontend
|
||||
- **Confidence:** 80%
|
||||
|
||||
Error handlers display detailed error messages exposing internal API structure.
|
||||
|
||||
**Fix:** Create generic error messages, log details only in dev mode.
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
### MED-001: Profile Manager DeleteProfile Missing Rollback
|
||||
- **File:** `pkg/mining/profile_manager.go:146-156`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 80%
|
||||
|
||||
If `saveProfiles()` fails after deletion, in-memory and on-disk state become inconsistent.
|
||||
|
||||
**Fix:** Store reference to deleted profile and restore on save failure.
|
||||
|
||||
---
|
||||
|
||||
### MED-002: Config Validation Missing for CLIArgs
|
||||
- **File:** `pkg/mining/mining.go:162-213`
|
||||
- **Domain:** Core Mining
|
||||
- **Confidence:** 83%
|
||||
|
||||
`Config.Validate()` doesn't validate `CLIArgs` field for shell characters.
|
||||
|
||||
**Fix:** Add CLIArgs validation in Config.Validate().
|
||||
|
||||
---
|
||||
|
||||
## Recommended Priority Order
|
||||
|
||||
### Immediate (Crash Prevention)
|
||||
1. CRIT-001: Panic from short public key in peer.go
|
||||
2. CRIT-002: Panic from short public key in transport.go
|
||||
3. CRIT-003: Race condition in node.go
|
||||
4. CRIT-004: Race condition in remote.go
|
||||
|
||||
### This Week (Security Critical)
|
||||
5. CRIT-005: XSS via bypassSecurityTrustHtml
|
||||
6. CRIT-006: Missing input validation
|
||||
7. HIGH-004: Unbounded tar extraction
|
||||
8. HIGH-006: Missing TLS hardening
|
||||
|
||||
### Next Sprint (Stability)
|
||||
9. HIGH-001: TTMiner goroutine leak
|
||||
10. HIGH-002: Timeout middleware race
|
||||
11. HIGH-005: Unvalidated Lines parameter
|
||||
12. HIGH-007: Pool/wallet validation
|
||||
13. HIGH-008: Signal handling
|
||||
14. HIGH-009: WebSocket validation
|
||||
15. HIGH-010: Memory leaks
|
||||
16. HIGH-011: Error disclosure
|
||||
|
||||
### Backlog (Quality)
|
||||
17. MED-001: Profile manager rollback
|
||||
18. MED-002: CLIArgs validation
|
||||
|
||||
---
|
||||
|
||||
## Positive Findings (Good Practices)
|
||||
|
||||
The codebase demonstrates several enterprise-quality patterns:
|
||||
|
||||
**Core Mining:**
|
||||
- Proper mutex usage with separate read/write locks
|
||||
- Panic recovery in goroutines
|
||||
- Graceful shutdown with `sync.Once`
|
||||
- Atomic writes for file operations
|
||||
- Input validation with shell character blocking
|
||||
|
||||
**P2P Networking:**
|
||||
- Constant-time comparison with `hmac.Equal`
|
||||
- Path traversal protection in tar extraction
|
||||
- Symlinks/hard links blocked
|
||||
- Message deduplication
|
||||
- Per-peer rate limiting
|
||||
|
||||
**CLI Commands:**
|
||||
- Proper argument separation (no shell execution)
|
||||
- Path validation in doctor.go
|
||||
- Instance name sanitization with regex
|
||||
|
||||
**Angular Frontend:**
|
||||
- No dynamic code execution patterns
|
||||
- No localStorage/sessionStorage usage
|
||||
- Type-safe HTTP client
|
||||
- ShadowDOM encapsulation
|
||||
|
||||
---
|
||||
|
||||
## Review Completion Status
|
||||
|
||||
- [x] Core Mining (`pkg/mining/`) - 5 issues found
|
||||
- [x] P2P Networking (`pkg/node/`) - 5 issues found
|
||||
- [x] CLI Commands (`cmd/`) - 4 issues found
|
||||
- [x] Angular Frontend (`ui/`) - 5 issues found
|
||||
|
||||
**Total Issues Identified: 19**
|
||||
307
PARALLEL_CODE_REVIEW.md
Normal file
307
PARALLEL_CODE_REVIEW.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# Parallel Code Review with Claude Code
|
||||
|
||||
A reproducible pattern for running multiple Opus code reviewers in parallel across different domains of a codebase.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This technique spawns 6-10 specialized code review agents simultaneously, each focused on a specific domain. Results are consolidated into a single TODO.md with prioritized findings.
|
||||
|
||||
**Best for:**
|
||||
- Large C/C++/Go/Rust codebases
|
||||
- Security audits
|
||||
- Pre-release quality gates
|
||||
- Technical debt assessment
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Define Review Domains
|
||||
|
||||
Analyze your codebase structure and identify 6-10 logical domains. Each domain should be:
|
||||
- Self-contained enough for independent review
|
||||
- Small enough to review thoroughly (5-20 key files)
|
||||
- Aligned with architectural boundaries
|
||||
|
||||
### Example Domain Breakdown (C++ Miner)
|
||||
|
||||
```
|
||||
1. Entry Point & App Lifecycle -> src/App.cpp, src/xmrig.cpp
|
||||
2. Core Controller & Miner -> src/core/
|
||||
3. CPU Backend -> src/backend/cpu/, src/backend/common/
|
||||
4. GPU Backends -> src/backend/opencl/, src/backend/cuda/
|
||||
5. Crypto Algorithms -> src/crypto/
|
||||
6. Network & Stratum -> src/base/net/stratum/, src/net/
|
||||
7. HTTP REST API -> src/base/api/, src/base/net/http/
|
||||
8. Hardware Access -> src/hw/, src/base/kernel/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Output File
|
||||
|
||||
Create a skeleton TODO.md to track progress:
|
||||
|
||||
```markdown
|
||||
# Code Review Findings - [Project Name]
|
||||
|
||||
Generated: [DATE]
|
||||
|
||||
## Review Domains
|
||||
|
||||
- [ ] Domain 1
|
||||
- [ ] Domain 2
|
||||
...
|
||||
|
||||
## Critical Issues
|
||||
_Pending review..._
|
||||
|
||||
## High Priority Issues
|
||||
_Pending review..._
|
||||
|
||||
## Medium Priority Issues
|
||||
_Pending review..._
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Launch Parallel Reviewers
|
||||
|
||||
Use this prompt template for each domain. Launch ALL domains simultaneously in a single message with multiple Task tool calls.
|
||||
|
||||
### Reviewer Prompt Template
|
||||
|
||||
```
|
||||
You are reviewing the [LANGUAGE] [PROJECT] for enterprise quality. Focus on:
|
||||
|
||||
**Domain: [DOMAIN NAME]**
|
||||
- `path/to/file1.cpp` - description
|
||||
- `path/to/file2.cpp` - description
|
||||
- `path/to/directory/` - description
|
||||
|
||||
Look for:
|
||||
1. Memory leaks, resource management issues
|
||||
2. Thread safety and race conditions
|
||||
3. Error handling gaps
|
||||
4. Null pointer dereferences
|
||||
5. Security vulnerabilities
|
||||
6. Input validation issues
|
||||
|
||||
Report your findings in a structured format with:
|
||||
- File path and line number
|
||||
- Issue severity (CRITICAL/HIGH/MEDIUM/LOW)
|
||||
- Confidence percentage (only report issues with 80%+ confidence)
|
||||
- Description of the problem
|
||||
- Suggested fix
|
||||
|
||||
Work from: /absolute/path/to/project
|
||||
```
|
||||
|
||||
### Launch Command Pattern
|
||||
|
||||
```
|
||||
Use Task tool with:
|
||||
- subagent_type: "feature-dev:code-reviewer"
|
||||
- run_in_background: true
|
||||
- description: "Review [Domain Name]"
|
||||
- prompt: [Template above filled in]
|
||||
|
||||
Launch ALL domains in ONE message to run in parallel.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Collect Results
|
||||
|
||||
After launching, wait for all agents to complete:
|
||||
|
||||
```
|
||||
Use TaskOutput tool with:
|
||||
- task_id: [agent_id from launch]
|
||||
- block: true
|
||||
- timeout: 120000
|
||||
```
|
||||
|
||||
Collect all results in parallel once agents start completing.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Consolidate Findings
|
||||
|
||||
Structure the final TODO.md with this format:
|
||||
|
||||
```markdown
|
||||
# Code Review Findings - [Project] Enterprise Audit
|
||||
|
||||
**Generated:** YYYY-MM-DD
|
||||
**Reviewed by:** N Parallel Opus Code Reviewers
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Domain | Critical | High | Medium | Total |
|
||||
|--------|----------|------|--------|-------|
|
||||
| Domain 1 | X | Y | Z | N |
|
||||
| Domain 2 | X | Y | Z | N |
|
||||
| **TOTAL** | **X** | **Y** | **Z** | **N** |
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### CRIT-001: [Short Title]
|
||||
- **File:** `path/to/file.cpp:LINE`
|
||||
- **Domain:** [Domain Name]
|
||||
- **Confidence:** XX%
|
||||
|
||||
[Description of the issue]
|
||||
|
||||
**Fix:** [Suggested fix]
|
||||
|
||||
---
|
||||
|
||||
[Repeat for each critical issue]
|
||||
|
||||
## High Priority Issues
|
||||
|
||||
### HIGH-001: [Short Title]
|
||||
- **File:** `path/to/file.cpp:LINE`
|
||||
- **Domain:** [Domain Name]
|
||||
- **Confidence:** XX%
|
||||
|
||||
[Description]
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
[Same format]
|
||||
|
||||
---
|
||||
|
||||
## Recommended Priority Order
|
||||
|
||||
### Immediate (Security Critical)
|
||||
1. CRIT-XXX: [title]
|
||||
2. CRIT-XXX: [title]
|
||||
|
||||
### This Week (Data Integrity)
|
||||
3. CRIT-XXX: [title]
|
||||
4. HIGH-XXX: [title]
|
||||
|
||||
### Next Sprint (Stability)
|
||||
5. HIGH-XXX: [title]
|
||||
|
||||
### Backlog (Quality)
|
||||
- MED-XXX items
|
||||
|
||||
---
|
||||
|
||||
## Review Completion Status
|
||||
|
||||
- [x] Domain 1 - N issues found
|
||||
- [x] Domain 2 - N issues found
|
||||
- [ ] Domain 3 - Review incomplete
|
||||
|
||||
**Total Issues Identified: N**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domain-Specific Prompts
|
||||
|
||||
### For C/C++ Projects
|
||||
|
||||
```
|
||||
Look for:
|
||||
1. Memory leaks, resource management issues (RAII violations)
|
||||
2. Buffer overflows, bounds checking
|
||||
3. Thread safety and race conditions
|
||||
4. Use-after-free, double-free
|
||||
5. Null pointer dereferences
|
||||
6. Integer overflow/underflow
|
||||
7. Format string vulnerabilities
|
||||
8. Uninitialized variables
|
||||
```
|
||||
|
||||
### For Go Projects
|
||||
|
||||
```
|
||||
Look for:
|
||||
1. Goroutine leaks
|
||||
2. Race conditions (run with -race)
|
||||
3. Nil pointer dereferences
|
||||
4. Error handling gaps (ignored errors)
|
||||
5. Context cancellation issues
|
||||
6. Channel deadlocks
|
||||
7. Slice/map concurrent access
|
||||
8. Resource cleanup (defer patterns)
|
||||
```
|
||||
|
||||
### For Network/API Code
|
||||
|
||||
```
|
||||
Look for:
|
||||
1. Buffer overflows in protocol parsing
|
||||
2. TLS/SSL configuration issues
|
||||
3. Input validation vulnerabilities
|
||||
4. Authentication/authorization gaps
|
||||
5. Timing attacks in comparisons
|
||||
6. Connection/request limits (DoS)
|
||||
7. CORS misconfigurations
|
||||
8. Information disclosure
|
||||
```
|
||||
|
||||
### For Crypto Code
|
||||
|
||||
```
|
||||
Look for:
|
||||
1. Side-channel vulnerabilities
|
||||
2. Weak random number generation
|
||||
3. Key/secret exposure in logs
|
||||
4. Timing attacks
|
||||
5. Buffer overflows in crypto ops
|
||||
6. Integer overflow in calculations
|
||||
7. Proper constant-time operations
|
||||
8. Key lifecycle management
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
1. **Be specific about file paths** - Give reviewers exact paths to focus on
|
||||
2. **Set confidence threshold** - Only report 80%+ confidence issues
|
||||
3. **Include context** - Mention the project type, language, and any special patterns
|
||||
4. **Limit scope** - 5-20 files per domain is ideal
|
||||
5. **Run in parallel** - Launch all agents in one message for efficiency
|
||||
6. **Use background mode** - `run_in_background: true` allows parallel execution
|
||||
7. **Consolidate immediately** - Write findings while context is fresh
|
||||
|
||||
---
|
||||
|
||||
## Example Invocation
|
||||
|
||||
```
|
||||
"Spin up Opus code reviewers to analyze this codebase for enterprise quality.
|
||||
Create a TODO.md with findings organized by severity."
|
||||
```
|
||||
|
||||
This triggers:
|
||||
1. Domain identification from project structure
|
||||
2. Parallel agent launch (6-10 reviewers)
|
||||
3. Result collection
|
||||
4. Consolidated TODO.md generation
|
||||
|
||||
---
|
||||
|
||||
## Metrics
|
||||
|
||||
Typical results for a medium-sized project (50-100k LOC):
|
||||
|
||||
- **Time:** 3-5 minutes for full parallel review
|
||||
- **Issues found:** 30-60 total
|
||||
- **Critical:** 5-15 issues
|
||||
- **High:** 15-25 issues
|
||||
- **False positive rate:** ~10-15% (filtered by confidence threshold)
|
||||
|
|
@ -15,6 +15,23 @@ import (
|
|||
|
||||
const signpostFilename = ".installed-miners"
|
||||
|
||||
// validateConfigPath validates that a config path is within the expected XDG config directory
|
||||
// This prevents path traversal attacks via manipulated signpost files
|
||||
func validateConfigPath(configPath string) error {
|
||||
// Get the expected XDG config base directory
|
||||
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
|
||||
|
||||
// Clean and resolve the config path
|
||||
cleanPath := filepath.Clean(configPath)
|
||||
|
||||
// Check if the path is within the expected directory
|
||||
if !strings.HasPrefix(cleanPath, expectedBase+string(os.PathSeparator)) && cleanPath != expectedBase {
|
||||
return fmt.Errorf("invalid config path: must be within %s", expectedBase)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doctorCmd represents the doctor command
|
||||
var doctorCmd = &cobra.Command{
|
||||
Use: "doctor",
|
||||
|
|
@ -50,7 +67,12 @@ func loadAndDisplayCache() (bool, error) {
|
|||
if err != nil {
|
||||
return false, fmt.Errorf("could not read signpost file: %w", err)
|
||||
}
|
||||
configPath := string(configPathBytes)
|
||||
configPath := strings.TrimSpace(string(configPathBytes))
|
||||
|
||||
// Security: Validate that the config path is within the expected directory
|
||||
if err := validateConfigPath(configPath); err != nil {
|
||||
return false, fmt.Errorf("security error: %w", err)
|
||||
}
|
||||
|
||||
cacheBytes, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
|
|
@ -103,7 +125,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
|||
return fmt.Errorf("could not marshal cache data: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
return fmt.Errorf("could not write cache file: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +134,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
|||
return fmt.Errorf("could not get home directory for signpost: %w", err)
|
||||
}
|
||||
signpostPath := filepath.Join(homeDir, signpostFilename)
|
||||
if err := os.WriteFile(signpostPath, []byte(configPath), 0644); err != nil {
|
||||
if err := os.WriteFile(signpostPath, []byte(configPath), 0600); err != nil {
|
||||
return fmt.Errorf("could not write signpost file: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Note: findPeerByPartialID is defined in remote.go and used for peer lookup
|
||||
|
||||
// peerCmd represents the peer parent command
|
||||
var peerCmd = &cobra.Command{
|
||||
Use: "peer",
|
||||
|
|
@ -114,26 +116,16 @@ var peerRemoveCmd = &cobra.Command{
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
peerID := args[0]
|
||||
|
||||
peer := findPeerByPartialID(peerID)
|
||||
if peer == nil {
|
||||
return fmt.Errorf("peer not found: %s", peerID)
|
||||
}
|
||||
|
||||
pr, err := getPeerRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get peer registry: %w", err)
|
||||
}
|
||||
|
||||
peer := pr.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
// Try partial match
|
||||
for _, p := range pr.ListPeers() {
|
||||
if len(p.ID) >= len(peerID) && p.ID[:len(peerID)] == peerID {
|
||||
peer = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if peer == nil {
|
||||
return fmt.Errorf("peer not found: %s", peerID)
|
||||
}
|
||||
|
||||
if err := pr.RemovePeer(peer.ID); err != nil {
|
||||
return fmt.Errorf("failed to remove peer: %w", err)
|
||||
}
|
||||
|
|
@ -152,22 +144,7 @@ var peerPingCmd = &cobra.Command{
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
peerID := args[0]
|
||||
|
||||
pr, err := getPeerRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get peer registry: %w", err)
|
||||
}
|
||||
|
||||
peer := pr.GetPeer(peerID)
|
||||
if peer == nil {
|
||||
// Try partial match
|
||||
for _, p := range pr.ListPeers() {
|
||||
if len(p.ID) >= len(peerID) && p.ID[:len(peerID)] == peerID {
|
||||
peer = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peer := findPeerByPartialID(peerID)
|
||||
if peer == nil {
|
||||
return fmt.Errorf("peer not found: %s", peerID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,24 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/Snider/Mining/pkg/mining"
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// validateUpdateConfigPath validates that a config path is within the expected XDG config directory
|
||||
func validateUpdateConfigPath(configPath string) error {
|
||||
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
|
||||
cleanPath := filepath.Clean(configPath)
|
||||
if !strings.HasPrefix(cleanPath, expectedBase+string(os.PathSeparator)) && cleanPath != expectedBase {
|
||||
return fmt.Errorf("invalid config path: must be within %s", expectedBase)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateCmd represents the update command
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
|
|
@ -34,20 +46,26 @@ var updateCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not read signpost file: %w", err)
|
||||
}
|
||||
configPath := string(configPathBytes)
|
||||
configPath := strings.TrimSpace(string(configPathBytes))
|
||||
|
||||
// Security: Validate that the config path is within the expected directory
|
||||
if err := validateUpdateConfigPath(configPath); err != nil {
|
||||
return fmt.Errorf("security error: %w", err)
|
||||
}
|
||||
|
||||
cacheBytes, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read cache file from %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
var cachedDetails []*mining.InstallationDetails
|
||||
if err := json.Unmarshal(cacheBytes, &cachedDetails); err != nil {
|
||||
// Fix: Use SystemInfo type (matches what doctor.go saves)
|
||||
var systemInfo mining.SystemInfo
|
||||
if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil {
|
||||
return fmt.Errorf("could not parse cache file: %w", err)
|
||||
}
|
||||
|
||||
updatesFound := false
|
||||
for _, details := range cachedDetails {
|
||||
for _, details := range systemInfo.InstalledMinersInfo {
|
||||
if !details.IsInstalled {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,7 +208,11 @@ func (da *DigestAuth) validateBasic(c *gin.Context, authHeader string) bool {
|
|||
// generateNonce creates a cryptographically random nonce
|
||||
func (da *DigestAuth) generateNonce() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// Cryptographic failure is critical - fall back to time-based nonce
|
||||
// This should never happen on a properly configured system
|
||||
return hex.EncodeToString([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ func (h *EventHub) Run() {
|
|||
if _, ok := h.clients[client]; ok {
|
||||
delete(h.clients, client)
|
||||
client.safeClose()
|
||||
// Decrement WebSocket connection metrics
|
||||
RecordWSConnection(false)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
logging.Debug("client disconnected", logging.Fields{"total": len(h.clients)})
|
||||
|
|
|
|||
|
|
@ -387,3 +387,253 @@ func TestPeerRegistry_Persistence(t *testing.T) {
|
|||
t.Errorf("expected name 'Persistent Peer', got '%s'", loaded.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Security Feature Tests ---
|
||||
|
||||
func TestPeerRegistry_AuthMode(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
// Default should be Open
|
||||
if pr.GetAuthMode() != PeerAuthOpen {
|
||||
t.Errorf("expected default auth mode to be Open, got %d", pr.GetAuthMode())
|
||||
}
|
||||
|
||||
// Set to Allowlist
|
||||
pr.SetAuthMode(PeerAuthAllowlist)
|
||||
if pr.GetAuthMode() != PeerAuthAllowlist {
|
||||
t.Errorf("expected auth mode to be Allowlist after setting, got %d", pr.GetAuthMode())
|
||||
}
|
||||
|
||||
// Set back to Open
|
||||
pr.SetAuthMode(PeerAuthOpen)
|
||||
if pr.GetAuthMode() != PeerAuthOpen {
|
||||
t.Errorf("expected auth mode to be Open after resetting, got %d", pr.GetAuthMode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_PublicKeyAllowlist(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
testKey := "base64PublicKeyExample1234567890123456"
|
||||
|
||||
// Initially key should not be allowed
|
||||
if pr.IsPublicKeyAllowed(testKey) {
|
||||
t.Error("key should not be allowed before adding")
|
||||
}
|
||||
|
||||
// Add key to allowlist
|
||||
pr.AllowPublicKey(testKey)
|
||||
if !pr.IsPublicKeyAllowed(testKey) {
|
||||
t.Error("key should be allowed after adding")
|
||||
}
|
||||
|
||||
// List should contain the key
|
||||
keys := pr.ListAllowedPublicKeys()
|
||||
found := false
|
||||
for _, k := range keys {
|
||||
if k == testKey {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("ListAllowedPublicKeys should contain the added key")
|
||||
}
|
||||
|
||||
// Revoke key
|
||||
pr.RevokePublicKey(testKey)
|
||||
if pr.IsPublicKeyAllowed(testKey) {
|
||||
t.Error("key should not be allowed after revoking")
|
||||
}
|
||||
|
||||
// List should be empty
|
||||
keys = pr.ListAllowedPublicKeys()
|
||||
if len(keys) != 0 {
|
||||
t.Errorf("expected 0 keys after revoke, got %d", len(keys))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_IsPeerAllowed_OpenMode(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
pr.SetAuthMode(PeerAuthOpen)
|
||||
|
||||
// In Open mode, any peer should be allowed
|
||||
if !pr.IsPeerAllowed("unknown-peer", "unknown-key") {
|
||||
t.Error("in Open mode, all peers should be allowed")
|
||||
}
|
||||
|
||||
if !pr.IsPeerAllowed("", "") {
|
||||
t.Error("in Open mode, even empty IDs should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_IsPeerAllowed_AllowlistMode(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
pr.SetAuthMode(PeerAuthAllowlist)
|
||||
|
||||
// Unknown peer with unknown key should be rejected
|
||||
if pr.IsPeerAllowed("unknown-peer", "unknown-key") {
|
||||
t.Error("in Allowlist mode, unknown peers should be rejected")
|
||||
}
|
||||
|
||||
// Pre-registered peer should be allowed
|
||||
peer := &Peer{
|
||||
ID: "registered-peer",
|
||||
Name: "Registered",
|
||||
PublicKey: "registered-key",
|
||||
}
|
||||
pr.AddPeer(peer)
|
||||
|
||||
if !pr.IsPeerAllowed("registered-peer", "any-key") {
|
||||
t.Error("pre-registered peer should be allowed in Allowlist mode")
|
||||
}
|
||||
|
||||
// Peer with allowlisted public key should be allowed
|
||||
pr.AllowPublicKey("allowed-key-1234567890")
|
||||
if !pr.IsPeerAllowed("new-peer", "allowed-key-1234567890") {
|
||||
t.Error("peer with allowlisted key should be allowed")
|
||||
}
|
||||
|
||||
// Unknown peer with non-allowlisted key should still be rejected
|
||||
if pr.IsPeerAllowed("another-peer", "not-allowed-key") {
|
||||
t.Error("peer without allowlisted key should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_PeerNameValidation(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
peerName string
|
||||
shouldErr bool
|
||||
}{
|
||||
{"empty name allowed", "", false},
|
||||
{"single char", "A", false},
|
||||
{"simple name", "MyPeer", false},
|
||||
{"name with hyphen", "my-peer", false},
|
||||
{"name with underscore", "my_peer", false},
|
||||
{"name with space", "My Peer", false},
|
||||
{"name with numbers", "Peer123", false},
|
||||
{"max length name", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789AB", false},
|
||||
{"too long name", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABC", true},
|
||||
{"starts with hyphen", "-peer", true},
|
||||
{"ends with hyphen", "peer-", true},
|
||||
{"special chars", "peer@host", true},
|
||||
{"unicode chars", "peer\u0000name", true},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
peer := &Peer{
|
||||
ID: "test-peer-" + string(rune('A'+i)),
|
||||
Name: tc.peerName,
|
||||
}
|
||||
err := pr.AddPeer(peer)
|
||||
if tc.shouldErr && err == nil {
|
||||
t.Errorf("expected error for name '%s' but got none", tc.peerName)
|
||||
} else if !tc.shouldErr && err != nil {
|
||||
t.Errorf("unexpected error for name '%s': %v", tc.peerName, err)
|
||||
}
|
||||
// Clean up for next test
|
||||
if err == nil {
|
||||
pr.RemovePeer(peer.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_ScoreRecording(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
peer := &Peer{
|
||||
ID: "score-record-test",
|
||||
Name: "Score Peer",
|
||||
Score: 50, // Start at neutral
|
||||
}
|
||||
pr.AddPeer(peer)
|
||||
|
||||
// Record successes - score should increase
|
||||
for i := 0; i < 5; i++ {
|
||||
pr.RecordSuccess("score-record-test")
|
||||
}
|
||||
updated := pr.GetPeer("score-record-test")
|
||||
if updated.Score <= 50 {
|
||||
t.Errorf("score should increase after successes, got %f", updated.Score)
|
||||
}
|
||||
|
||||
// Record failures - score should decrease
|
||||
initialScore := updated.Score
|
||||
for i := 0; i < 3; i++ {
|
||||
pr.RecordFailure("score-record-test")
|
||||
}
|
||||
updated = pr.GetPeer("score-record-test")
|
||||
if updated.Score >= initialScore {
|
||||
t.Errorf("score should decrease after failures, got %f (was %f)", updated.Score, initialScore)
|
||||
}
|
||||
|
||||
// Record timeouts - score should decrease
|
||||
initialScore = updated.Score
|
||||
pr.RecordTimeout("score-record-test")
|
||||
updated = pr.GetPeer("score-record-test")
|
||||
if updated.Score >= initialScore {
|
||||
t.Errorf("score should decrease after timeout, got %f (was %f)", updated.Score, initialScore)
|
||||
}
|
||||
|
||||
// Score should be clamped to min/max
|
||||
for i := 0; i < 100; i++ {
|
||||
pr.RecordSuccess("score-record-test")
|
||||
}
|
||||
updated = pr.GetPeer("score-record-test")
|
||||
if updated.Score > ScoreMaximum {
|
||||
t.Errorf("score should be clamped to max %f, got %f", ScoreMaximum, updated.Score)
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
pr.RecordFailure("score-record-test")
|
||||
}
|
||||
updated = pr.GetPeer("score-record-test")
|
||||
if updated.Score < ScoreMinimum {
|
||||
t.Errorf("score should be clamped to min %f, got %f", ScoreMinimum, updated.Score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRegistry_GetPeersByScore(t *testing.T) {
|
||||
pr, cleanup := setupTestPeerRegistry(t)
|
||||
defer cleanup()
|
||||
|
||||
// Add peers with different scores
|
||||
peers := []*Peer{
|
||||
{ID: "low-score", Name: "Low", Score: 20},
|
||||
{ID: "high-score", Name: "High", Score: 90},
|
||||
{ID: "mid-score", Name: "Mid", Score: 50},
|
||||
}
|
||||
|
||||
for _, p := range peers {
|
||||
pr.AddPeer(p)
|
||||
}
|
||||
|
||||
sorted := pr.GetPeersByScore()
|
||||
if len(sorted) != 3 {
|
||||
t.Fatalf("expected 3 peers, got %d", len(sorted))
|
||||
}
|
||||
|
||||
// Should be sorted by score descending
|
||||
if sorted[0].ID != "high-score" {
|
||||
t.Errorf("first peer should be high-score, got %s", sorted[0].ID)
|
||||
}
|
||||
if sorted[1].ID != "mid-score" {
|
||||
t.Errorf("second peer should be mid-score, got %s", sorted[1].ID)
|
||||
}
|
||||
if sorted[2].ID != "low-score" {
|
||||
t.Errorf("third peer should be low-score, got %s", sorted[2].ID)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
ui/src/app/api-config.service.ts
Normal file
117
ui/src/app/api-config.service.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { Injectable, InjectionToken, inject } from '@angular/core';
|
||||
|
||||
/**
|
||||
* API Configuration interface for dependency injection
|
||||
*/
|
||||
export interface ApiConfig {
|
||||
/** Base URL for HTTP API (e.g., 'http://localhost:9090') */
|
||||
apiHost?: string;
|
||||
/** API namespace (e.g., '/api/v1/mining') */
|
||||
apiNamespace?: string;
|
||||
/** Force HTTPS/WSS even on localhost */
|
||||
forceSecure?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection token for providing custom API configuration
|
||||
*/
|
||||
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
|
||||
|
||||
/**
|
||||
* Service to provide consistent API URL configuration across the application.
|
||||
*
|
||||
* By default, it auto-detects the protocol and host from the current page location,
|
||||
* which allows the app to work correctly whether served over HTTP or HTTPS.
|
||||
*
|
||||
* The configuration can be customized by providing API_CONFIG in the app module:
|
||||
*
|
||||
* @example
|
||||
* // In app.config.ts
|
||||
* providers: [
|
||||
* { provide: API_CONFIG, useValue: { apiHost: 'https://api.example.com' } }
|
||||
* ]
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiConfigService {
|
||||
private readonly config = inject(API_CONFIG, { optional: true });
|
||||
|
||||
/** Default API namespace */
|
||||
private readonly defaultNamespace = '/api/v1/mining';
|
||||
|
||||
/**
|
||||
* Get the base URL for HTTP API requests
|
||||
* @returns Full HTTP base URL (e.g., 'https://localhost:9090/api/v1/mining')
|
||||
*/
|
||||
get apiBaseUrl(): string {
|
||||
const host = this.getApiHost();
|
||||
const namespace = this.config?.apiNamespace ?? this.defaultNamespace;
|
||||
return `${host}${namespace}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WebSocket URL for event streaming
|
||||
* @returns Full WebSocket URL (e.g., 'wss://localhost:9090/api/v1/mining/ws/events')
|
||||
*/
|
||||
get wsUrl(): string {
|
||||
const host = this.getApiHost();
|
||||
const namespace = this.config?.apiNamespace ?? this.defaultNamespace;
|
||||
const protocol = host.startsWith('https') ? 'wss' : 'ws';
|
||||
// Replace http(s):// with ws(s)://
|
||||
const wsHost = host.replace(/^https?:\/\//, `${protocol}://`);
|
||||
return `${wsHost}${namespace}/ws/events`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API host (protocol + hostname + port)
|
||||
*/
|
||||
private getApiHost(): string {
|
||||
// If custom host is configured, use it
|
||||
if (this.config?.apiHost) {
|
||||
return this.config.apiHost;
|
||||
}
|
||||
|
||||
// Auto-detect from current page location
|
||||
if (typeof window !== 'undefined' && window.location) {
|
||||
const { protocol, hostname, port } = window.location;
|
||||
|
||||
// Determine if we should use secure protocol
|
||||
const isSecure = protocol === 'https:' || this.config?.forceSecure;
|
||||
const httpProtocol = isSecure ? 'https' : 'http';
|
||||
|
||||
// Default to port 9090 if we're on a dev server (4200) or no port specified
|
||||
const apiPort = this.getApiPort(port);
|
||||
|
||||
return `${httpProtocol}://${hostname}:${apiPort}`;
|
||||
}
|
||||
|
||||
// Fallback for SSR or non-browser environments
|
||||
return 'http://localhost:9090';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the API port based on the current page port
|
||||
*/
|
||||
private getApiPort(currentPort: string): string {
|
||||
// If we're on the Angular dev server (4200), use the API default port
|
||||
if (currentPort === '4200' || currentPort === '') {
|
||||
return '9090';
|
||||
}
|
||||
// Otherwise, assume we're served from the same port as the API
|
||||
return currentPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the connection is secure (HTTPS/WSS)
|
||||
*/
|
||||
get isSecure(): boolean {
|
||||
if (this.config?.forceSecure) {
|
||||
return true;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.protocol === 'https:';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
82
ui/src/app/components/sidebar/icon.component.ts
Normal file
82
ui/src/app/components/sidebar/icon.component.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
export type IconName =
|
||||
| 'dashboard'
|
||||
| 'workers'
|
||||
| 'console'
|
||||
| 'pools'
|
||||
| 'profiles'
|
||||
| 'miners'
|
||||
| 'nodes';
|
||||
|
||||
/**
|
||||
* Icon component that renders SVG icons without using innerHTML.
|
||||
* This avoids the need for bypassSecurityTrustHtml and provides
|
||||
* a safer, more maintainable approach to icon rendering.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-icon',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
@switch (name()) {
|
||||
@case ('dashboard') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('workers') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('console') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('pools') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('profiles') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('miners') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
|
||||
</svg>
|
||||
}
|
||||
@case ('nodes') {
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class IconComponent {
|
||||
name = input.required<IconName>();
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { Component, signal, output, input, inject, HostListener } from '@angular/core';
|
||||
import { Component, signal, output, input, HostListener } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { IconComponent, IconName } from './icon.component';
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: SafeHtml;
|
||||
icon: IconName;
|
||||
route: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, IconComponent],
|
||||
template: `
|
||||
<!-- Mobile menu button (visible on small screens) -->
|
||||
<button class="mobile-menu-btn" (click)="toggleMobileMenu()">
|
||||
|
|
@ -61,7 +61,9 @@ interface NavItem {
|
|||
[class.active]="currentRoute() === item.route"
|
||||
(click)="navigateAndClose(item.route)"
|
||||
[title]="collapsed() && !mobileOpen() ? item.label : ''">
|
||||
<span class="nav-icon" [innerHTML]="item.icon"></span>
|
||||
<span class="nav-icon">
|
||||
<app-icon [name]="item.icon" />
|
||||
</span>
|
||||
@if (!collapsed() || mobileOpen()) {
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
}
|
||||
|
|
@ -216,11 +218,6 @@ interface NavItem {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon :deep(svg) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -296,8 +293,6 @@ interface NavItem {
|
|||
`]
|
||||
})
|
||||
export class SidebarComponent {
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
collapsed = signal(false);
|
||||
mobileOpen = signal(false);
|
||||
currentRoute = input<string>('dashboard');
|
||||
|
|
@ -312,54 +307,15 @@ export class SidebarComponent {
|
|||
}
|
||||
|
||||
navItems: NavItem[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
route: 'dashboard',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'workers',
|
||||
label: 'Workers',
|
||||
route: 'workers',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'console',
|
||||
label: 'Console',
|
||||
route: 'console',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'pools',
|
||||
label: 'Pools',
|
||||
route: 'pools',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'profiles',
|
||||
label: 'Profiles',
|
||||
route: 'profiles',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'miners',
|
||||
label: 'Miners',
|
||||
route: 'miners',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>')
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
label: 'Nodes',
|
||||
route: 'nodes',
|
||||
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>')
|
||||
}
|
||||
{ id: 'dashboard', label: 'Dashboard', route: 'dashboard', icon: 'dashboard' },
|
||||
{ id: 'workers', label: 'Workers', route: 'workers', icon: 'workers' },
|
||||
{ id: 'console', label: 'Console', route: 'console', icon: 'console' },
|
||||
{ id: 'pools', label: 'Pools', route: 'pools', icon: 'pools' },
|
||||
{ id: 'profiles', label: 'Profiles', route: 'profiles', icon: 'profiles' },
|
||||
{ id: 'miners', label: 'Miners', route: 'miners', icon: 'miners' },
|
||||
{ id: 'nodes', label: 'Nodes', route: 'nodes', icon: 'nodes' }
|
||||
];
|
||||
|
||||
private trustIcon(svg: string): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(svg);
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
this.collapsed.update(v => !v);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Injectable, signal, computed } from '@angular/core';
|
||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { of, interval, Subscription } from 'rxjs';
|
||||
import { switchMap, catchError, tap } from 'rxjs/operators';
|
||||
import { ApiConfigService } from './api-config.service';
|
||||
|
||||
// --- Node Interfaces ---
|
||||
export interface NodeIdentity {
|
||||
|
|
@ -63,9 +64,14 @@ export interface NodeState {
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class NodeService {
|
||||
private apiBaseUrl = 'http://localhost:9090/api/v1/mining';
|
||||
private readonly apiConfig = inject(ApiConfigService);
|
||||
private pollingSubscription?: Subscription;
|
||||
|
||||
/** Get the API base URL from configuration */
|
||||
private get apiBaseUrl(): string {
|
||||
return this.apiConfig.apiBaseUrl;
|
||||
}
|
||||
|
||||
// --- State Signal ---
|
||||
public state = signal<NodeState>({
|
||||
initialized: false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue